import { BadRequestException, Injectable, Logger, NotFoundException, } from '@nestjs/common' import { CardType, Order } from '@prisma/client' import { OrderStatus } from '@mp-pilates/shared' import { PrismaService } from '../prisma/prisma.service' import { WechatPayService, WxPaymentParams } from './wechat-pay.service' import { InviteService } from '../invite/invite.service' import { MembershipService } from '../membership/membership.service' export interface CreateOrderResult { order: Order paymentParams: WxPaymentParams } export interface PaginatedOrders { items: T[] total: number page: number limit: number } @Injectable() export class PaymentService { private readonly logger = new Logger(PaymentService.name) constructor( private readonly prisma: PrismaService, private readonly wechatPayService: WechatPayService, private readonly inviteService: InviteService, private readonly membershipService: MembershipService, ) {} // ─── User: create order ──────────────────────────────────────────────────── async createOrder(userId: string, cardTypeId: string, inviterId?: string): Promise { const cardType = await this.prisma.cardType.findUnique({ where: { id: cardTypeId } }) if (!cardType) { throw new NotFoundException(`CardType ${cardTypeId} not found`) } if (!cardType.isActive) { throw new BadRequestException(`CardType ${cardTypeId} is not active`) } const user = await this.prisma.user.findUnique({ where: { id: userId } }) if (!user) { throw new NotFoundException(`User ${userId} not found`) } if (cardType.type === 'TRIAL') { await this.inviteService.validateInviterForTrialOrder(userId, inviterId) } const referral = await this.prisma.inviteReferral.findUnique({ where: { inviteeId: userId } }) const amount = referral ? Math.round(Number(cardType.price) * 95 / 100) : Number(cardType.price) const orderNo = `${Date.now()}${Math.random().toString(36).substring(2, 8)}` const order = await this.prisma.order.create({ data: { userId, cardTypeId, orderNo, amount, inviteInviterId: referral?.inviterId, purchasedCategory: cardType.type, status: OrderStatus.PENDING, }, }) const paymentParams = await this.wechatPayService.createUnifiedOrder({ orderNo, amount, openid: user.openid, description: cardType.name, }) return { order: { ...order }, paymentParams: { ...paymentParams } } } // ─── WeChat callback ─────────────────────────────────────────────────────── async handleWxNotify(headers: Record, body: Record): Promise { const rawBody = typeof body === 'string' ? body : JSON.stringify(body) const isValid = this.wechatPayService.verifySignature(headers, rawBody) if (!isValid) { this.logger.warn('WeChat Pay signature verification failed') return this.buildFailXml('FAIL', 'SIGN_ERROR') } const notification = this.wechatPayService.parseNotification(body) if (!notification.success) { this.logger.warn(`WeChat Pay notification not success: orderNo=${notification.orderNo}`) return this.buildSuccessXml() } const existingOrder = await this.prisma.order.findUnique({ where: { orderNo: notification.orderNo }, }) if (!existingOrder) { this.logger.warn(`Order not found: orderNo=${notification.orderNo}`) return this.buildSuccessXml() } // Idempotency: already processed if (existingOrder.status === OrderStatus.PAID) { this.logger.log(`Order already PAID (idempotent): orderNo=${notification.orderNo}`) return this.buildSuccessXml() } const cardType = await this.prisma.cardType.findUnique({ where: { id: existingOrder.cardTypeId }, }) if (!cardType) { this.logger.error(`CardType not found for order ${existingOrder.id}`) return this.buildFailXml('FAIL', 'CARD_TYPE_NOT_FOUND') } const now = new Date() await this.prisma.$transaction(async (tx) => { const claimed = await tx.order.updateMany({ where: { id: existingOrder.id, status: OrderStatus.PENDING }, data: { status: OrderStatus.PAID }, }) if (!claimed.count) return const membership = await this.membershipService.grantPurchasedCard( tx, existingOrder.userId, cardType, now, ) await tx.order.update({ where: { id: existingOrder.id }, data: { status: OrderStatus.PAID, wxTransactionId: notification.wxTransactionId, paidAt: now, membershipId: membership.id, }, }) await this.inviteService.rewardPaidOrder(tx, existingOrder, now) }) await this.inviteService.recordTrialOrderPaid(existingOrder.id) this.logger.log(`Order PAID and membership granted: orderNo=${notification.orderNo}`) return this.buildSuccessXml() } // ─── User: list own orders ───────────────────────────────────────────────── async getMyOrders( userId: string, page = 1, limit = 10, ): Promise> { const skip = (page - 1) * limit const [data, total] = await Promise.all([ this.prisma.order.findMany({ where: { userId }, include: { cardType: true }, orderBy: { createdAt: 'desc' }, skip, take: limit, }), this.prisma.order.count({ where: { userId } }), ]) return { items: data.map((o) => ({ ...o, cardType: { ...o.cardType } })), total, page, limit, } } // ─── Admin: list all orders ──────────────────────────────────────────────── async getAllOrders( page = 1, limit = 10, status?: OrderStatus, ): Promise> { const skip = (page - 1) * limit const where = status ? { status } : {} const [data, total] = await Promise.all([ this.prisma.order.findMany({ where, include: { cardType: true, user: { select: { id: true, nickname: true, phone: true } }, }, orderBy: { createdAt: 'desc' }, skip, take: limit, }), this.prisma.order.count({ where }), ]) this.logger.log(`getAllOrders: page=${page}, limit=${limit}, status=${status}, count=${total}`) return { items: data.map((o) => ({ ...o, cardType: { ...o.cardType }, user: { ...o.user }, })), total, page, limit, } } // ─── Helpers ─────────────────────────────────────────────────────────────── private buildSuccessXml(): string { return `` } private buildFailXml(code: string, msg: string): string { return `` } }