From f387e9021fb7e95d977380bb3c8ac7c52520f9ab Mon Sep 17 00:00:00 2001 From: Xavier Abad <77491413+xabg2@users.noreply.github.com> Date: Fri, 3 Oct 2025 10:27:55 +0200 Subject: [PATCH 01/13] feat: command to sync users with tiers in drive --- src/cli/sync-users-with-drive-tiers.ts | 86 +++++++++++++++++++ src/core/users/MongoDBUsersTiersRepository.ts | 50 +++++++++++ src/services/storage.service.ts | 2 +- tests/src/utils/factory.ts | 1 + 4 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 src/cli/sync-users-with-drive-tiers.ts diff --git a/src/cli/sync-users-with-drive-tiers.ts b/src/cli/sync-users-with-drive-tiers.ts new file mode 100644 index 00000000..cb7e877c --- /dev/null +++ b/src/cli/sync-users-with-drive-tiers.ts @@ -0,0 +1,86 @@ +import axios from 'axios'; +import { MongoClient } from 'mongodb'; +import Stripe from 'stripe'; + +import envVariablesConfig from '../config'; +import { PaymentService } from '../services/payment.service'; +import { UsersService } from '../services/users.service'; +import { UsersRepository } from '../core/users/UsersRepository'; +import { MongoDBUsersRepository } from '../core/users/MongoDBUsersRepository'; +import { StorageService } from '../services/storage.service'; +import { + DisplayBillingRepository, + MongoDBDisplayBillingRepository, +} from '../core/users/MongoDBDisplayBillingRepository'; +import { CouponsRepository } from '../core/coupons/CouponsRepository'; +import { UsersCouponsRepository } from '../core/coupons/UsersCouponsRepository'; +import { MongoDBCouponsRepository } from '../core/coupons/MongoDBCouponsRepository'; +import { MongoDBUsersCouponsRepository } from '../core/coupons/MongoDBUsersCouponsRepository'; +import { ProductsRepository } from '../core/users/ProductsRepository'; +import { MongoDBProductsRepository } from '../core/users/MongoDBProductsRepository'; +import { Bit2MeService } from '../services/bit2me.service'; +import { TiersService } from '../services/tiers.service'; +import { MongoDBTiersRepository, TiersRepository } from '../core/users/MongoDBTiersRepository'; +import { MongoDBUsersTiersRepository, UsersTiersRepository } from '../core/users/MongoDBUsersTiersRepository'; + +const [, , filePath, provider] = process.argv; + +if (!filePath || !provider) { + throw new Error('Missing "filePath" or "provider" params'); +} + +async function main() { + const mongoClient = await new MongoClient(envVariablesConfig.MONGO_URI).connect(); + try { + const stripe = new Stripe(envVariablesConfig.STRIPE_SECRET_KEY, { apiVersion: '2025-02-24.acacia' }); + const usersRepository: UsersRepository = new MongoDBUsersRepository(mongoClient); + const storageService = new StorageService(envVariablesConfig, axios); + const displayBillingRepository: DisplayBillingRepository = new MongoDBDisplayBillingRepository(mongoClient); + const couponsRepository: CouponsRepository = new MongoDBCouponsRepository(mongoClient); + const usersCouponsRepository: UsersCouponsRepository = new MongoDBUsersCouponsRepository(mongoClient); + const productsRepository: ProductsRepository = new MongoDBProductsRepository(mongoClient); + const tiersRepository: TiersRepository = new MongoDBTiersRepository(mongoClient); + const usersTiersRepository: UsersTiersRepository = new MongoDBUsersTiersRepository(mongoClient); + const bit2MeService = new Bit2MeService( + envVariablesConfig, + axios, + envVariablesConfig.CRYPTO_PAYMENTS_PROCESSOR_SECRET_KEY, + envVariablesConfig.CRYPTO_PAYMENTS_PROCESSOR_API_KEY, + envVariablesConfig.CRYPTO_PAYMENTS_PROCESSOR_API_URL, + ); + const paymentService = new PaymentService(stripe, productsRepository, bit2MeService); + const usersService = new UsersService( + usersRepository, + paymentService, + displayBillingRepository, + couponsRepository, + usersCouponsRepository, + envVariablesConfig, + axios, + ); + const tiersService = new TiersService( + usersService, + paymentService, + tiersRepository, + usersTiersRepository, + storageService, + envVariablesConfig, + ); + + const userIdsAndForeignTierId = await usersTiersRepository.getUserTierMappings(); + + userIdsAndForeignTierId.map(async ({ userUuid, foreignTierId }) => { + await storageService.updateUserStorageAndTier(userUuid, undefined, foreignTierId); + }); + } finally { + await mongoClient.close(); + } +} + +main() + .then(() => { + console.log('Users and tiers synced'); + }) + .catch((err) => { + console.error('Error while syncing users: ', err.message); + }); diff --git a/src/core/users/MongoDBUsersTiersRepository.ts b/src/core/users/MongoDBUsersTiersRepository.ts index 518cc456..8e6ae2b5 100644 --- a/src/core/users/MongoDBUsersTiersRepository.ts +++ b/src/core/users/MongoDBUsersTiersRepository.ts @@ -9,6 +9,7 @@ export interface UserTier { } export interface UsersTiersRepository { + getUserTierMappings(isBusiness?: boolean): Promise>; insertTierToUser(userId: User['id'], tierId: Tier['id']): Promise; updateUserTier(userId: User['id'], oldTierId: Tier['id'], newTierId: Tier['id']): Promise; deleteTierFromUser(userId: User['id'], tierId: Tier['id']): Promise; @@ -30,6 +31,55 @@ export class MongoDBUsersTiersRepository implements UsersTiersRepository { this.collection = mongo.db('payments').collection>('users_tiers'); } + async getUserTierMappings(isBusiness = false): Promise> { + const results = await this.collection + .aggregate([ + { + $match: { + 'featuresPerService.drive.workspaces.enabled': isBusiness, + }, + }, + { + $addFields: { + userIdObj: { $toObjectId: '$userId' }, + tierIdObj: { $toObjectId: '$tierId' }, + }, + }, + { + $lookup: { + from: 'users', + localField: 'userIdObj', + foreignField: '_id', + as: 'user', + }, + }, + { + $unwind: '$user', + }, + { + $lookup: { + from: 'tiers', + localField: 'tierIdObj', + foreignField: '_id', + as: 'tier', + }, + }, + { + $unwind: '$tier', + }, + { + $project: { + _id: 0, + userUuid: '$user.uuid', + foreignTierId: '$tier.foreignTierId', + }, + }, + ]) + .toArray(); + + return results as Array<{ userUuid: string; foreignTierId: string }>; + } + async insertTierToUser(userId: User['id'], tierId: Tier['id']): Promise { await this.collection.insertOne({ userId, diff --git a/src/services/storage.service.ts b/src/services/storage.service.ts index 9fbdb106..9868cc46 100644 --- a/src/services/storage.service.ts +++ b/src/services/storage.service.ts @@ -18,7 +18,7 @@ export class StorageService { private readonly axios: Axios, ) {} - async updateUserStorageAndTier(uuid: string, newStorageBytes: number, foreignTierId: string): Promise { + async updateUserStorageAndTier(uuid: string, newStorageBytes?: number, foreignTierId?: string): Promise { const jwt = signToken('5m', this.config.DRIVE_NEW_GATEWAY_SECRET); const params: AxiosRequestConfig = { headers: { diff --git a/tests/src/utils/factory.ts b/tests/src/utils/factory.ts index b23f20ff..77705e78 100644 --- a/tests/src/utils/factory.ts +++ b/tests/src/utils/factory.ts @@ -24,6 +24,7 @@ const getTiersRepository = (): TiersRepository => { const getUsersTiersRepository = (): UsersTiersRepository => { return { + getUserTierMappings: jest.fn(), deleteAllUserTiers: jest.fn(), deleteTierFromUser: jest.fn(), findTierIdByUserId: jest.fn(), From d1e1f22b43106444362724e4e0a846ce8a932110 Mon Sep 17 00:00:00 2001 From: Xavier Abad <77491413+xabg2@users.noreply.github.com> Date: Fri, 3 Oct 2025 11:01:41 +0200 Subject: [PATCH 02/13] fix: usefor loop as map does not wait for async functions --- src/cli/sync-users-with-drive-tiers.ts | 62 +++----------------------- 1 file changed, 5 insertions(+), 57 deletions(-) diff --git a/src/cli/sync-users-with-drive-tiers.ts b/src/cli/sync-users-with-drive-tiers.ts index cb7e877c..19546dba 100644 --- a/src/cli/sync-users-with-drive-tiers.ts +++ b/src/cli/sync-users-with-drive-tiers.ts @@ -1,77 +1,25 @@ import axios from 'axios'; import { MongoClient } from 'mongodb'; -import Stripe from 'stripe'; import envVariablesConfig from '../config'; -import { PaymentService } from '../services/payment.service'; -import { UsersService } from '../services/users.service'; -import { UsersRepository } from '../core/users/UsersRepository'; -import { MongoDBUsersRepository } from '../core/users/MongoDBUsersRepository'; import { StorageService } from '../services/storage.service'; -import { - DisplayBillingRepository, - MongoDBDisplayBillingRepository, -} from '../core/users/MongoDBDisplayBillingRepository'; -import { CouponsRepository } from '../core/coupons/CouponsRepository'; -import { UsersCouponsRepository } from '../core/coupons/UsersCouponsRepository'; -import { MongoDBCouponsRepository } from '../core/coupons/MongoDBCouponsRepository'; -import { MongoDBUsersCouponsRepository } from '../core/coupons/MongoDBUsersCouponsRepository'; -import { ProductsRepository } from '../core/users/ProductsRepository'; -import { MongoDBProductsRepository } from '../core/users/MongoDBProductsRepository'; -import { Bit2MeService } from '../services/bit2me.service'; -import { TiersService } from '../services/tiers.service'; -import { MongoDBTiersRepository, TiersRepository } from '../core/users/MongoDBTiersRepository'; import { MongoDBUsersTiersRepository, UsersTiersRepository } from '../core/users/MongoDBUsersTiersRepository'; -const [, , filePath, provider] = process.argv; +const [, , subType] = process.argv; -if (!filePath || !provider) { - throw new Error('Missing "filePath" or "provider" params'); -} +const isBusiness = subType?.toLowerCase() === 'business'; async function main() { const mongoClient = await new MongoClient(envVariablesConfig.MONGO_URI).connect(); try { - const stripe = new Stripe(envVariablesConfig.STRIPE_SECRET_KEY, { apiVersion: '2025-02-24.acacia' }); - const usersRepository: UsersRepository = new MongoDBUsersRepository(mongoClient); const storageService = new StorageService(envVariablesConfig, axios); - const displayBillingRepository: DisplayBillingRepository = new MongoDBDisplayBillingRepository(mongoClient); - const couponsRepository: CouponsRepository = new MongoDBCouponsRepository(mongoClient); - const usersCouponsRepository: UsersCouponsRepository = new MongoDBUsersCouponsRepository(mongoClient); - const productsRepository: ProductsRepository = new MongoDBProductsRepository(mongoClient); - const tiersRepository: TiersRepository = new MongoDBTiersRepository(mongoClient); const usersTiersRepository: UsersTiersRepository = new MongoDBUsersTiersRepository(mongoClient); - const bit2MeService = new Bit2MeService( - envVariablesConfig, - axios, - envVariablesConfig.CRYPTO_PAYMENTS_PROCESSOR_SECRET_KEY, - envVariablesConfig.CRYPTO_PAYMENTS_PROCESSOR_API_KEY, - envVariablesConfig.CRYPTO_PAYMENTS_PROCESSOR_API_URL, - ); - const paymentService = new PaymentService(stripe, productsRepository, bit2MeService); - const usersService = new UsersService( - usersRepository, - paymentService, - displayBillingRepository, - couponsRepository, - usersCouponsRepository, - envVariablesConfig, - axios, - ); - const tiersService = new TiersService( - usersService, - paymentService, - tiersRepository, - usersTiersRepository, - storageService, - envVariablesConfig, - ); - const userIdsAndForeignTierId = await usersTiersRepository.getUserTierMappings(); + const userIdsAndForeignTierId = await usersTiersRepository.getUserTierMappings(isBusiness); - userIdsAndForeignTierId.map(async ({ userUuid, foreignTierId }) => { + for (const { userUuid, foreignTierId } of userIdsAndForeignTierId) { await storageService.updateUserStorageAndTier(userUuid, undefined, foreignTierId); - }); + } } finally { await mongoClient.close(); } From 719e7b4a475a81807cbc08745efda0b1624ee980 Mon Sep 17 00:00:00 2001 From: Xavier Abad <77491413+xabg2@users.noreply.github.com> Date: Mon, 6 Oct 2025 17:02:51 +0200 Subject: [PATCH 03/13] feat: pass the tierID when interacting with workspaces --- src/app.ts | 2 +- src/controller/business.controller.ts | 20 ++++++- src/services/tiers.service.ts | 9 ++- src/services/users.service.ts | 20 +++++-- tests/src/services/tiers.service.test.ts | 20 ++++--- tests/src/services/users.service.test.ts | 73 +++++++++++++++++++++++- 6 files changed, 125 insertions(+), 19 deletions(-) diff --git a/src/app.ts b/src/app.ts index 1a39a442..3918e5a1 100644 --- a/src/app.ts +++ b/src/app.ts @@ -55,7 +55,7 @@ export async function buildApp({ fastify.register(controller(paymentService, usersService, config, cacheService, licenseCodesService, tiersService)); fastify.register(objStorageController(paymentService), { prefix: '/object-storage' }); - fastify.register(businessController(paymentService, usersService, config), { prefix: '/business' }); + fastify.register(businessController(paymentService, usersService, tiersService, config), { prefix: '/business' }); fastify.register(productsController(productsService, cacheService, config), { prefix: '/products' }); fastify.register(checkoutController(usersService, paymentService), { prefix: '/checkout' }); fastify.register(customerController(usersService, paymentService, cacheService), { prefix: '/customer' }); diff --git a/src/controller/business.controller.ts b/src/controller/business.controller.ts index 859bd000..56e61fe1 100644 --- a/src/controller/business.controller.ts +++ b/src/controller/business.controller.ts @@ -12,8 +12,15 @@ import { assertUser } from '../utils/assertUser'; import fastifyJwt from '@fastify/jwt'; import fastifyLimit from '@fastify/rate-limit'; import Stripe from 'stripe'; +import { TiersService } from '../services/tiers.service'; +import { Service } from '../core/users/Tier'; -export default function (paymentService: PaymentService, usersService: UsersService, config: AppConfig) { +export default function ( + paymentService: PaymentService, + usersService: UsersService, + tiersService: TiersService, + config: AppConfig, +) { return async function (fastify: FastifyInstance) { fastify.register(fastifyJwt, { secret: config.JWT_SECRET }); fastify.register(fastifyLimit, { @@ -92,7 +99,16 @@ export default function (paymentService: PaymentService, usersService: UsersServ }, }); - await usersService.updateWorkspaceStorage(user.uuid, Number(maxSpaceBytes), workspaceUpdatedSeats); + const price = updatedSub.items.data[0]?.price; + const productId = typeof price?.product === 'string' ? price.product : price?.product.id; + const tier = await tiersService.getTierProductsByProductsId(productId as string, 'subscription'); + + await usersService.updateWorkspace({ + ownerId: user.uuid, + tierId: tier.featuresPerService[Service.Drive].foreignTierId, + maxSpaceBytes: Number(maxSpaceBytes), + seats: workspaceUpdatedSeats, + }); return res.status(200).send(updatedSub); } catch (err) { diff --git a/src/services/tiers.service.ts b/src/services/tiers.service.ts index e7bbce8d..afe5fb42 100644 --- a/src/services/tiers.service.ts +++ b/src/services/tiers.service.ts @@ -248,9 +248,15 @@ export class TiersService { const maxSpaceBytes = features.workspaces.maxSpaceBytesPerSeat; const address = customer.address?.line1 ?? undefined; const phoneNumber = customer.phone ?? undefined; + const driveTierId = tier.featuresPerService[Service.Drive].foreignTierId; try { - await this.usersService.updateWorkspaceStorage(userWithEmail.uuid, Number(maxSpaceBytes), subscriptionSeats); + await this.usersService.updateWorkspace({ + ownerId: userWithEmail.uuid, + maxSpaceBytes: Number(maxSpaceBytes), + seats: subscriptionSeats, + tierId: driveTierId, + }); log.info(`[DRIVE/WORKSPACES]: The workspace for user ${userWithEmail.uuid} has been updated`); } catch (err) { if (isAxiosError(err) && err.response?.status === 404) { @@ -260,6 +266,7 @@ export class TiersService { await this.usersService.initializeWorkspace(userWithEmail.uuid, { newStorageBytes: Number(maxSpaceBytes), seats: subscriptionSeats, + tierId: driveTierId, address, phoneNumber, }); diff --git a/src/services/users.service.ts b/src/services/users.service.ts index b52e35fd..674beae1 100644 --- a/src/services/users.service.ts +++ b/src/services/users.service.ts @@ -168,7 +168,7 @@ export class UsersService { async initializeWorkspace( ownerId: string, - payload: { newStorageBytes: number; seats: number; address?: string; phoneNumber?: string }, + payload: { newStorageBytes: number; seats: number; tierId: string; address?: string; phoneNumber?: string }, ): Promise { const jwt = signToken('5m', this.config.DRIVE_NEW_GATEWAY_SECRET); const params: AxiosRequestConfig = { @@ -182,6 +182,7 @@ export class UsersService { `${this.config.DRIVE_NEW_GATEWAY_URL}/gateway/workspaces`, { ownerId, + tierId: payload.tierId, maxSpaceBytes: payload.newStorageBytes * payload.seats, address: payload.address, numberOfSeats: payload.seats, @@ -216,7 +217,17 @@ export class UsersService { ); } - async updateWorkspaceStorage(ownerId: string, maxSpaceBytes: number, seats: number): Promise { + async updateWorkspace({ + ownerId, + tierId, + maxSpaceBytes, + seats, + }: { + ownerId: string; + tierId: string; + maxSpaceBytes: number; + seats: number; + }): Promise { const jwt = signToken('5m', this.config.DRIVE_NEW_GATEWAY_SECRET); const requestConfig: AxiosRequestConfig = { headers: { @@ -225,12 +236,13 @@ export class UsersService { }, }; - return this.axios.put( - `${this.config.DRIVE_NEW_GATEWAY_URL}/gateway/workspaces/storage`, + return this.axios.patch( + `${this.config.DRIVE_NEW_GATEWAY_URL}/gateway/workspaces`, { ownerId, maxSpaceBytes: maxSpaceBytes * seats, numberOfSeats: seats, + tierId, }, requestConfig, ); diff --git a/tests/src/services/tiers.service.test.ts b/tests/src/services/tiers.service.test.ts index d5f1a262..84e79f07 100644 --- a/tests/src/services/tiers.service.test.ts +++ b/tests/src/services/tiers.service.test.ts @@ -483,7 +483,7 @@ describe('TiersService tests', () => { jest.spyOn(tiersRepository, 'findByProductId').mockImplementation(() => Promise.resolve(tier)); const updateWorkspaceStorage = jest - .spyOn(usersService, 'updateWorkspaceStorage') + .spyOn(usersService, 'updateWorkspace') .mockImplementation(() => Promise.resolve()); await expect( @@ -504,7 +504,7 @@ describe('TiersService tests', () => { jest.spyOn(tiersRepository, 'findByProductId').mockImplementation(() => Promise.resolve(tier)); const updateWorkspaceStorage = jest - .spyOn(usersService, 'updateWorkspaceStorage') + .spyOn(usersService, 'updateWorkspace') .mockImplementation(() => Promise.resolve()); await tiersService.applyTier( @@ -515,11 +515,12 @@ describe('TiersService tests', () => { logger, ); - expect(updateWorkspaceStorage).toHaveBeenCalledWith( - userWithEmail.uuid, - tier.featuresPerService[Service.Drive].workspaces.maxSpaceBytesPerSeat, - mockedInvoiceLineItem.quantity, - ); + expect(updateWorkspaceStorage).toHaveBeenCalledWith({ + ownerId: userWithEmail.uuid, + maxSpaceBytes: tier.featuresPerService[Service.Drive].workspaces.maxSpaceBytesPerSeat, + seats: mockedInvoiceLineItem.quantity, + tierId: tier.featuresPerService[Service.Drive].foreignTierId, + }); }); it('When workspaces is enabled and the workspace do not exist, then it is initialized', async () => { @@ -538,7 +539,7 @@ describe('TiersService tests', () => { toJSON: () => ({}), } as AxiosError; - jest.spyOn(usersService, 'updateWorkspaceStorage').mockImplementation(() => Promise.reject(axiosError404)); + jest.spyOn(usersService, 'updateWorkspace').mockImplementation(() => Promise.reject(axiosError404)); const initializeWorkspace = jest.spyOn(usersService, 'initializeWorkspace').mockResolvedValue(); @@ -549,6 +550,7 @@ describe('TiersService tests', () => { seats: amountOfSeats, address: mockedCustomer.address?.line1 ?? undefined, phoneNumber: mockedCustomer.phone ?? undefined, + tierId: tier.featuresPerService[Service.Drive].foreignTierId, }); }); @@ -564,7 +566,7 @@ describe('TiersService tests', () => { const unexpectedError = new Error('Unexpected error'); - jest.spyOn(usersService, 'updateWorkspaceStorage').mockRejectedValue(unexpectedError); + jest.spyOn(usersService, 'updateWorkspace').mockRejectedValue(unexpectedError); await expect( tiersService.applyDriveFeatures(userWithEmail, mockedCustomer, amountOfSeats, tier, logger), diff --git a/tests/src/services/users.service.test.ts b/tests/src/services/users.service.test.ts index 90f81c97..15ef25ee 100644 --- a/tests/src/services/users.service.test.ts +++ b/tests/src/services/users.service.test.ts @@ -5,8 +5,9 @@ import { ExtendedSubscription } from '../../../src/services/payment.service'; import { CouponNotBeingTrackedError, UserNotFoundError } from '../../../src/services/users.service'; import config from '../../../src/config'; import { FREE_PLAN_BYTES_SPACE } from '../../../src/constants'; -import { getActiveSubscriptions, getCoupon, getUser, newTier, voidPromise } from '../fixtures'; +import { getActiveSubscriptions, getCoupon, getCustomer, getUser, newTier, voidPromise } from '../fixtures'; import { createTestServices } from '../helpers/services-factory'; +import { Service } from '../../../src/core/users/Tier'; jest.mock('jsonwebtoken', () => ({ ...jest.requireActual('jsonwebtoken'), @@ -92,7 +93,7 @@ describe('UsersService tests', () => { }); }); - describe('Find customer by User UUId', () => { + describe('Find customer by User UUID', () => { it('When looking for a customer by UUID with the correct params, then the customer is found', async () => { const mockedUser = getUser(); (usersRepository.findUserByUuid as jest.Mock).mockResolvedValue(mockedUser); @@ -115,6 +116,74 @@ describe('UsersService tests', () => { }); }); + describe('Workspaces', () => { + test('When initializing the workspace, then the workspace is initialized using the correct params', async () => { + const userWithEmail = { ...getUser(), email: 'test@internxt.com' }; + const tier = newTier(); + const mockedCustomer = getCustomer(); + const amountOfSeats = 5; + + const axiosPostSpy = jest.spyOn(axios, 'post').mockResolvedValue({} as any); + + await usersService.initializeWorkspace(userWithEmail.uuid, { + newStorageBytes: tier.featuresPerService[Service.Drive].workspaces.maxSpaceBytesPerSeat, + seats: amountOfSeats, + address: mockedCustomer.address?.line1 ?? undefined, + phoneNumber: mockedCustomer.phone ?? undefined, + tierId: tier.featuresPerService[Service.Drive].foreignTierId, + }); + + expect(axiosPostSpy).toHaveBeenCalledWith( + `${process.env.DRIVE_NEW_GATEWAY_URL}/gateway/workspaces`, + { + ownerId: userWithEmail.uuid, + maxSpaceBytes: tier.featuresPerService[Service.Drive].workspaces.maxSpaceBytesPerSeat * amountOfSeats, + numberOfSeats: amountOfSeats, + address: mockedCustomer.address?.line1 ?? undefined, + phoneNumber: mockedCustomer.phone ?? undefined, + tierId: tier.featuresPerService[Service.Drive].foreignTierId, + }, + { + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer undefined', + }, + }, + ); + }); + + test('When updating the workspace, then the workspace is updated using the correct params', async () => { + const userWithEmail = { ...getUser(), email: 'test@internxt.com' }; + const tier = newTier(); + const amountOfSeats = 5; + + const axiosPostSpy = jest.spyOn(axios, 'patch').mockResolvedValue({} as any); + + await usersService.updateWorkspace({ + ownerId: userWithEmail.uuid, + maxSpaceBytes: tier.featuresPerService[Service.Drive].workspaces.maxSpaceBytesPerSeat, + seats: amountOfSeats, + tierId: tier.featuresPerService[Service.Drive].foreignTierId, + }); + + expect(axiosPostSpy).toHaveBeenCalledWith( + `${process.env.DRIVE_NEW_GATEWAY_URL}/gateway/workspaces`, + { + ownerId: userWithEmail.uuid, + maxSpaceBytes: tier.featuresPerService[Service.Drive].workspaces.maxSpaceBytesPerSeat * amountOfSeats, + numberOfSeats: amountOfSeats, + tierId: tier.featuresPerService[Service.Drive].foreignTierId, + }, + { + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer undefined', + }, + }, + ); + }); + }); + describe('Cancel user subscription', () => { describe('Cancel the user Individual subscription', () => { it('When the customer wants to cancel the individual subscription, then the Stripe plan is cancelled and the storage is restored', async () => { From c8a6e66232bc62de80e8033d2210e7cfa4897413 Mon Sep 17 00:00:00 2001 From: Xavier Abad <77491413+xabg2@users.noreply.github.com> Date: Mon, 6 Oct 2025 20:07:52 +0200 Subject: [PATCH 04/13] feat: handle business user-tier relationship --- src/cli/sync-users-with-drive-tiers.ts | 86 ++++++++++++++++++++++++-- src/services/users.service.ts | 11 +++- 2 files changed, 90 insertions(+), 7 deletions(-) diff --git a/src/cli/sync-users-with-drive-tiers.ts b/src/cli/sync-users-with-drive-tiers.ts index 19546dba..7bc84109 100644 --- a/src/cli/sync-users-with-drive-tiers.ts +++ b/src/cli/sync-users-with-drive-tiers.ts @@ -1,24 +1,102 @@ import axios from 'axios'; import { MongoClient } from 'mongodb'; +import Stripe from 'stripe'; import envVariablesConfig from '../config'; +import { LicenseCodesService } from '../services/licenseCodes.service'; +import { PaymentService } from '../services/payment.service'; +import { UsersService } from '../services/users.service'; +import { UsersRepository } from '../core/users/UsersRepository'; +import { MongoDBUsersRepository } from '../core/users/MongoDBUsersRepository'; +import { LicenseCodesRepository } from '../core/users/LicenseCodeRepository'; +import { MongoDBLicenseCodesRepository } from '../core/users/MongoDBLicenseCodesRepository'; import { StorageService } from '../services/storage.service'; +import { + DisplayBillingRepository, + MongoDBDisplayBillingRepository, +} from '../core/users/MongoDBDisplayBillingRepository'; +import { CouponsRepository } from '../core/coupons/CouponsRepository'; +import { UsersCouponsRepository } from '../core/coupons/UsersCouponsRepository'; +import { MongoDBCouponsRepository } from '../core/coupons/MongoDBCouponsRepository'; +import { MongoDBUsersCouponsRepository } from '../core/coupons/MongoDBUsersCouponsRepository'; +import { ProductsRepository } from '../core/users/ProductsRepository'; +import { MongoDBProductsRepository } from '../core/users/MongoDBProductsRepository'; +import { Bit2MeService } from '../services/bit2me.service'; +import { TiersService } from '../services/tiers.service'; +import { MongoDBTiersRepository, TiersRepository } from '../core/users/MongoDBTiersRepository'; import { MongoDBUsersTiersRepository, UsersTiersRepository } from '../core/users/MongoDBUsersTiersRepository'; const [, , subType] = process.argv; const isBusiness = subType?.toLowerCase() === 'business'; +async function updateBusinessUsers(usersTiersRepository: UsersTiersRepository, usersService: UsersService) { + const userIdsAndForeignTierId = await usersTiersRepository.getUserTierMappings(true); + for (const { userUuid, foreignTierId } of userIdsAndForeignTierId) { + await usersService.updateWorkspace({ + ownerId: userUuid, + tierId: foreignTierId, + }); + } +} + +async function updatePersonalUsers(usersTiersRepository: UsersTiersRepository, storageService: StorageService) { + const userIdsAndForeignTierId = await usersTiersRepository.getUserTierMappings(false); + for (const { userUuid, foreignTierId } of userIdsAndForeignTierId) { + await storageService.updateUserStorageAndTier(userUuid, undefined, foreignTierId); + } +} + async function main() { const mongoClient = await new MongoClient(envVariablesConfig.MONGO_URI).connect(); try { + const stripe = new Stripe(envVariablesConfig.STRIPE_SECRET_KEY, { apiVersion: '2025-02-24.acacia' }); + const usersRepository: UsersRepository = new MongoDBUsersRepository(mongoClient); const storageService = new StorageService(envVariablesConfig, axios); + const licenseCodesRepository: LicenseCodesRepository = new MongoDBLicenseCodesRepository(mongoClient); + const displayBillingRepository: DisplayBillingRepository = new MongoDBDisplayBillingRepository(mongoClient); + const couponsRepository: CouponsRepository = new MongoDBCouponsRepository(mongoClient); + const usersCouponsRepository: UsersCouponsRepository = new MongoDBUsersCouponsRepository(mongoClient); + const productsRepository: ProductsRepository = new MongoDBProductsRepository(mongoClient); + const tiersRepository: TiersRepository = new MongoDBTiersRepository(mongoClient); const usersTiersRepository: UsersTiersRepository = new MongoDBUsersTiersRepository(mongoClient); + const bit2MeService = new Bit2MeService( + envVariablesConfig, + axios, + envVariablesConfig.CRYPTO_PAYMENTS_PROCESSOR_SECRET_KEY, + envVariablesConfig.CRYPTO_PAYMENTS_PROCESSOR_API_KEY, + envVariablesConfig.CRYPTO_PAYMENTS_PROCESSOR_API_URL, + ); + const paymentService = new PaymentService(stripe, productsRepository, bit2MeService); + const usersService = new UsersService( + usersRepository, + paymentService, + displayBillingRepository, + couponsRepository, + usersCouponsRepository, + envVariablesConfig, + axios, + ); + const tiersService = new TiersService( + usersService, + paymentService, + tiersRepository, + usersTiersRepository, + storageService, + envVariablesConfig, + ); + const licenseCodesService = new LicenseCodesService({ + paymentService, + usersService, + storageService, + licenseCodesRepository, + tiersService, + }); - const userIdsAndForeignTierId = await usersTiersRepository.getUserTierMappings(isBusiness); - - for (const { userUuid, foreignTierId } of userIdsAndForeignTierId) { - await storageService.updateUserStorageAndTier(userUuid, undefined, foreignTierId); + if (isBusiness) { + await updateBusinessUsers(usersTiersRepository, usersService); + } else { + await updatePersonalUsers(usersTiersRepository, storageService); } } finally { await mongoClient.close(); diff --git a/src/services/users.service.ts b/src/services/users.service.ts index 674beae1..11cbf3c4 100644 --- a/src/services/users.service.ts +++ b/src/services/users.service.ts @@ -225,10 +225,15 @@ export class UsersService { }: { ownerId: string; tierId: string; - maxSpaceBytes: number; - seats: number; + maxSpaceBytes?: number; + seats?: number; }): Promise { const jwt = signToken('5m', this.config.DRIVE_NEW_GATEWAY_SECRET); + let totalMaxSpaceBytes: number | undefined = undefined; + + if (maxSpaceBytes && seats) { + totalMaxSpaceBytes = maxSpaceBytes * seats; + } const requestConfig: AxiosRequestConfig = { headers: { 'Content-Type': 'application/json', @@ -240,7 +245,7 @@ export class UsersService { `${this.config.DRIVE_NEW_GATEWAY_URL}/gateway/workspaces`, { ownerId, - maxSpaceBytes: maxSpaceBytes * seats, + maxSpaceBytes: totalMaxSpaceBytes, numberOfSeats: seats, tierId, }, From 697e47c66604ad612f10ef6c55e80b04a9232aaa Mon Sep 17 00:00:00 2001 From: Xavier Abad <77491413+xabg2@users.noreply.github.com> Date: Tue, 7 Oct 2025 13:13:50 +0200 Subject: [PATCH 05/13] fix: use the correct foreignTierId from Drive --- src/core/users/MongoDBUsersTiersRepository.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/users/MongoDBUsersTiersRepository.ts b/src/core/users/MongoDBUsersTiersRepository.ts index 8e6ae2b5..6d591655 100644 --- a/src/core/users/MongoDBUsersTiersRepository.ts +++ b/src/core/users/MongoDBUsersTiersRepository.ts @@ -71,7 +71,7 @@ export class MongoDBUsersTiersRepository implements UsersTiersRepository { $project: { _id: 0, userUuid: '$user.uuid', - foreignTierId: '$tier.foreignTierId', + foreignTierId: '$tier.featuresPerService.drive.foreignTierId', }, }, ]) From e5c689ffc9a318c6409f1fd513283dd77029a788 Mon Sep 17 00:00:00 2001 From: Xavier Abad <77491413+xabg2@users.noreply.github.com> Date: Tue, 7 Oct 2025 16:24:12 +0200 Subject: [PATCH 06/13] feat: add payments header --- src/cli/sync-users-with-drive-tiers.ts | 86 ++++++++++++------- src/core/users/MongoDBUsersTiersRepository.ts | 37 ++++++-- src/services/storage.service.ts | 1 + src/services/users.service.ts | 1 + 4 files changed, 88 insertions(+), 37 deletions(-) diff --git a/src/cli/sync-users-with-drive-tiers.ts b/src/cli/sync-users-with-drive-tiers.ts index 7bc84109..09c89841 100644 --- a/src/cli/sync-users-with-drive-tiers.ts +++ b/src/cli/sync-users-with-drive-tiers.ts @@ -3,13 +3,10 @@ import { MongoClient } from 'mongodb'; import Stripe from 'stripe'; import envVariablesConfig from '../config'; -import { LicenseCodesService } from '../services/licenseCodes.service'; import { PaymentService } from '../services/payment.service'; import { UsersService } from '../services/users.service'; import { UsersRepository } from '../core/users/UsersRepository'; import { MongoDBUsersRepository } from '../core/users/MongoDBUsersRepository'; -import { LicenseCodesRepository } from '../core/users/LicenseCodeRepository'; -import { MongoDBLicenseCodesRepository } from '../core/users/MongoDBLicenseCodesRepository'; import { StorageService } from '../services/storage.service'; import { DisplayBillingRepository, @@ -22,29 +19,65 @@ import { MongoDBUsersCouponsRepository } from '../core/coupons/MongoDBUsersCoupo import { ProductsRepository } from '../core/users/ProductsRepository'; import { MongoDBProductsRepository } from '../core/users/MongoDBProductsRepository'; import { Bit2MeService } from '../services/bit2me.service'; -import { TiersService } from '../services/tiers.service'; -import { MongoDBTiersRepository, TiersRepository } from '../core/users/MongoDBTiersRepository'; import { MongoDBUsersTiersRepository, UsersTiersRepository } from '../core/users/MongoDBUsersTiersRepository'; const [, , subType] = process.argv; const isBusiness = subType?.toLowerCase() === 'business'; -async function updateBusinessUsers(usersTiersRepository: UsersTiersRepository, usersService: UsersService) { +export async function updateBusinessUsers(usersTiersRepository: UsersTiersRepository, usersService: UsersService) { const userIdsAndForeignTierId = await usersTiersRepository.getUserTierMappings(true); + const errors: Array<{ userUuid: string; error: string }> = []; + for (const { userUuid, foreignTierId } of userIdsAndForeignTierId) { - await usersService.updateWorkspace({ - ownerId: userUuid, - tierId: foreignTierId, + try { + console.log(`Processing user: ${userUuid} with business foreign tier id: ${foreignTierId}`); + await usersService.updateWorkspace({ + ownerId: userUuid, + tierId: foreignTierId, + }); + console.log(`Successfully updated user: ${userUuid}`); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(`āœ— Failed to update user ${userUuid}: ${errorMessage}`); + errors.push({ userUuid, error: errorMessage }); + } + } + + if (errors.length > 0) { + console.error(`\n=== SUMMARY: ${errors.length} user(s) failed ===`); + errors.forEach(({ userUuid, error }) => { + console.error(` - ${userUuid}: ${error}`); }); } + + return { total: userIdsAndForeignTierId.length, failed: errors.length, errors }; } -async function updatePersonalUsers(usersTiersRepository: UsersTiersRepository, storageService: StorageService) { +export async function updatePersonalUsers(usersTiersRepository: UsersTiersRepository, storageService: StorageService) { const userIdsAndForeignTierId = await usersTiersRepository.getUserTierMappings(false); + const errors: Array<{ userUuid: string; error: string }> = []; + for (const { userUuid, foreignTierId } of userIdsAndForeignTierId) { - await storageService.updateUserStorageAndTier(userUuid, undefined, foreignTierId); + try { + console.log(`Processing user: ${userUuid} with individual foreign tier id: ${foreignTierId}`); + await storageService.updateUserStorageAndTier(userUuid, undefined, foreignTierId); + console.log(`Successfully updated user: ${userUuid}`); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(`āœ— Failed to update user ${userUuid}: ${errorMessage}`); + errors.push({ userUuid, error: errorMessage }); + } + } + + if (errors.length > 0) { + console.error(`\n=== SUMMARY: ${errors.length} user(s) failed ===`); + errors.forEach(({ userUuid, error }) => { + console.error(` - ${userUuid}: ${error}`); + }); } + + return { total: userIdsAndForeignTierId.length, failed: errors.length, errors }; } async function main() { @@ -53,12 +86,10 @@ async function main() { const stripe = new Stripe(envVariablesConfig.STRIPE_SECRET_KEY, { apiVersion: '2025-02-24.acacia' }); const usersRepository: UsersRepository = new MongoDBUsersRepository(mongoClient); const storageService = new StorageService(envVariablesConfig, axios); - const licenseCodesRepository: LicenseCodesRepository = new MongoDBLicenseCodesRepository(mongoClient); const displayBillingRepository: DisplayBillingRepository = new MongoDBDisplayBillingRepository(mongoClient); const couponsRepository: CouponsRepository = new MongoDBCouponsRepository(mongoClient); const usersCouponsRepository: UsersCouponsRepository = new MongoDBUsersCouponsRepository(mongoClient); const productsRepository: ProductsRepository = new MongoDBProductsRepository(mongoClient); - const tiersRepository: TiersRepository = new MongoDBTiersRepository(mongoClient); const usersTiersRepository: UsersTiersRepository = new MongoDBUsersTiersRepository(mongoClient); const bit2MeService = new Bit2MeService( envVariablesConfig, @@ -77,27 +108,21 @@ async function main() { envVariablesConfig, axios, ); - const tiersService = new TiersService( - usersService, - paymentService, - tiersRepository, - usersTiersRepository, - storageService, - envVariablesConfig, - ); - const licenseCodesService = new LicenseCodesService({ - paymentService, - usersService, - storageService, - licenseCodesRepository, - tiersService, - }); + let result; if (isBusiness) { - await updateBusinessUsers(usersTiersRepository, usersService); + result = await updateBusinessUsers(usersTiersRepository, usersService); } else { - await updatePersonalUsers(usersTiersRepository, storageService); + result = await updatePersonalUsers(usersTiersRepository, storageService); + } + + console.log(`\nāœ“ Sync completed: ${result.total - result.failed}/${result.total} users updated successfully`); + + if (result.failed > 0) { + process.exit(1); } + } catch (error) { + throw error; } finally { await mongoClient.close(); } @@ -109,4 +134,5 @@ main() }) .catch((err) => { console.error('Error while syncing users: ', err.message); + process.exit(1); }); diff --git a/src/core/users/MongoDBUsersTiersRepository.ts b/src/core/users/MongoDBUsersTiersRepository.ts index 6d591655..19450b46 100644 --- a/src/core/users/MongoDBUsersTiersRepository.ts +++ b/src/core/users/MongoDBUsersTiersRepository.ts @@ -9,7 +9,10 @@ export interface UserTier { } export interface UsersTiersRepository { - getUserTierMappings(isBusiness?: boolean): Promise>; + getUserTierMappings( + isBusiness?: boolean, + userId?: string, + ): Promise>; insertTierToUser(userId: User['id'], tierId: Tier['id']): Promise; updateUserTier(userId: User['id'], oldTierId: Tier['id'], newTierId: Tier['id']): Promise; deleteTierFromUser(userId: User['id'], tierId: Tier['id']): Promise; @@ -31,20 +34,35 @@ export class MongoDBUsersTiersRepository implements UsersTiersRepository { this.collection = mongo.db('payments').collection>('users_tiers'); } - async getUserTierMappings(isBusiness = false): Promise> { + async getUserTierMappings( + isBusiness = false, + userId?: string, + ): Promise> { + const matchStage: any = { + 'featuresPerService.drive.workspaces.enabled': isBusiness, + }; + + if (userId) { + matchStage.uuid = userId; + } + const results = await this.collection .aggregate([ - { - $match: { - 'featuresPerService.drive.workspaces.enabled': isBusiness, - }, - }, { $addFields: { userIdObj: { $toObjectId: '$userId' }, tierIdObj: { $toObjectId: '$tierId' }, }, }, + ...(userId + ? [ + { + $match: { + userId: userId, + }, + }, + ] + : []), { $lookup: { from: 'users', @@ -67,6 +85,11 @@ export class MongoDBUsersTiersRepository implements UsersTiersRepository { { $unwind: '$tier', }, + { + $match: { + 'tier.featuresPerService.drive.workspaces.enabled': isBusiness, + }, + }, { $project: { _id: 0, diff --git a/src/services/storage.service.ts b/src/services/storage.service.ts index 9868cc46..374eb471 100644 --- a/src/services/storage.service.ts +++ b/src/services/storage.service.ts @@ -24,6 +24,7 @@ export class StorageService { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${jwt}`, + 'x-internxt-payments-header': process.env.X_INTERNXT_PAYMENTS_HEADER, }, }; diff --git a/src/services/users.service.ts b/src/services/users.service.ts index 11cbf3c4..7218c939 100644 --- a/src/services/users.service.ts +++ b/src/services/users.service.ts @@ -238,6 +238,7 @@ export class UsersService { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${jwt}`, + 'x-internxt-payments-header': process.env.X_INTERNXT_PAYMENTS_HEADER, }, }; From 4329782bddfde9197e716e3775b1ba3f9f001cad Mon Sep 17 00:00:00 2001 From: Xavier Abad <77491413+xabg2@users.noreply.github.com> Date: Tue, 7 Oct 2025 17:42:00 +0200 Subject: [PATCH 07/13] fix: improve the function name to use a friendly name --- src/cli/sync-users-with-drive-tiers.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/cli/sync-users-with-drive-tiers.ts b/src/cli/sync-users-with-drive-tiers.ts index 09c89841..8d105741 100644 --- a/src/cli/sync-users-with-drive-tiers.ts +++ b/src/cli/sync-users-with-drive-tiers.ts @@ -54,7 +54,10 @@ export async function updateBusinessUsers(usersTiersRepository: UsersTiersReposi return { total: userIdsAndForeignTierId.length, failed: errors.length, errors }; } -export async function updatePersonalUsers(usersTiersRepository: UsersTiersRepository, storageService: StorageService) { +export async function updateIndividualUsers( + usersTiersRepository: UsersTiersRepository, + storageService: StorageService, +) { const userIdsAndForeignTierId = await usersTiersRepository.getUserTierMappings(false); const errors: Array<{ userUuid: string; error: string }> = []; @@ -113,7 +116,7 @@ async function main() { if (isBusiness) { result = await updateBusinessUsers(usersTiersRepository, usersService); } else { - result = await updatePersonalUsers(usersTiersRepository, storageService); + result = await updateIndividualUsers(usersTiersRepository, storageService); } console.log(`\nāœ“ Sync completed: ${result.total - result.failed}/${result.total} users updated successfully`); From 5a8b14979008f960a9199433ee63f4c5c961b9fc Mon Sep 17 00:00:00 2001 From: Xavier Abad <77491413+xabg2@users.noreply.github.com> Date: Wed, 8 Oct 2025 10:29:12 +0200 Subject: [PATCH 08/13] feat: fetch the tier for an specific user --- src/cli/sync-users-with-drive-tiers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/sync-users-with-drive-tiers.ts b/src/cli/sync-users-with-drive-tiers.ts index 8d105741..f4f0be38 100644 --- a/src/cli/sync-users-with-drive-tiers.ts +++ b/src/cli/sync-users-with-drive-tiers.ts @@ -58,7 +58,7 @@ export async function updateIndividualUsers( usersTiersRepository: UsersTiersRepository, storageService: StorageService, ) { - const userIdsAndForeignTierId = await usersTiersRepository.getUserTierMappings(false); + const userIdsAndForeignTierId = await usersTiersRepository.getUserTierMappings(false, '6842cc7e370958d12dcb2246'); const errors: Array<{ userUuid: string; error: string }> = []; for (const { userUuid, foreignTierId } of userIdsAndForeignTierId) { From f24a50ab973b325c922c45e80499c791436b93e5 Mon Sep 17 00:00:00 2001 From: Xavier Abad <77491413+xabg2@users.noreply.github.com> Date: Wed, 8 Oct 2025 10:35:15 +0200 Subject: [PATCH 09/13] fix: remove hardcoded id --- src/cli/sync-users-with-drive-tiers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/sync-users-with-drive-tiers.ts b/src/cli/sync-users-with-drive-tiers.ts index f4f0be38..8d105741 100644 --- a/src/cli/sync-users-with-drive-tiers.ts +++ b/src/cli/sync-users-with-drive-tiers.ts @@ -58,7 +58,7 @@ export async function updateIndividualUsers( usersTiersRepository: UsersTiersRepository, storageService: StorageService, ) { - const userIdsAndForeignTierId = await usersTiersRepository.getUserTierMappings(false, '6842cc7e370958d12dcb2246'); + const userIdsAndForeignTierId = await usersTiersRepository.getUserTierMappings(false); const errors: Array<{ userUuid: string; error: string }> = []; for (const { userUuid, foreignTierId } of userIdsAndForeignTierId) { From 0b21fd1a0b4f02fdfb57121884d304cb90815e91 Mon Sep 17 00:00:00 2001 From: Xavier Abad <77491413+xabg2@users.noreply.github.com> Date: Wed, 8 Oct 2025 13:27:24 +0200 Subject: [PATCH 10/13] fix: pass the userID as a param --- src/cli/sync-users-with-drive-tiers.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cli/sync-users-with-drive-tiers.ts b/src/cli/sync-users-with-drive-tiers.ts index 8d105741..c1a5969e 100644 --- a/src/cli/sync-users-with-drive-tiers.ts +++ b/src/cli/sync-users-with-drive-tiers.ts @@ -21,12 +21,12 @@ import { MongoDBProductsRepository } from '../core/users/MongoDBProductsReposito import { Bit2MeService } from '../services/bit2me.service'; import { MongoDBUsersTiersRepository, UsersTiersRepository } from '../core/users/MongoDBUsersTiersRepository'; -const [, , subType] = process.argv; +const [, , subType, userId] = process.argv; const isBusiness = subType?.toLowerCase() === 'business'; export async function updateBusinessUsers(usersTiersRepository: UsersTiersRepository, usersService: UsersService) { - const userIdsAndForeignTierId = await usersTiersRepository.getUserTierMappings(true); + const userIdsAndForeignTierId = await usersTiersRepository.getUserTierMappings(true, userId); const errors: Array<{ userUuid: string; error: string }> = []; for (const { userUuid, foreignTierId } of userIdsAndForeignTierId) { @@ -58,7 +58,7 @@ export async function updateIndividualUsers( usersTiersRepository: UsersTiersRepository, storageService: StorageService, ) { - const userIdsAndForeignTierId = await usersTiersRepository.getUserTierMappings(false); + const userIdsAndForeignTierId = await usersTiersRepository.getUserTierMappings(false, userId); const errors: Array<{ userUuid: string; error: string }> = []; for (const { userUuid, foreignTierId } of userIdsAndForeignTierId) { From 4033644e953afb35fcccaf7c2394baf97a511fe0 Mon Sep 17 00:00:00 2001 From: Xavier Abad <77491413+xabg2@users.noreply.github.com> Date: Thu, 22 Jan 2026 09:54:14 +0100 Subject: [PATCH 11/13] tests: get users tiers relationship --- src/core/users/MongoDBUsersTiersRepository.ts | 20 +----- .../users/MongoDBUsersTiersRepository.test.ts | 66 +++++++++++++++++++ 2 files changed, 68 insertions(+), 18 deletions(-) diff --git a/src/core/users/MongoDBUsersTiersRepository.ts b/src/core/users/MongoDBUsersTiersRepository.ts index 19450b46..0f214ecd 100644 --- a/src/core/users/MongoDBUsersTiersRepository.ts +++ b/src/core/users/MongoDBUsersTiersRepository.ts @@ -36,16 +36,8 @@ export class MongoDBUsersTiersRepository implements UsersTiersRepository { async getUserTierMappings( isBusiness = false, - userId?: string, + userUuid?: string, ): Promise> { - const matchStage: any = { - 'featuresPerService.drive.workspaces.enabled': isBusiness, - }; - - if (userId) { - matchStage.uuid = userId; - } - const results = await this.collection .aggregate([ { @@ -54,15 +46,6 @@ export class MongoDBUsersTiersRepository implements UsersTiersRepository { tierIdObj: { $toObjectId: '$tierId' }, }, }, - ...(userId - ? [ - { - $match: { - userId: userId, - }, - }, - ] - : []), { $lookup: { from: 'users', @@ -88,6 +71,7 @@ export class MongoDBUsersTiersRepository implements UsersTiersRepository { { $match: { 'tier.featuresPerService.drive.workspaces.enabled': isBusiness, + ...(userUuid ? { 'user.uuid': userUuid } : {}), }, }, { diff --git a/tests/src/core/users/MongoDBUsersTiersRepository.test.ts b/tests/src/core/users/MongoDBUsersTiersRepository.test.ts index 3b389e5e..e7c0e6fa 100644 --- a/tests/src/core/users/MongoDBUsersTiersRepository.test.ts +++ b/tests/src/core/users/MongoDBUsersTiersRepository.test.ts @@ -2,12 +2,32 @@ import { MongoMemoryServer } from 'mongodb-memory-server'; import { MongoClient } from 'mongodb'; import { MongoDBUsersTiersRepository } from '../../../../src/core/users/MongoDBUsersTiersRepository'; import { getUser, newTier } from '../../fixtures'; +import { Service } from '../../../../src/core/users/Tier'; describe('Testing the users and tiers collection', () => { let mongoServer: MongoMemoryServer; let client: MongoClient; let repository: MongoDBUsersTiersRepository; + async function createUserWithTier(userUuid: string, foreignTierId: string) { + const usersCollection = client.db('payments').collection('users'); + const tiersCollection = client.db('payments').collection('tiers'); + + const insertedUser = await usersCollection.insertOne({ uuid: userUuid }); + const insertedTier = await tiersCollection.insertOne({ + featuresPerService: { + drive: { + workspaces: { enabled: false }, + foreignTierId, + }, + }, + }); + + await repository.insertTierToUser(insertedUser.insertedId.toString(), insertedTier.insertedId.toString()); + + return insertedUser.insertedId.toString(); + } + beforeAll(async () => { mongoServer = await MongoMemoryServer.create(); const uri = mongoServer.getUri(); @@ -24,6 +44,10 @@ describe('Testing the users and tiers collection', () => { beforeEach(async () => { const collection = (repository as any).collection; await collection.deleteMany({}); + const usersCollection = client.db('payments').collection('users'); + await usersCollection.deleteMany({}); + const tiersCollection = client.db('payments').collection('tiers'); + await tiersCollection.deleteMany({}); }); it('when inserting a tier for a user, then it should be stored correctly', async () => { @@ -110,4 +134,46 @@ describe('Testing the users and tiers collection', () => { userTiers = await repository.findTierIdByUserId(userId); expect(userTiers).toHaveLength(0); }); + + describe('Get User Tier map', () => { + test('When getting user tier mappings without specific user id, then it should return all maps', async () => { + const user1 = getUser(); + const tier1 = newTier(); + const user2 = getUser(); + const tier2 = newTier(); + + await createUserWithTier(user1.uuid, tier1.featuresPerService[Service.Drive].foreignTierId); + await createUserWithTier(user2.uuid, tier2.featuresPerService[Service.Drive].foreignTierId); + + const userTiers = await repository.getUserTierMappings(false); + + expect(userTiers).toHaveLength(2); + expect(userTiers[0]).toStrictEqual({ + userUuid: user1.uuid, + foreignTierId: tier1.featuresPerService[Service.Drive].foreignTierId, + }); + expect(userTiers[1]).toStrictEqual({ + userUuid: user2.uuid, + foreignTierId: tier2.featuresPerService[Service.Drive].foreignTierId, + }); + }); + + test('When getting user tier mappings with specific user id, then it should return only that user map', async () => { + const user1 = getUser(); + const tier1 = newTier(); + const user2 = getUser(); + const tier2 = newTier(); + + await createUserWithTier(user1.uuid, tier1.featuresPerService[Service.Drive].foreignTierId); + await createUserWithTier(user2.uuid, tier2.featuresPerService[Service.Drive].foreignTierId); + + const userTiers = await repository.getUserTierMappings(false, user1.uuid); + + expect(userTiers).toHaveLength(1); + expect(userTiers[0]).toStrictEqual({ + userUuid: user1.uuid, + foreignTierId: tier1.featuresPerService[Service.Drive].foreignTierId, + }); + }); + }); }); From 095bc461f5e8f67a4f78840bb2ba3d2ec3b5c683 Mon Sep 17 00:00:00 2001 From: Xavier Abad <77491413+xabg2@users.noreply.github.com> Date: Thu, 22 Jan 2026 10:06:46 +0100 Subject: [PATCH 12/13] fix: pass custom headers if needed for some functions --- src/cli/sync-users-with-drive-tiers.ts | 7 ++++++- src/services/storage.service.ts | 9 +++++++-- src/services/users.service.ts | 4 +++- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/cli/sync-users-with-drive-tiers.ts b/src/cli/sync-users-with-drive-tiers.ts index c1a5969e..439c18a3 100644 --- a/src/cli/sync-users-with-drive-tiers.ts +++ b/src/cli/sync-users-with-drive-tiers.ts @@ -35,6 +35,9 @@ export async function updateBusinessUsers(usersTiersRepository: UsersTiersReposi await usersService.updateWorkspace({ ownerId: userUuid, tierId: foreignTierId, + customHeaders: { + 'x-internxt-payments-header': process.env.X_INTERNXT_PAYMENTS_HEADER as string, + }, }); console.log(`Successfully updated user: ${userUuid}`); } catch (error) { @@ -64,7 +67,9 @@ export async function updateIndividualUsers( for (const { userUuid, foreignTierId } of userIdsAndForeignTierId) { try { console.log(`Processing user: ${userUuid} with individual foreign tier id: ${foreignTierId}`); - await storageService.updateUserStorageAndTier(userUuid, undefined, foreignTierId); + await storageService.updateUserStorageAndTier(userUuid, undefined, foreignTierId, { + 'x-internxt-payments-header': process.env.X_INTERNXT_PAYMENTS_HEADER as string, + }); console.log(`Successfully updated user: ${userUuid}`); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); diff --git a/src/services/storage.service.ts b/src/services/storage.service.ts index 9f3010c0..516cf4a6 100644 --- a/src/services/storage.service.ts +++ b/src/services/storage.service.ts @@ -18,13 +18,18 @@ export class StorageService { private readonly axios: Axios, ) {} - async updateUserStorageAndTier(uuid: string, newStorageBytes?: number, foreignTierId?: string): Promise { + async updateUserStorageAndTier( + uuid: string, + newStorageBytes?: number, + foreignTierId?: string, + customHeaders?: Record, + ): Promise { const jwt = signToken('5m', this.config.DRIVE_NEW_GATEWAY_SECRET); const params: AxiosRequestConfig = { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${jwt}`, - 'x-internxt-payments-header': process.env.X_INTERNXT_PAYMENTS_HEADER, + ...customHeaders, }, }; diff --git a/src/services/users.service.ts b/src/services/users.service.ts index 1384a2ac..bc42dbb5 100644 --- a/src/services/users.service.ts +++ b/src/services/users.service.ts @@ -217,11 +217,13 @@ export class UsersService { tierId, maxSpaceBytes, seats, + customHeaders, }: { ownerId: string; tierId: string; maxSpaceBytes?: number; seats?: number; + customHeaders?: Record; }): Promise { const jwt = signToken('5m', this.config.DRIVE_NEW_GATEWAY_SECRET); let totalMaxSpaceBytes: number | undefined = undefined; @@ -233,7 +235,7 @@ export class UsersService { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${jwt}`, - 'x-internxt-payments-header': process.env.X_INTERNXT_PAYMENTS_HEADER, + ...customHeaders, }, }; From 98648ebc8e9836cfbc1f78e919a87d5130855d33 Mon Sep 17 00:00:00 2001 From: Xavier Abad <77491413+xabg2@users.noreply.github.com> Date: Thu, 22 Jan 2026 10:20:25 +0100 Subject: [PATCH 13/13] tests: add test for b2b user tier mapper --- .../users/MongoDBUsersTiersRepository.test.ts | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/tests/src/core/users/MongoDBUsersTiersRepository.test.ts b/tests/src/core/users/MongoDBUsersTiersRepository.test.ts index e7c0e6fa..5c3f271f 100644 --- a/tests/src/core/users/MongoDBUsersTiersRepository.test.ts +++ b/tests/src/core/users/MongoDBUsersTiersRepository.test.ts @@ -9,7 +9,7 @@ describe('Testing the users and tiers collection', () => { let client: MongoClient; let repository: MongoDBUsersTiersRepository; - async function createUserWithTier(userUuid: string, foreignTierId: string) { + async function createUserWithTier(userUuid: string, foreignTierId: string, isBusiness = false) { const usersCollection = client.db('payments').collection('users'); const tiersCollection = client.db('payments').collection('tiers'); @@ -17,7 +17,7 @@ describe('Testing the users and tiers collection', () => { const insertedTier = await tiersCollection.insertOne({ featuresPerService: { drive: { - workspaces: { enabled: false }, + workspaces: { enabled: isBusiness }, foreignTierId, }, }, @@ -175,5 +175,23 @@ describe('Testing the users and tiers collection', () => { foreignTierId: tier1.featuresPerService[Service.Drive].foreignTierId, }); }); + + test('When getting user tier mappings for business users, then it should return only business tiers', async () => { + const personalUser = getUser(); + const personalTier = newTier(); + const businessUser = getUser(); + const businessTier = newTier(); + + await createUserWithTier(personalUser.uuid, personalTier.featuresPerService[Service.Drive].foreignTierId, false); + await createUserWithTier(businessUser.uuid, businessTier.featuresPerService[Service.Drive].foreignTierId, true); + + const userTiers = await repository.getUserTierMappings(true); + + expect(userTiers).toHaveLength(1); + expect(userTiers[0]).toStrictEqual({ + userUuid: businessUser.uuid, + foreignTierId: businessTier.featuresPerService[Service.Drive].foreignTierId, + }); + }); }); });