feat: add manual recharge and operation logs

This commit is contained in:
hectorzhao
2026-07-01 15:54:56 +08:00
parent 50d75f1cf3
commit 7ff6d7eac3
14 changed files with 339 additions and 32 deletions
+11
View File
@@ -4,6 +4,7 @@ import { TenantId } from '../common/tenant-id.decorator';
import {
BillingService,
BillingActionDto,
CreateManualRechargeDto,
CreateRechargeOrderDto,
CreateAccountTransactionDto,
CreateBillingPlanDto,
@@ -58,6 +59,16 @@ export class BillingController {
return this.billing.createRechargeOrder(body);
}
@Get('manual-recharges')
listManualRechargeRecords(@TenantId() tenantId?: string) {
return this.billing.listManualRechargeRecords(tenantId);
}
@Post('manual-recharges')
createManualRecharge(@Body() body: CreateManualRechargeDto) {
return this.billing.createManualRecharge(body);
}
@Post('estimate')
estimateSmsCost(@Body() body: EstimateSmsCostDto) {
return this.billing.estimateSmsCost(body);
+29
View File
@@ -104,6 +104,35 @@ describe('BillingService', () => {
});
});
it('creates manual recharge records for operator top-ups', async () => {
const prisma = createPrismaMock();
const service = new BillingService(prisma as never);
const order = await service.createManualRecharge({
tenantId: 'tenant-1',
amountCents: 2000,
smsUnits: 0,
operatorId: 'admin-1',
remark: '线下转账人工充值',
});
expect(order).toEqual(expect.objectContaining({ amountCents: 2000, payMethod: 'manual_topup', status: 'paid' }));
expect(prisma.rechargeOrder.create).toHaveBeenCalledWith({
data: expect.objectContaining({
payMethod: 'manual_topup',
operatorId: 'admin-1',
remark: '线下转账人工充值',
}),
});
expect(prisma.accountTransaction.create).toHaveBeenCalledWith({
data: expect.objectContaining({
transactionType: 'recharge',
amountCents: 2000,
relatedType: 'recharge_order',
}),
});
});
it('writes freeze, charge, release, refund, and adjustment transactions', async () => {
const prisma = createPrismaMock();
const service = new BillingService(prisma as never);
+31
View File
@@ -48,6 +48,14 @@ export interface CreateRechargeOrderDto {
remark?: string;
}
export interface CreateManualRechargeDto {
tenantId: string;
amountCents: number;
smsUnits?: number;
operatorId?: string;
remark?: string;
}
export interface EstimateSmsCostDto {
tenantId: string;
applicationId?: string;
@@ -147,6 +155,18 @@ export class BillingService {
});
}
listManualRechargeRecords(tenantId?: string) {
return this.prisma.rechargeOrder.findMany({
where: {
tenantId,
payMethod: 'manual_topup',
},
include: { plan: true },
orderBy: { createdAt: 'desc' },
take: 100,
});
}
async createRechargeOrder(data: CreateRechargeOrderDto) {
const plan = data.planId ? await this.prisma.billingPlan.findUnique({ where: { id: data.planId } }) : null;
const amountCents = data.amountCents ?? plan?.priceCents ?? 0;
@@ -179,6 +199,17 @@ export class BillingService {
return order;
}
createManualRecharge(data: CreateManualRechargeDto) {
return this.createRechargeOrder({
tenantId: data.tenantId,
amountCents: data.amountCents,
smsUnits: data.smsUnits ?? 0,
payMethod: 'manual_topup',
operatorId: data.operatorId,
remark: data.remark,
});
}
estimateSmsCost(data: EstimateSmsCostDto) {
const billingUnits = estimateBillingUnits(data.content);
const unitPrice = data.unitPrice ?? 0;