Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 146 additions & 0 deletions src/cli/sync-users-with-drive-tiers.ts
Original file line number Diff line number Diff line change
@@ -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);
});
57 changes: 57 additions & 0 deletions src/core/users/MongoDBUsersTiersRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ export interface UserTier {
}

export interface UsersTiersRepository {
getUserTierMappings(
isBusiness?: boolean,
userId?: string,
): Promise<Array<{ userUuid: string; foreignTierId: string }>>;
insertTierToUser(userId: User['id'], tierId: Tier['id']): Promise<void>;
updateUserTier(userId: User['id'], oldTierId: Tier['id'], newTierId: Tier['id']): Promise<boolean>;
deleteTierFromUser(userId: User['id'], tierId: Tier['id']): Promise<boolean>;
Expand All @@ -30,6 +34,59 @@ export class MongoDBUsersTiersRepository implements UsersTiersRepository {
this.collection = mongo.db('payments').collection<Omit<UserTier, 'id'>>('users_tiers');
}

async getUserTierMappings(
isBusiness = false,
userUuid?: string,
): Promise<Array<{ userUuid: string; foreignTierId: string }>> {
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<void> {
await this.collection.insertOne({
userId,
Expand Down
8 changes: 7 additions & 1 deletion src/services/storage.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,18 @@ export class StorageService {
private readonly axios: Axios,
) {}

async updateUserStorageAndTier(uuid: string, newStorageBytes: number, foreignTierId: string): Promise<void> {
async updateUserStorageAndTier(
uuid: string,
newStorageBytes?: number,
foreignTierId?: string,
customHeaders?: Record<string, string>,
): Promise<void> {
const jwt = signToken('5m', this.config.DRIVE_NEW_GATEWAY_SECRET);
const params: AxiosRequestConfig = {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${jwt}`,
...customHeaders,
},
};

Expand Down
14 changes: 11 additions & 3 deletions src/services/users.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,25 +217,33 @@ export class UsersService {
tierId,
maxSpaceBytes,
seats,
customHeaders,
}: {
ownerId: string;
tierId: string;
maxSpaceBytes: number;
seats: number;
maxSpaceBytes?: number;
seats?: number;
customHeaders?: Record<string, string>;
}): Promise<void> {
const jwt = signToken('5m', this.config.DRIVE_NEW_GATEWAY_SECRET);
let totalMaxSpaceBytes: number | undefined = undefined;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be:

let totalMaxSpaceBytes = maxSpaceBytes;

?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The space can be undefined because we can update the customer only by using the tierId.


if (maxSpaceBytes && seats) {
totalMaxSpaceBytes = maxSpaceBytes * seats;
}
const requestConfig: AxiosRequestConfig = {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${jwt}`,
...customHeaders,
},
};

return this.axios.patch(
`${this.config.DRIVE_NEW_GATEWAY_URL}/gateway/workspaces`,
{
ownerId,
maxSpaceBytes: maxSpaceBytes * seats,
maxSpaceBytes: totalMaxSpaceBytes,
numberOfSeats: seats,
tierId,
},
Expand Down
84 changes: 84 additions & 0 deletions tests/src/core/users/MongoDBUsersTiersRepository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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,
});
});
});
});
1 change: 1 addition & 0 deletions tests/src/utils/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const getTiersRepository = (): TiersRepository => {

const getUsersTiersRepository = (): UsersTiersRepository => {
return {
getUserTierMappings: jest.fn(),
deleteAllUserTiers: jest.fn(),
deleteTierFromUser: jest.fn(),
findTierIdByUserId: jest.fn(),
Expand Down
Loading