feat: improve application access and money precision
This commit is contained in:
@@ -61,6 +61,16 @@ describe('BillingService', () => {
|
||||
amountCents: 20,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
service.estimateSmsCost({ tenantId: 'tenant-1', content: '四位小数单价', phoneCount: 2, unitPrice: 325 }),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
unitPrice: 325,
|
||||
totalBillingUnits: 2,
|
||||
amountCents: 650,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('allows sending only when cash balance plus credit is greater than zero', async () => {
|
||||
@@ -81,7 +91,7 @@ describe('BillingService', () => {
|
||||
await expect(service.checkAccount({ tenantId: 'tenant-1', amountCents: 999999 })).resolves.toEqual(
|
||||
expect.objectContaining({ availableAmount: 1500, creditCents: 500, canSend: true }),
|
||||
);
|
||||
await expect(service.updateCreditLimit('tenant-1', { creditCents: 1.5 })).rejects.toThrow('授信额度必须为整数金额');
|
||||
await expect(service.updateCreditLimit('tenant-1', { creditCents: 1.5 })).rejects.toThrow('授信额度最多支持人民币小数点后 4 位');
|
||||
expect(prisma.operationLog.create).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({
|
||||
action: 'billing.credit_limit_updated',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { assertMoneyUnits, moneyToNumber } from '../common/money';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
export interface CreateTenantAccountDto {
|
||||
@@ -87,7 +88,8 @@ export class BillingService {
|
||||
}
|
||||
|
||||
createAccount(data: CreateTenantAccountDto) {
|
||||
assertCreditAmount(data.creditCents ?? 0);
|
||||
assertMoneyUnits(data.balanceCents ?? 0, '账户余额', { allowNegative: true });
|
||||
assertMoneyUnits(data.creditCents ?? 0, '授信额度', { allowNegative: true });
|
||||
const createData: Prisma.TenantAccountUncheckedCreateInput = {
|
||||
tenantId: data.tenantId,
|
||||
balanceCents: data.balanceCents ?? 0,
|
||||
@@ -98,7 +100,7 @@ export class BillingService {
|
||||
}
|
||||
|
||||
async updateCreditLimit(tenantId: string, data: UpdateCreditLimitDto) {
|
||||
assertCreditAmount(data.creditCents);
|
||||
assertMoneyUnits(data.creditCents, '授信额度', { allowNegative: true });
|
||||
const account = await this.getAccountOrCreate(tenantId);
|
||||
const updated = await this.prisma.tenantAccount.update({
|
||||
where: { tenantId },
|
||||
@@ -112,7 +114,7 @@ export class BillingService {
|
||||
resource: 'tenant_account',
|
||||
resourceId: account.id,
|
||||
detail: {
|
||||
previousCreditCents: account.creditCents,
|
||||
previousCreditCents: moneyToNumber(account.creditCents),
|
||||
creditCents: data.creditCents,
|
||||
remark: data.remark,
|
||||
} as Prisma.InputJsonValue,
|
||||
@@ -148,7 +150,7 @@ export class BillingService {
|
||||
},
|
||||
select: { relatedId: true, balanceAfter: true },
|
||||
});
|
||||
const balanceAfterByOrderId = new Map(transactions.map((transaction) => [transaction.relatedId, transaction.balanceAfter]));
|
||||
const balanceAfterByOrderId = new Map(transactions.map((transaction) => [transaction.relatedId, moneyToNumber(transaction.balanceAfter)]));
|
||||
|
||||
return orders.map((order) => ({
|
||||
...order,
|
||||
@@ -158,6 +160,7 @@ export class BillingService {
|
||||
|
||||
async createRechargeOrder(data: CreateRechargeOrderDto) {
|
||||
const amountCents = data.amountCents;
|
||||
assertMoneyUnits(amountCents, '充值金额', { allowNegative: true, allowZero: false });
|
||||
const order = await this.prisma.rechargeOrder.create({
|
||||
data: {
|
||||
tenantId: data.tenantId,
|
||||
@@ -211,7 +214,10 @@ export class BillingService {
|
||||
estimateSmsCost(data: EstimateSmsCostDto) {
|
||||
const billingUnits = estimateBillingUnits(data.content);
|
||||
const unitPrice = data.unitPrice ?? 0;
|
||||
assertMoneyUnits(unitPrice, '短信单价');
|
||||
const totalUnits = billingUnits * data.phoneCount;
|
||||
const amountCents = totalUnits * unitPrice;
|
||||
assertMoneyUnits(amountCents, '短信计费金额');
|
||||
return {
|
||||
tenantId: data.tenantId,
|
||||
applicationId: data.applicationId,
|
||||
@@ -221,20 +227,22 @@ export class BillingService {
|
||||
billingUnitsPerMessage: billingUnits,
|
||||
totalBillingUnits: totalUnits,
|
||||
unitPrice,
|
||||
amountCents: totalUnits * unitPrice,
|
||||
amountCents,
|
||||
};
|
||||
}
|
||||
|
||||
async checkAccount(data: BillingActionDto) {
|
||||
const account = await this.getAccountOrCreate(data.tenantId);
|
||||
const requiredAmount = data.amountCents ?? 0;
|
||||
const availableAmount = account.balanceCents + account.creditCents;
|
||||
const balanceCents = moneyToNumber(account.balanceCents);
|
||||
const creditCents = moneyToNumber(account.creditCents);
|
||||
const availableAmount = balanceCents + creditCents;
|
||||
return {
|
||||
tenantId: data.tenantId,
|
||||
requiredAmount,
|
||||
availableAmount,
|
||||
balanceCents: account.balanceCents,
|
||||
creditCents: account.creditCents,
|
||||
balanceCents,
|
||||
creditCents,
|
||||
canSend: availableAmount > 0,
|
||||
};
|
||||
}
|
||||
@@ -316,6 +324,7 @@ export class BillingService {
|
||||
}
|
||||
|
||||
createRule(data: CreateBillingRuleDto) {
|
||||
assertMoneyUnits(data.unitPrice, '计费规则单价');
|
||||
return this.prisma.billingRule.create({
|
||||
data: {
|
||||
code: data.code,
|
||||
@@ -337,7 +346,7 @@ export class BillingService {
|
||||
|
||||
private async applyAccountDelta(data: CreateAccountTransactionDto) {
|
||||
const account = await this.getAccountOrCreate(data.tenantId);
|
||||
const nextBalance = account.balanceCents + (data.amountCents ?? 0);
|
||||
const nextBalance = moneyToNumber(account.balanceCents) + (data.amountCents ?? 0);
|
||||
await this.prisma.tenantAccount.update({
|
||||
where: { tenantId: data.tenantId },
|
||||
data: {
|
||||
@@ -359,12 +368,6 @@ export class BillingService {
|
||||
}
|
||||
}
|
||||
|
||||
function assertCreditAmount(creditCents: number) {
|
||||
if (!Number.isInteger(creditCents)) {
|
||||
throw new BadRequestException('授信额度必须为整数金额(分)');
|
||||
}
|
||||
}
|
||||
|
||||
function estimateBillingUnits(content: string) {
|
||||
const length = [...content].length;
|
||||
if (length <= 70) {
|
||||
|
||||
Reference in New Issue
Block a user