feat(M08-A): 接入权益和余额下单抵扣

This commit is contained in:
Codex
2026-06-25 22:39:02 +08:00
parent 3d885852cd
commit dda2b3cbc6
20 changed files with 555 additions and 243 deletions
+57 -4
View File
@@ -1,6 +1,7 @@
import { randomBytes } from 'node:crypto';
import type { PoolConnection, ResultSetHeader, RowDataPacket } from 'mysql2/promise';
import type { MySqlPool } from '../db/mysql.js';
import type { MarketingBenefitService } from '../wallets/marketing-benefit-service.js';
export type PricingMode = 'HOURLY' | 'OVERNIGHT' | 'FULL_DAY';
@@ -18,6 +19,14 @@ export interface QuoteInput {
adjustment?: PricingAdjustment;
}
export interface OrderBenefitInput {
couponGrantId?: string | null;
packageHoldingId?: string | null;
packageMinutes?: number;
packageCreditCents?: number;
clientRequestId?: string;
}
interface RoomPricingRow extends RowDataPacket {
id: string;
storeId: string;
@@ -33,6 +42,7 @@ interface RoomPricingRow extends RowDataPacket {
depositCents: number;
minimumMinutes: number;
maxAdvanceDays: number;
roomCategoryId: string | null;
}
interface CountRow extends RowDataPacket { total: number }
@@ -41,7 +51,10 @@ export class PricingError extends Error {
}
export class PricingRepository {
constructor(private readonly pool: MySqlPool) {}
constructor(
private readonly pool: MySqlPool,
private readonly benefits?: Pick<MarketingBenefitService, 'reserveForOrderInTransaction'>
) {}
async quote(input: QuoteInput) {
const room = await this.loadRoom(this.pool, input.tenantId, input.roomId);
@@ -55,6 +68,7 @@ export class PricingRepository {
userId: string;
holdMinutes?: number;
allowedStoreIds?: string[] | null;
benefits?: OrderBenefitInput | null;
}) {
return this.transaction(async (connection) => {
const room = await this.loadRoom(connection, input.tenantId, input.roomId, true);
@@ -66,7 +80,7 @@ export class PricingRepository {
const isHoliday = await this.isHoliday(
connection, input.tenantId, input.startAt, room.timezone
);
const quote = this.calculate(room, input, isHoliday);
let quote = this.calculate(room, input, isHoliday);
const holdMinutes = Math.min(Math.max(input.holdMinutes ?? 15, 5), 30);
const orderNo = `QP${Date.now()}${randomBytes(4).toString('hex').toUpperCase()}`;
const [orderResult] = await connection.execute<ResultSetHeader>(
@@ -79,6 +93,43 @@ export class PricingRepository {
holdMinutes, quote.totalCents]
);
const orderId = String(orderResult.insertId);
let benefitReservation = null;
const requestedBenefits = input.benefits;
if (this.benefits && requestedBenefits
&& (requestedBenefits.couponGrantId || requestedBenefits.packageHoldingId)) {
if (!requestedBenefits.clientRequestId) {
throw new PricingError('BENEFIT_CLIENT_REQUEST_ID_REQUIRED');
}
benefitReservation = await this.benefits.reserveForOrderInTransaction(connection, {
tenantId: input.tenantId,
userId: input.userId,
orderId,
storeId: String(room.storeId),
roomId: input.roomId,
roomCategoryId: room.roomCategoryId,
orderAmountCents: quote.totalCents,
orderStartAt: input.startAt,
isHoliday,
couponGrantId: requestedBenefits.couponGrantId ?? null,
packageHoldingId: requestedBenefits.packageHoldingId ?? null,
packageMinutes: requestedBenefits.packageMinutes,
packageCreditCents: requestedBenefits.packageCreditCents,
clientRequestId: requestedBenefits.clientRequestId,
traceId: requestedBenefits.clientRequestId
});
quote = this.calculate(room, {
...input,
adjustment: {
discountCents: benefitReservation.discountCents,
packageCreditCents: benefitReservation.packageCreditCents
}
}, isHoliday);
await connection.execute(
`UPDATE qipai_orders SET total_amount_cents = ?
WHERE tenant_id = ? AND id = ?`,
[quote.totalCents, input.tenantId, orderId]
);
}
await connection.execute(
`INSERT INTO qipai_order_price_snapshots
(tenant_id, order_id, pricing_mode, duration_minutes, unit_price_cents,
@@ -115,7 +166,8 @@ export class PricingRepository {
storeId: String(room.storeId),
reservationId: String(reservationResult.insertId),
holdMinutes,
quote
quote,
benefitReservation
};
});
}
@@ -250,7 +302,8 @@ export class PricingRepository {
r.minimum_spend_cents AS minimumSpendCents,
r.deposit_cents AS depositCents,
r.minimum_minutes AS minimumMinutes,
r.max_advance_days AS maxAdvanceDays
r.max_advance_days AS maxAdvanceDays,
r.room_category_id AS roomCategoryId
FROM qipai_rooms r
INNER JOIN qipai_stores s
ON s.id = r.store_id AND s.tenant_id = r.tenant_id AND s.deleted_at IS NULL