161 lines
5.1 KiB
TypeScript
161 lines
5.1 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
import type { Prisma } from '@prisma/client';
|
|
import { calcBenefitAmount, calcBenefitSummary, generateCouponNo } from '@dukang/domain';
|
|
import { PrismaService } from '../../common/prisma/prisma.module';
|
|
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
|
import { buildBenefitLedgerEvent, benefitLedgerWhere } from '../../common/event/event.helpers';
|
|
|
|
export type CouponAllocation = { couponId: string; amount: number };
|
|
|
|
@Injectable()
|
|
export class BenefitService {
|
|
constructor(private readonly prisma: PrismaService) {}
|
|
|
|
async grantOnOrderPaid(orderId: bigint) {
|
|
const order = await this.prisma.order.findUniqueOrThrow({
|
|
where: { id: orderId },
|
|
});
|
|
|
|
const product = await this.prisma.commonProductItem.findUnique({ where: { id: order.productId } });
|
|
const unitBenefit = calcBenefitAmount({
|
|
price: Number(order.listUnitPrice),
|
|
benefitAmount: product?.benefitAmount ? Number(product.benefitAmount) : null,
|
|
});
|
|
const totalBenefit = unitBenefit * order.quantity;
|
|
|
|
const coupon = await this.prisma.benefitCoupon.create({
|
|
data: {
|
|
couponNo: generateCouponNo(),
|
|
userId: order.userId,
|
|
orderId: order.id,
|
|
totalAmount: totalBenefit,
|
|
balance: totalBenefit,
|
|
sourceProduct: order.productName,
|
|
},
|
|
});
|
|
|
|
await this.prisma.commonEvent.create({
|
|
data: buildBenefitLedgerEvent({
|
|
userId: order.userId,
|
|
couponId: coupon.id,
|
|
type: 'GRANT',
|
|
amount: totalBenefit,
|
|
balanceAfter: totalBenefit,
|
|
refType: 'ORDER',
|
|
refId: order.id,
|
|
remark: '购酒赠券',
|
|
}),
|
|
});
|
|
|
|
return serializeBigInt(coupon);
|
|
}
|
|
|
|
async listCoupons(userId: bigint) {
|
|
const list = await this.prisma.benefitCoupon.findMany({
|
|
where: { userId, status: { in: ['ACTIVE', 'USED_UP'] } },
|
|
orderBy: { createdAt: 'desc' },
|
|
});
|
|
return serializeBigInt(list);
|
|
}
|
|
|
|
async getSummary(userId: bigint) {
|
|
const coupons = await this.prisma.benefitCoupon.findMany({
|
|
where: { userId, status: 'ACTIVE' },
|
|
orderBy: { createdAt: 'asc' },
|
|
});
|
|
const summary = calcBenefitSummary(
|
|
coupons.map((c) => Number(c.balance)),
|
|
);
|
|
return serializeBigInt(summary);
|
|
}
|
|
|
|
async getLedger(userId: bigint, couponId?: bigint) {
|
|
const list = await this.prisma.commonEvent.findMany({
|
|
where: benefitLedgerWhere(userId, couponId),
|
|
orderBy: { createdAt: 'desc' },
|
|
});
|
|
return serializeBigInt(list);
|
|
}
|
|
|
|
async getCoupon(userId: bigint, couponId: bigint) {
|
|
const coupon = await this.prisma.benefitCoupon.findFirst({
|
|
where: { id: couponId, userId },
|
|
});
|
|
if (!coupon) return null;
|
|
const ledgers = await this.prisma.commonEvent.findMany({
|
|
where: benefitLedgerWhere(undefined, couponId),
|
|
orderBy: { createdAt: 'desc' },
|
|
});
|
|
return serializeBigInt({ coupon, ledgers });
|
|
}
|
|
|
|
/** 核销扣减券余额(乐观锁),由 redeem 模块调用 */
|
|
async deductCoupons(
|
|
tx: Prisma.TransactionClient,
|
|
allocations: CouponAllocation[],
|
|
refType: 'STORE',
|
|
refId: bigint,
|
|
) {
|
|
for (const alloc of allocations) {
|
|
const coupon = await tx.benefitCoupon.findUniqueOrThrow({
|
|
where: { id: BigInt(alloc.couponId) },
|
|
});
|
|
const allocAmount = alloc.amount;
|
|
const updated = await tx.benefitCoupon.updateMany({
|
|
where: { id: coupon.id, version: coupon.version, balance: { gte: allocAmount } },
|
|
data: {
|
|
usedAmount: { increment: allocAmount },
|
|
balance: { decrement: allocAmount },
|
|
version: { increment: 1 },
|
|
status: Number(coupon.balance) - allocAmount <= 0 ? 'USED_UP' : 'ACTIVE',
|
|
},
|
|
});
|
|
if (updated.count === 0) throw new Error('BENEFIT_DEDUCT_CONFLICT');
|
|
|
|
const newBalance = Number(coupon.balance) - allocAmount;
|
|
await tx.commonEvent.create({
|
|
data: buildBenefitLedgerEvent({
|
|
userId: coupon.userId,
|
|
couponId: coupon.id,
|
|
type: 'REDEEM',
|
|
amount: -allocAmount,
|
|
balanceAfter: newBalance,
|
|
refType,
|
|
refId,
|
|
}),
|
|
});
|
|
}
|
|
}
|
|
|
|
/** 退款作废权益 */
|
|
async voidCouponsOnRefund(orderId: bigint) {
|
|
const coupons = await this.prisma.benefitCoupon.findMany({
|
|
where: { orderId, status: { in: ['ACTIVE', 'USED_UP'] } },
|
|
});
|
|
for (const coupon of coupons) {
|
|
const balance = Number(coupon.balance);
|
|
if (balance <= 0 && coupon.status === 'USED_UP') continue;
|
|
await this.prisma.$transaction(async (tx) => {
|
|
await tx.benefitCoupon.update({
|
|
where: { id: coupon.id },
|
|
data: { status: 'VOID', balance: 0 },
|
|
});
|
|
if (balance > 0) {
|
|
await tx.commonEvent.create({
|
|
data: buildBenefitLedgerEvent({
|
|
userId: coupon.userId,
|
|
couponId: coupon.id,
|
|
type: 'REFUND_VOID',
|
|
amount: -balance,
|
|
balanceAfter: 0,
|
|
refType: 'ORDER',
|
|
refId: orderId,
|
|
remark: '退款作废权益',
|
|
}),
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|