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: '退款作废权益', }), }); } }); } } }