feat: complete cmpp platform phases 0-5
This commit is contained in:
@@ -0,0 +1,349 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
export interface CreateBillingPlanDto {
|
||||
name: string;
|
||||
priceCents: number;
|
||||
smsUnits: number;
|
||||
validDays: number;
|
||||
status?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface CreateTenantAccountDto {
|
||||
tenantId: string;
|
||||
balanceCents?: number;
|
||||
smsUnits?: number;
|
||||
creditCents?: number;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export interface CreateAccountTransactionDto {
|
||||
tenantId: string;
|
||||
transactionType: string;
|
||||
amountCents?: number;
|
||||
smsUnits?: number;
|
||||
balanceAfter?: number;
|
||||
relatedType?: string;
|
||||
relatedId?: string;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export interface CreateBillingRuleDto {
|
||||
code: string;
|
||||
name: string;
|
||||
chargeBasis?: string;
|
||||
unitPrice: number;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export interface CreateRechargeOrderDto {
|
||||
tenantId: string;
|
||||
planId?: string;
|
||||
amountCents?: number;
|
||||
smsUnits?: number;
|
||||
payMethod?: string;
|
||||
operatorId?: string;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export interface EstimateSmsCostDto {
|
||||
tenantId: string;
|
||||
applicationId?: string;
|
||||
content: string;
|
||||
phoneCount: number;
|
||||
unitPrice?: number;
|
||||
taskId?: string;
|
||||
}
|
||||
|
||||
export interface BillingActionDto {
|
||||
tenantId: string;
|
||||
amountCents?: number;
|
||||
smsUnits?: number;
|
||||
relatedType?: string;
|
||||
relatedId?: string;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export interface CreateSmsBillingRecordDto {
|
||||
tenantId: string;
|
||||
applicationId?: string;
|
||||
taskId?: string;
|
||||
messageId?: string;
|
||||
phoneNumber?: string;
|
||||
content: string;
|
||||
unitPrice?: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class BillingService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
listPlans() {
|
||||
return this.prisma.billingPlan.findMany({ orderBy: { createdAt: 'desc' }, take: 100 });
|
||||
}
|
||||
|
||||
createPlan(data: CreateBillingPlanDto) {
|
||||
return this.prisma.billingPlan.create({
|
||||
data: {
|
||||
name: data.name,
|
||||
priceCents: data.priceCents,
|
||||
smsUnits: data.smsUnits,
|
||||
validDays: data.validDays,
|
||||
status: data.status ?? 'active',
|
||||
description: data.description,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
listAccounts() {
|
||||
return this.prisma.tenantAccount.findMany({
|
||||
include: { tenant: true },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 100,
|
||||
});
|
||||
}
|
||||
|
||||
createAccount(data: CreateTenantAccountDto) {
|
||||
const createData: Prisma.TenantAccountUncheckedCreateInput = {
|
||||
tenantId: data.tenantId,
|
||||
balanceCents: data.balanceCents ?? 0,
|
||||
smsUnits: data.smsUnits ?? 0,
|
||||
creditCents: data.creditCents ?? 0,
|
||||
status: data.status ?? 'active',
|
||||
};
|
||||
return this.prisma.tenantAccount.create({ data: createData });
|
||||
}
|
||||
|
||||
listTransactions(tenantId?: string) {
|
||||
return this.prisma.accountTransaction.findMany({
|
||||
where: tenantId ? { tenantId } : undefined,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 100,
|
||||
});
|
||||
}
|
||||
|
||||
createTransaction(data: CreateAccountTransactionDto) {
|
||||
const createData: Prisma.AccountTransactionUncheckedCreateInput = {
|
||||
tenantId: data.tenantId,
|
||||
transactionType: data.transactionType,
|
||||
amountCents: data.amountCents ?? 0,
|
||||
smsUnits: data.smsUnits ?? 0,
|
||||
balanceAfter: data.balanceAfter ?? 0,
|
||||
relatedType: data.relatedType,
|
||||
relatedId: data.relatedId,
|
||||
remark: data.remark,
|
||||
};
|
||||
return this.prisma.accountTransaction.create({ data: createData });
|
||||
}
|
||||
|
||||
listRechargeOrders(tenantId?: string) {
|
||||
return this.prisma.rechargeOrder.findMany({
|
||||
where: tenantId ? { tenantId } : undefined,
|
||||
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;
|
||||
const smsUnits = data.smsUnits ?? plan?.smsUnits ?? 0;
|
||||
const order = await this.prisma.rechargeOrder.create({
|
||||
data: {
|
||||
tenantId: data.tenantId,
|
||||
planId: data.planId,
|
||||
orderNo: `R${Date.now()}${Math.floor(Math.random() * 10000).toString().padStart(4, '0')}`,
|
||||
amountCents,
|
||||
smsUnits,
|
||||
status: 'paid',
|
||||
payMethod: data.payMethod ?? 'manual',
|
||||
paidAt: new Date(),
|
||||
operatorId: data.operatorId,
|
||||
remark: data.remark,
|
||||
},
|
||||
});
|
||||
|
||||
await this.applyAccountDelta({
|
||||
tenantId: data.tenantId,
|
||||
transactionType: 'recharge',
|
||||
amountCents,
|
||||
smsUnits,
|
||||
relatedType: 'recharge_order',
|
||||
relatedId: order.id,
|
||||
remark: data.remark,
|
||||
});
|
||||
|
||||
return order;
|
||||
}
|
||||
|
||||
estimateSmsCost(data: EstimateSmsCostDto) {
|
||||
const billingUnits = estimateBillingUnits(data.content);
|
||||
const unitPrice = data.unitPrice ?? 0;
|
||||
const totalUnits = billingUnits * data.phoneCount;
|
||||
return {
|
||||
tenantId: data.tenantId,
|
||||
applicationId: data.applicationId,
|
||||
taskId: data.taskId,
|
||||
contentLength: [...data.content].length,
|
||||
phoneCount: data.phoneCount,
|
||||
billingUnitsPerMessage: billingUnits,
|
||||
totalBillingUnits: totalUnits,
|
||||
unitPrice,
|
||||
amountCents: totalUnits * unitPrice,
|
||||
};
|
||||
}
|
||||
|
||||
async checkAccount(data: BillingActionDto) {
|
||||
const account = await this.getAccountOrCreate(data.tenantId);
|
||||
const requiredAmount = data.amountCents ?? 0;
|
||||
const requiredUnits = data.smsUnits ?? 0;
|
||||
const availableAmount = account.balanceCents + account.creditCents;
|
||||
return {
|
||||
tenantId: data.tenantId,
|
||||
requiredAmount,
|
||||
requiredUnits,
|
||||
availableAmount,
|
||||
availableSmsUnits: account.smsUnits,
|
||||
canSend: availableAmount >= requiredAmount && account.smsUnits >= requiredUnits,
|
||||
};
|
||||
}
|
||||
|
||||
freeze(data: BillingActionDto) {
|
||||
return this.applyAccountDelta({
|
||||
...data,
|
||||
transactionType: 'frozen',
|
||||
amountCents: -(data.amountCents ?? 0),
|
||||
smsUnits: -(data.smsUnits ?? 0),
|
||||
});
|
||||
}
|
||||
|
||||
charge(data: BillingActionDto) {
|
||||
return this.applyAccountDelta({
|
||||
...data,
|
||||
transactionType: 'charged',
|
||||
amountCents: -(data.amountCents ?? 0),
|
||||
smsUnits: -(data.smsUnits ?? 0),
|
||||
});
|
||||
}
|
||||
|
||||
release(data: BillingActionDto) {
|
||||
return this.applyAccountDelta({
|
||||
...data,
|
||||
transactionType: 'released',
|
||||
amountCents: data.amountCents ?? 0,
|
||||
smsUnits: data.smsUnits ?? 0,
|
||||
});
|
||||
}
|
||||
|
||||
refund(data: BillingActionDto) {
|
||||
return this.applyAccountDelta({
|
||||
...data,
|
||||
transactionType: 'refunded',
|
||||
amountCents: data.amountCents ?? 0,
|
||||
smsUnits: data.smsUnits ?? 0,
|
||||
});
|
||||
}
|
||||
|
||||
adjust(data: BillingActionDto) {
|
||||
return this.applyAccountDelta({
|
||||
...data,
|
||||
transactionType: 'adjusted',
|
||||
amountCents: data.amountCents ?? 0,
|
||||
smsUnits: data.smsUnits ?? 0,
|
||||
});
|
||||
}
|
||||
|
||||
async createSmsBillingRecord(data: CreateSmsBillingRecordDto) {
|
||||
const estimate = this.estimateSmsCost({
|
||||
tenantId: data.tenantId,
|
||||
applicationId: data.applicationId,
|
||||
taskId: data.taskId,
|
||||
content: data.content,
|
||||
phoneCount: 1,
|
||||
unitPrice: data.unitPrice ?? 0,
|
||||
});
|
||||
return this.prisma.smsBillingRecord.create({
|
||||
data: {
|
||||
tenantId: data.tenantId,
|
||||
applicationId: data.applicationId,
|
||||
taskId: data.taskId,
|
||||
messageId: data.messageId,
|
||||
phoneNumber: data.phoneNumber,
|
||||
contentLength: estimate.contentLength,
|
||||
billingUnits: estimate.billingUnitsPerMessage,
|
||||
unitPrice: estimate.unitPrice,
|
||||
amountCents: estimate.amountCents,
|
||||
billingStatus: 'estimated',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
listSmsBillingRecords(tenantId?: string, taskId?: string) {
|
||||
return this.prisma.smsBillingRecord.findMany({
|
||||
where: { tenantId, taskId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 100,
|
||||
});
|
||||
}
|
||||
|
||||
listRules() {
|
||||
return this.prisma.billingRule.findMany({ orderBy: { createdAt: 'desc' }, take: 100 });
|
||||
}
|
||||
|
||||
createRule(data: CreateBillingRuleDto) {
|
||||
return this.prisma.billingRule.create({
|
||||
data: {
|
||||
code: data.code,
|
||||
name: data.name,
|
||||
chargeBasis: data.chargeBasis ?? 'submit_success',
|
||||
unitPrice: data.unitPrice,
|
||||
status: data.status ?? 'active',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async getAccountOrCreate(tenantId: string) {
|
||||
return this.prisma.tenantAccount.upsert({
|
||||
where: { tenantId },
|
||||
update: {},
|
||||
create: { tenantId, balanceCents: 0, smsUnits: 0, creditCents: 0, status: 'active' },
|
||||
});
|
||||
}
|
||||
|
||||
private async applyAccountDelta(data: CreateAccountTransactionDto) {
|
||||
const account = await this.getAccountOrCreate(data.tenantId);
|
||||
const nextBalance = account.balanceCents + (data.amountCents ?? 0);
|
||||
const nextUnits = account.smsUnits + (data.smsUnits ?? 0);
|
||||
await this.prisma.tenantAccount.update({
|
||||
where: { tenantId: data.tenantId },
|
||||
data: {
|
||||
balanceCents: nextBalance,
|
||||
smsUnits: nextUnits,
|
||||
},
|
||||
});
|
||||
|
||||
return this.prisma.accountTransaction.create({
|
||||
data: {
|
||||
tenantId: data.tenantId,
|
||||
transactionType: data.transactionType,
|
||||
amountCents: data.amountCents ?? 0,
|
||||
smsUnits: data.smsUnits ?? 0,
|
||||
balanceAfter: nextBalance,
|
||||
relatedType: data.relatedType,
|
||||
relatedId: data.relatedId,
|
||||
remark: data.remark,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function estimateBillingUnits(content: string) {
|
||||
const length = [...content].length;
|
||||
if (length <= 70) {
|
||||
return 1;
|
||||
}
|
||||
return Math.ceil(length / 67);
|
||||
}
|
||||
Reference in New Issue
Block a user