Skip to content

Commit cc21635

Browse files
authored
Merge pull request #994 from internxt/feature/cello-referral
[PB-5982]: add referral integration for token generation and purchase tracking
2 parents ef7a914 + 36883c4 commit cc21635

8 files changed

Lines changed: 169 additions & 0 deletions

src/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { nanoid } from 'nanoid';
3131
import { getClientIdFromHeaders } from './common/decorators/client.decorator';
3232
import { AuthGuard } from './modules/auth/auth.guard';
3333
import { CacheManagerModule } from './modules/cache-manager/cache-manager.module';
34+
import { ReferralModule } from './modules/referral/referral.module';
3435

3536
@Module({
3637
imports: [
@@ -145,6 +146,7 @@ import { CacheManagerModule } from './modules/cache-manager/cache-manager.module
145146
WorkspacesModule,
146147
GatewayModule,
147148
CacheManagerModule,
149+
ReferralModule,
148150
],
149151
controllers: [],
150152
providers: [

src/config/configuration.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,5 +175,9 @@ export default () => ({
175175
endpointForSignedUrls: process.env.AVATAR_ENDPOINT_REWRITE_FOR_SIGNED_URLS,
176176
forcePathStyle: process.env.AVATAR_FORCE_PATH_STYLE || 'true',
177177
},
178+
cello: {
179+
productId: process.env.CELLO_PRODUCT_ID,
180+
productSecret: process.env.CELLO_PRODUCT_SECRET,
181+
},
178182
executeCronjobs: process.env.EXECUTE_JOBS === 'true',
179183
});
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { ConfigModule, ConfigService } from '@nestjs/config';
2+
import { Test, type TestingModule } from '@nestjs/testing';
3+
import { type Logger } from '@nestjs/common';
4+
import { createMock } from '@golevelup/ts-jest';
5+
import { verify } from 'jsonwebtoken';
6+
import { CelloReferralService } from './cello-referral.service';
7+
8+
describe('CelloReferralService', () => {
9+
let service: CelloReferralService;
10+
11+
const celloConfig = {
12+
'cello.productId': 'test-product-id',
13+
'cello.productSecret': 'test-product-secret',
14+
};
15+
16+
beforeEach(async () => {
17+
const module: TestingModule = await Test.createTestingModule({
18+
imports: [ConfigModule],
19+
providers: [CelloReferralService],
20+
})
21+
.setLogger(createMock<Logger>())
22+
.compile();
23+
24+
service = module.get<CelloReferralService>(CelloReferralService);
25+
26+
const configService = module.get<ConfigService>(ConfigService);
27+
jest
28+
.spyOn(configService, 'get')
29+
.mockImplementation((key: string) => celloConfig[key]);
30+
});
31+
32+
afterEach(() => {
33+
jest.clearAllMocks();
34+
jest.restoreAllMocks();
35+
});
36+
37+
describe('generateToken', () => {
38+
it('When called, then it returns a valid JWT signed with HS512', () => {
39+
const signupDate = new Date('2024-01-15T10:00:00.000Z');
40+
const token = service.generateToken('user-uuid', signupDate);
41+
42+
const decoded = verify(token, celloConfig['cello.productSecret'], {
43+
algorithms: ['HS512'],
44+
}) as Record<string, unknown>;
45+
46+
expect(decoded.productId).toBe(celloConfig['cello.productId']);
47+
expect(decoded.productUserId).toBe('user-uuid');
48+
expect(decoded.signupDate).toBe('2024-01-15T10:00:00.000Z');
49+
expect(decoded.iat).toBeDefined();
50+
expect(decoded.exp).toBe((decoded.iat as number) + 3600);
51+
});
52+
});
53+
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { ConfigService } from '@nestjs/config';
3+
import { sign } from 'jsonwebtoken';
4+
import { ReferralService } from './referral.service';
5+
6+
@Injectable()
7+
export class CelloReferralService extends ReferralService {
8+
constructor(private readonly configService: ConfigService) {
9+
super();
10+
}
11+
12+
generateToken(productUserId: string, signupDate: Date): string {
13+
const productId = this.configService.get<string>('cello.productId');
14+
const productSecret = this.configService.get<string>('cello.productSecret');
15+
const now = Math.floor(Date.now() / 1000);
16+
17+
return sign(
18+
{
19+
productId,
20+
productUserId,
21+
signupDate: signupDate.toISOString(),
22+
iat: now,
23+
exp: now + 3600,
24+
},
25+
productSecret,
26+
{ algorithm: 'HS512' },
27+
);
28+
}
29+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { type Logger } from '@nestjs/common';
2+
import { Test } from '@nestjs/testing';
3+
import { type DeepMocked, createMock } from '@golevelup/ts-jest';
4+
import { newUser } from '../../../test/fixtures';
5+
import { ReferralController } from './referral.controller';
6+
import { ReferralService } from './referral.service';
7+
8+
describe('ReferralController', () => {
9+
let controller: ReferralController;
10+
let referralService: DeepMocked<ReferralService>;
11+
12+
const user = newUser();
13+
14+
beforeEach(async () => {
15+
const moduleRef = await Test.createTestingModule({
16+
controllers: [ReferralController],
17+
})
18+
.setLogger(createMock<Logger>())
19+
.useMocker(() => createMock())
20+
.compile();
21+
22+
controller = moduleRef.get(ReferralController);
23+
referralService = moduleRef.get(ReferralService);
24+
});
25+
26+
afterEach(() => {
27+
jest.clearAllMocks();
28+
});
29+
30+
describe('POST /token', () => {
31+
it('When called, then it returns the generated token', async () => {
32+
referralService.generateToken.mockReturnValue('jwt-token');
33+
34+
const result = await controller.generateToken(user);
35+
36+
expect(result).toEqual({ token: 'jwt-token' });
37+
expect(referralService.generateToken).toHaveBeenCalledWith(
38+
user.uuid,
39+
user.createdAt,
40+
);
41+
});
42+
});
43+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
2+
import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
3+
import { User as UserDecorator } from '../auth/decorators/user.decorator';
4+
import { type User } from '../user/user.domain';
5+
import { ReferralService } from './referral.service';
6+
7+
@ApiTags('Referral')
8+
@Controller('referral')
9+
export class ReferralController {
10+
constructor(private readonly referralService: ReferralService) {}
11+
12+
@Post('/token')
13+
@HttpCode(HttpStatus.OK)
14+
@ApiOperation({ summary: 'Generate referral token' })
15+
@ApiOkResponse({ description: 'Referral token generated successfully' })
16+
async generateToken(@UserDecorator() user: User) {
17+
const token = this.referralService.generateToken(user.uuid, user.createdAt);
18+
return { token };
19+
}
20+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Module } from '@nestjs/common';
2+
import { ReferralController } from './referral.controller';
3+
import { ReferralService } from './referral.service';
4+
import { CelloReferralService } from './cello-referral.service';
5+
6+
@Module({
7+
controllers: [ReferralController],
8+
providers: [
9+
{
10+
provide: ReferralService,
11+
useClass: CelloReferralService,
12+
},
13+
],
14+
})
15+
export class ReferralModule {}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export abstract class ReferralService {
2+
abstract generateToken(productUserId: string, signupDate: Date): string;
3+
}

0 commit comments

Comments
 (0)