feat(M08-A): 接入权益和余额下单抵扣
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import type { PoolConnection, ResultSetHeader, RowDataPacket } from 'mysql2/promise';
|
||||
import type { AccessProfile } from '../auth/rbac-repository.js';
|
||||
import type { MySqlPool } from '../db/mysql.js';
|
||||
import type { MarketingBenefitService } from '../wallets/marketing-benefit-service.js';
|
||||
|
||||
export const orderActions = [
|
||||
'SUBMIT', 'CONFIRM_PAYMENT', 'RESERVE', 'START', 'FINISH', 'CANCEL',
|
||||
@@ -71,7 +72,10 @@ export class OrderStateError extends Error {
|
||||
}
|
||||
|
||||
export class OrderStateRepository {
|
||||
constructor(private readonly pool: MySqlPool) {}
|
||||
constructor(
|
||||
private readonly pool: MySqlPool,
|
||||
private readonly benefits?: Pick<MarketingBenefitService, 'releaseReservedInTransaction'>
|
||||
) {}
|
||||
|
||||
async transition(actor: OrderActor, orderId: string, action: OrderAction, reason = '') {
|
||||
return this.transaction(async (connection) => {
|
||||
@@ -223,6 +227,21 @@ export class OrderStateRepository {
|
||||
WHERE tenant_id = ? AND order_id = ?`,
|
||||
[tenantId, orderId]
|
||||
);
|
||||
if (this.benefits) {
|
||||
const [users] = await connection.execute<RowDataPacket[]>(
|
||||
`SELECT user_id AS userId FROM qipai_order_user_access
|
||||
WHERE tenant_id = ? AND order_id = ? ORDER BY id LIMIT 1`,
|
||||
[tenantId, orderId]
|
||||
);
|
||||
if (users[0]?.userId) {
|
||||
await this.benefits.releaseReservedInTransaction(connection, {
|
||||
tenantId,
|
||||
userId: String(users[0].userId),
|
||||
orderId,
|
||||
traceId: `order-benefit-release-${orderId}`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user