feat(M08-A): 接入顾客端续费换房分享

This commit is contained in:
Codex
2026-06-25 11:34:23 +08:00
parent a4e13d911e
commit 874b9b8e29
7 changed files with 337 additions and 5 deletions
@@ -83,6 +83,14 @@ export class OrderManagementRepository {
});
}
async customerRenew(actor: OrderActor, orderId: string, input: {
endAt: Date; pricingPolicy: PricingPolicy; reason: string;
}) {
return this.renewForActor(actor, orderId, input, async (connection, order) => {
await this.assertOwner(connection, actor.tenantId, order.id, actor.userId);
});
}
async changeRoom(actor: OrderActor, orderId: string, input: {
roomId: string; reason: string;
}) {
@@ -124,6 +132,17 @@ export class OrderManagementRepository {
});
}
async customerChangeRoom(actor: OrderActor, orderId: string, input: {
roomId: string; reason: string;
}) {
return this.changeRoomForActor(actor, orderId, input, async (connection, order, target) => {
await this.assertOwner(connection, actor.tenantId, order.id, actor.userId);
if (target.storeId !== order.storeId) {
throw new OrderManagementError('ORDER_ROOM_CHANGE_STORE_INVALID');
}
});
}
async adjustTime(actor: OrderActor, orderId: string, input: {
startAt?: Date; endAt?: Date; reason: string;
}) {
@@ -220,6 +239,93 @@ export class OrderManagementRepository {
};
}
private async renewForActor(
actor: OrderActor, orderId: string, input: {
endAt: Date; pricingPolicy: PricingPolicy; reason: string;
},
authorize: (connection: PoolConnection, order: OrderRow) => Promise<void>
) {
return this.transaction(async (connection) => {
const order = await this.loadOrder(connection, actor.tenantId, orderId);
await authorize(connection, order);
const duplicate = await this.duplicate(connection, actor, orderId);
if (duplicate) return this.duplicateResult(orderId, duplicate);
if (!['PAID', 'RESERVED', 'IN_PROGRESS'].includes(order.status)) {
throw new OrderManagementError('ORDER_RENEW_STATUS_INVALID');
}
if (input.endAt <= order.endAt) throw new OrderManagementError('ORDER_RENEW_END_INVALID');
await this.lockRooms(connection, actor.tenantId, [order.roomId]);
await this.assertAvailable(
connection, actor.tenantId, order.roomId, order.endAt, input.endAt, orderId
);
const unitPrice = await this.resolveUnitPrice(
connection, actor.tenantId, order, input.pricingPolicy
);
const amountDeltaCents = Math.ceil(
(input.endAt.getTime() - order.endAt.getTime()) / 3600000
) * unitPrice;
await connection.execute(
`UPDATE qipai_orders
SET end_at = ?, total_amount_cents = total_amount_cents + ?,
adjustment_amount_cents = adjustment_amount_cents + ?
WHERE tenant_id = ? AND id = ?`,
[input.endAt, amountDeltaCents, amountDeltaCents, actor.tenantId, orderId]
);
await connection.execute(
`UPDATE qipai_room_reservations
SET ends_at = ?, expires_at = GREATEST(expires_at, ?)
WHERE tenant_id = ? AND order_id = ?`,
[input.endAt, input.endAt, actor.tenantId, orderId]
);
return this.record(connection, actor, order, 'RENEW', input.reason, {
endAt: order.endAt
}, { endAt: input.endAt, pricingPolicy: input.pricingPolicy }, amountDeltaCents);
});
}
private async changeRoomForActor(
actor: OrderActor, orderId: string, input: { roomId: string; reason: string },
authorize: (
connection: PoolConnection, order: OrderRow, target: RoomRow
) => Promise<void>
) {
return this.transaction(async (connection) => {
const order = await this.loadOrder(connection, actor.tenantId, orderId);
const duplicate = await this.duplicate(connection, actor, orderId);
if (duplicate) return this.duplicateResult(orderId, duplicate);
if (!['PENDING_PAYMENT', 'PAID', 'RESERVED', 'IN_PROGRESS'].includes(order.status)) {
throw new OrderManagementError('ORDER_ROOM_CHANGE_STATUS_INVALID');
}
if (input.roomId === order.roomId) throw new OrderManagementError('ORDER_ROOM_UNCHANGED');
await this.lockRooms(connection, actor.tenantId, [order.roomId, input.roomId]);
const target = await this.loadRoom(connection, actor.tenantId, input.roomId);
await authorize(connection, order, target);
await this.assertAvailable(
connection, actor.tenantId, target.id, order.startAt, order.endAt, orderId
);
const oldRoom = await this.loadRoom(connection, actor.tenantId, order.roomId);
const hours = Math.ceil((order.endAt.getTime() - order.startAt.getTime()) / 3600000);
const amountDeltaCents = hours * (target.basePriceCents - oldRoom.basePriceCents);
await connection.execute(
`UPDATE qipai_orders
SET store_id = ?, room_id = ?,
total_amount_cents = GREATEST(0, total_amount_cents + ?),
adjustment_amount_cents = adjustment_amount_cents + ?
WHERE tenant_id = ? AND id = ?`,
[target.storeId, target.id, amountDeltaCents, amountDeltaCents,
actor.tenantId, orderId]
);
await connection.execute(
`UPDATE qipai_room_reservations SET room_id = ?
WHERE tenant_id = ? AND order_id = ?`,
[target.id, actor.tenantId, orderId]
);
return this.record(connection, actor, order, 'CHANGE_ROOM', input.reason, {
storeId: order.storeId, roomId: order.roomId
}, { storeId: target.storeId, roomId: target.id }, amountDeltaCents);
});
}
private async loadOrder(connection: PoolConnection, tenantId: string, orderId: string) {
const [rows] = await connection.execute<OrderRow[]>(
`SELECT o.id, o.store_id AS storeId, o.room_id AS roomId, o.status,