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..439c18a3 --- /dev/null +++ b/src/cli/sync-users-with-drive-tiers.ts @@ -0,0 +1,146 @@ +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 { MongoDBUsersTiersRepository, UsersTiersRepository } from '../core/users/MongoDBUsersTiersRepository'; + +const [, , subType, userId] = process.argv; + +const isBusiness = subType?.toLowerCase() === 'business'; + +export async function updateBusinessUsers(usersTiersRepository: UsersTiersRepository, usersService: UsersService) { + const userIdsAndForeignTierId = await usersTiersRepository.getUserTierMappings(true, userId); + const errors: Array<{ userUuid: string; error: string }> = []; + + for (const { userUuid, foreignTierId } of userIdsAndForeignTierId) { + try { + console.log(`Processing user: ${userUuid} with business foreign tier id: ${foreignTierId}`); + 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) { + 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 }; +} + +export async function updateIndividualUsers( + usersTiersRepository: UsersTiersRepository, + storageService: StorageService, +) { + const userIdsAndForeignTierId = await usersTiersRepository.getUserTierMappings(false, userId); + const errors: Array<{ userUuid: string; error: string }> = []; + + for (const { userUuid, foreignTierId } of userIdsAndForeignTierId) { + try { + console.log(`Processing user: ${userUuid} with individual foreign tier id: ${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); + 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() { + 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 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, + ); + + let result; + if (isBusiness) { + result = await updateBusinessUsers(usersTiersRepository, usersService); + } else { + result = await updateIndividualUsers(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(); + } +} + +main() + .then(() => { + console.log('Users and tiers synced'); + }) + .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 518cc456..0f214ecd 100644 --- a/src/core/users/MongoDBUsersTiersRepository.ts +++ b/src/core/users/MongoDBUsersTiersRepository.ts @@ -9,6 +9,10 @@ export interface UserTier { } export interface UsersTiersRepository { + 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; @@ -30,6 +34,59 @@ export class MongoDBUsersTiersRepository implements UsersTiersRepository { this.collection = mongo.db('payments').collection>('users_tiers'); } + async getUserTierMappings( + isBusiness = false, + userUuid?: string, + ): Promise> { + const results = await this.collection + .aggregate([ + { + $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', + }, + { + $match: { + 'tier.featuresPerService.drive.workspaces.enabled': isBusiness, + ...(userUuid ? { 'user.uuid': userUuid } : {}), + }, + }, + { + $project: { + _id: 0, + userUuid: '$user.uuid', + foreignTierId: '$tier.featuresPerService.drive.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 da0741bf..516cf4a6 100644 --- a/src/services/storage.service.ts +++ b/src/services/storage.service.ts @@ -18,12 +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}`, + ...customHeaders, }, }; diff --git a/src/services/users.service.ts b/src/services/users.service.ts index 0be08e3d..bc42dbb5 100644 --- a/src/services/users.service.ts +++ b/src/services/users.service.ts @@ -217,17 +217,25 @@ export class UsersService { tierId, maxSpaceBytes, seats, + customHeaders, }: { ownerId: string; tierId: string; - maxSpaceBytes: number; - seats: number; + maxSpaceBytes?: number; + seats?: number; + customHeaders?: Record; }): 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', Authorization: `Bearer ${jwt}`, + ...customHeaders, }, }; @@ -235,7 +243,7 @@ export class UsersService { `${this.config.DRIVE_NEW_GATEWAY_URL}/gateway/workspaces`, { ownerId, - maxSpaceBytes: maxSpaceBytes * seats, + maxSpaceBytes: totalMaxSpaceBytes, numberOfSeats: seats, tierId, }, diff --git a/tests/src/core/users/MongoDBUsersTiersRepository.test.ts b/tests/src/core/users/MongoDBUsersTiersRepository.test.ts index 3b389e5e..5c3f271f 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, isBusiness = false) { + 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: isBusiness }, + 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,64 @@ 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, + }); + }); + + 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, + }); + }); + }); }); diff --git a/tests/src/utils/factory.ts b/tests/src/utils/factory.ts index 8fe75a8b..0be320a4 100644 --- a/tests/src/utils/factory.ts +++ b/tests/src/utils/factory.ts @@ -25,6 +25,7 @@ const getTiersRepository = (): TiersRepository => { const getUsersTiersRepository = (): UsersTiersRepository => { return { + getUserTierMappings: jest.fn(), deleteAllUserTiers: jest.fn(), deleteTierFromUser: jest.fn(), findTierIdByUserId: jest.fn(),