Skip to content

Commit b45de62

Browse files
committed
refactor: introduce payment limits boundary and cache stub
1 parent 8ae9098 commit b45de62

8 files changed

Lines changed: 81 additions & 21 deletions

src/limits/limits.module.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import { Module } from '@nestjs/common';
22
import { LimitsService } from './limits.service';
33
import { LimitsController } from './limits.controller';
44
import { PrismaModule } from '../prisma/prisma.module';
5+
import { CacheService } from '../common/cache/cache.service';
56

67
@Module({
78
imports: [PrismaModule],
89
controllers: [LimitsController],
9-
providers: [LimitsService],
10+
providers: [LimitsService, CacheService],
1011
exports: [LimitsService],
1112
})
1213
export class LimitsModule {}

src/limits/limits.service.spec.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ import { Test, TestingModule } from '@nestjs/testing';
22
import { NotFoundException } from '@nestjs/common';
33
import { LimitsService, LimitExceededException } from './limits.service';
44
import { PrismaService } from '../prisma/prisma.service';
5+
import { CacheService } from '../common/cache/cache.service';
56

67
describe('LimitsService', () => {
78
let service: LimitsService;
89
let prisma: any;
10+
let cacheService: { get: jest.Mock; set: jest.Mock; delete: jest.Mock };
911

1012
const walletId = 'wallet-uuid-1';
1113

@@ -21,8 +23,14 @@ describe('LimitsService', () => {
2123
},
2224
};
2325

26+
cacheService = { get: jest.fn(), set: jest.fn(), delete: jest.fn() };
27+
2428
const module: TestingModule = await Test.createTestingModule({
25-
providers: [LimitsService, { provide: PrismaService, useValue: prisma }],
29+
providers: [
30+
LimitsService,
31+
{ provide: PrismaService, useValue: prisma },
32+
{ provide: CacheService, useValue: cacheService },
33+
],
2634
}).compile();
2735

2836
service = module.get<LimitsService>(LimitsService);
@@ -50,6 +58,17 @@ describe('LimitsService', () => {
5058
const result = await service.getLimits(walletId);
5159
expect(result).toEqual(limit);
5260
});
61+
62+
it('should use the cache layer for wallet limits', async () => {
63+
const limit = { walletId, dailyLimit: 100, perTransactionLimit: 10 };
64+
cacheService.get.mockReturnValue(limit);
65+
66+
const result = await service.getLimits(walletId);
67+
68+
expect(result).toEqual(limit);
69+
expect(cacheService.get).toHaveBeenCalledWith(`limits:${walletId}`);
70+
expect(prisma.walletLimit.findUnique).not.toHaveBeenCalled();
71+
});
5372
});
5473

5574
describe('checkLimits', () => {

src/limits/limits.service.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import {
55
HttpStatus,
66
} from '@nestjs/common';
77
import { PrismaService } from '../prisma/prisma.service';
8-
import { CreateLimitDto, LimitPeriod } from './dto/create-limit.dto';
9-
import { UpdateLimitDto } from './dto/update-limit.dto';
8+
import { CacheService } from '../common/cache/cache.service';
9+
import { PaymentLimitsPort } from '../payments/ports/payment-limits.port';
1010

1111
export const LIMIT_ERROR_CODES = {
1212
PER_TX_LIMIT_EXCEEDED: 'LIMIT_PER_TX_EXCEEDED',
@@ -26,19 +26,36 @@ export class LimitExceededException extends HttpException {
2626
}
2727

2828
@Injectable()
29-
export class LimitsService {
30-
constructor(private readonly prisma: PrismaService) {}
29+
export class LimitsService implements PaymentLimitsPort {
30+
constructor(
31+
private readonly prisma: PrismaService,
32+
private readonly cacheService: CacheService,
33+
) {}
3134

3235
async setLimits(walletId: string, daily: number, perTx: number) {
33-
return this.prisma.walletLimit.upsert({
36+
const updated = await this.prisma.walletLimit.upsert({
3437
where: { walletId },
3538
update: { dailyLimit: daily, perTransactionLimit: perTx },
3639
create: { walletId, dailyLimit: daily, perTransactionLimit: perTx },
3740
});
41+
42+
this.cacheService.set(`limits:${walletId}`, updated);
43+
return updated;
3844
}
3945

4046
async getLimits(walletId: string) {
41-
return this.prisma.walletLimit.findUnique({ where: { walletId } });
47+
const cacheKey = `limits:${walletId}`;
48+
const cached = this.cacheService.get(cacheKey);
49+
if (cached) {
50+
return cached;
51+
}
52+
53+
const limits = await this.prisma.walletLimit.findUnique({ where: { walletId } });
54+
if (limits) {
55+
this.cacheService.set(cacheKey, limits);
56+
}
57+
58+
return limits;
4259
}
4360

4461
async checkLimits(walletId: string, amount: number): Promise<void> {
@@ -83,6 +100,9 @@ export class LimitsService {
83100
const existing = await this.getLimits(walletId);
84101
if (!existing)
85102
throw new NotFoundException(`No limits found for wallet ${walletId}`);
86-
return this.prisma.walletLimit.delete({ where: { walletId } });
103+
104+
const deleted = await this.prisma.walletLimit.delete({ where: { walletId } });
105+
this.cacheService.delete(`limits:${walletId}`);
106+
return deleted;
87107
}
88108
}

src/payments/payments-limits.integration.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { WalletsService } from '../wallets/wallets.service';
66
import { PrismaService } from '../prisma/prisma.service';
77
import { PaymentStatus } from './entities/payment.entity';
88
import { WalletStatus } from '../wallets/domain/wallet.model';
9+
import { PAYMENT_LIMITS_PORT } from './ports/payment-limits.port';
910

1011
describe('Payments and Limits Integration', () => {
1112
let paymentsService: PaymentsService;
@@ -38,6 +39,7 @@ describe('Payments and Limits Integration', () => {
3839
PaymentsService,
3940
LimitsService,
4041
{ provide: PrismaService, useValue: mockPrisma },
42+
{ provide: PAYMENT_LIMITS_PORT, useExisting: LimitsService },
4143
{ provide: WalletsService, useValue: mockWalletsService },
4244
],
4345
}).compile();

src/payments/payments.module.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,15 @@ import { PaymentsService } from './payments.service';
33
import { PaymentsController } from './payments.controller';
44
import { LimitsModule } from '../limits/limits.module';
55
import { WalletsModule } from '../wallets/wallets.module';
6+
import { LimitsService } from '../limits/limits.service';
7+
import { PAYMENT_LIMITS_PORT } from './ports/payment-limits.port';
68

79
@Module({
810
imports: [LimitsModule, WalletsModule],
911
controllers: [PaymentsController],
10-
providers: [PaymentsService],
12+
providers: [
13+
PaymentsService,
14+
{ provide: PAYMENT_LIMITS_PORT, useExisting: LimitsService },
15+
],
1116
})
1217
export class PaymentsModule {}

src/payments/payments.service.spec.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { Test, TestingModule } from '@nestjs/testing';
22
import { BadRequestException, NotFoundException } from '@nestjs/common';
33
import { PaymentsService } from './payments.service';
44
import { PrismaService } from '../prisma/prisma.service';
5-
import { LimitsService } from '../limits/limits.service';
65
import { WalletsService } from '../wallets/wallets.service';
6+
import { PAYMENT_LIMITS_PORT } from './ports/payment-limits.port';
77
import { WalletStatus } from '../wallets/domain/wallet.model';
88
import { PaymentStatus } from './entities/payment.entity';
99

@@ -26,7 +26,7 @@ const BASE_DTO = {
2626
describe('PaymentsService', () => {
2727
let service: PaymentsService;
2828
let prisma: any;
29-
let limitsService: any;
29+
let paymentLimitsPort: any;
3030
let walletsService: any;
3131

3232
beforeEach(async () => {
@@ -39,14 +39,14 @@ describe('PaymentsService', () => {
3939
count: jest.fn(),
4040
},
4141
};
42-
limitsService = { checkLimits: jest.fn() };
42+
paymentLimitsPort = { checkLimits: jest.fn() };
4343
walletsService = { findWalletById: jest.fn() };
4444

4545
const module: TestingModule = await Test.createTestingModule({
4646
providers: [
4747
PaymentsService,
4848
{ provide: PrismaService, useValue: prisma },
49-
{ provide: LimitsService, useValue: limitsService },
49+
{ provide: PAYMENT_LIMITS_PORT, useValue: paymentLimitsPort },
5050
{ provide: WalletsService, useValue: walletsService },
5151
],
5252
}).compile();
@@ -63,7 +63,7 @@ describe('PaymentsService', () => {
6363
walletsService.findWalletById
6464
.mockResolvedValueOnce(ACTIVE_WALLET)
6565
.mockResolvedValueOnce(RECEIVER_WALLET);
66-
limitsService.checkLimits.mockResolvedValue(undefined);
66+
paymentLimitsPort.checkLimits.mockResolvedValue(undefined);
6767
prisma.payment.create.mockResolvedValue({
6868
id: 1,
6969
...BASE_DTO,
@@ -78,7 +78,7 @@ describe('PaymentsService', () => {
7878
expect(walletsService.findWalletById).toHaveBeenCalledWith(
7979
BASE_DTO.receiverWalletId,
8080
);
81-
expect(limitsService.checkLimits).toHaveBeenCalledWith(
81+
expect(paymentLimitsPort.checkLimits).toHaveBeenCalledWith(
8282
BASE_DTO.walletId,
8383
BASE_DTO.amount,
8484
);
@@ -151,7 +151,7 @@ describe('PaymentsService', () => {
151151
walletsService.findWalletById
152152
.mockResolvedValueOnce(ACTIVE_WALLET)
153153
.mockResolvedValueOnce(RECEIVER_WALLET);
154-
limitsService.checkLimits.mockResolvedValue(undefined);
154+
paymentLimitsPort.checkLimits.mockResolvedValue(undefined);
155155
prisma.payment.create.mockResolvedValue({
156156
id: 1,
157157
...BASE_DTO,
@@ -160,7 +160,7 @@ describe('PaymentsService', () => {
160160

161161
await service.create(BASE_DTO);
162162

163-
expect(limitsService.checkLimits).toHaveBeenCalledWith(
163+
expect(paymentLimitsPort.checkLimits).toHaveBeenCalledWith(
164164
BASE_DTO.walletId,
165165
BASE_DTO.amount,
166166
);

src/payments/payments.service.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import {
2+
Inject,
23
Injectable,
34
NotFoundException,
45
BadRequestException,
56
} from '@nestjs/common';
67
import { CreatePaymentDto } from './dto/create-payment.dto';
78
import { UpdatePaymentDto } from './dto/update-payment.dto';
89
import { PrismaService } from '../prisma/prisma.service';
9-
import { LimitsService } from '../limits/limits.service';
1010
import { WalletsService } from '../wallets/wallets.service';
11+
import {
12+
PAYMENT_LIMITS_PORT,
13+
PaymentLimitsPort,
14+
} from './ports/payment-limits.port';
1115
import { WalletStatus } from '../wallets/domain/wallet.model';
1216
import { PaymentStatus } from './entities/payment.entity';
1317
import { PaginationDto, PaginatedResponse } from '../common/dto/pagination.dto';
@@ -24,7 +28,8 @@ const ALLOWED_TRANSITIONS: Record<string, PaymentStatus[]> = {
2428
export class PaymentsService {
2529
constructor(
2630
private readonly prisma: PrismaService,
27-
private readonly limitsService: LimitsService,
31+
@Inject(PAYMENT_LIMITS_PORT)
32+
private readonly paymentLimitsPort: PaymentLimitsPort,
2833
private readonly walletsService: WalletsService,
2934
) {}
3035

@@ -47,7 +52,7 @@ export class PaymentsService {
4752
}
4853

4954
await this.walletsService.findWalletById(receiverWalletId);
50-
await this.limitsService.checkLimits(walletId, amount);
55+
await this.paymentLimitsPort.checkLimits(walletId, amount);
5156

5257
return this.prisma.payment.create({
5358
data: {
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Injectable, InjectionToken } from '@nestjs/common';
2+
3+
export const PAYMENT_LIMITS_PORT = Symbol('PAYMENT_LIMITS_PORT') as InjectionToken;
4+
5+
@Injectable()
6+
export abstract class PaymentLimitsPort {
7+
abstract checkLimits(walletId: string, amount: number): Promise<void> | void;
8+
}

0 commit comments

Comments
 (0)