feat(M04-A): 完成定价快照与并发时段预占
This commit is contained in:
@@ -17,6 +17,7 @@ import { StoreRoomRepository, StoreRoomError } from '../dist/stores/store-room-r
|
||||
import { ContentRepository, ContentError } from '../dist/content/content-repository.js';
|
||||
import { StoreDiscoveryRepository } from '../dist/stores/store-discovery-repository.js';
|
||||
import { StoreAccessRepository, StoreAccessError } from '../dist/stores/access-repository.js';
|
||||
import { PricingRepository, PricingError } from '../dist/orders/pricing-repository.js';
|
||||
import {
|
||||
executeMigrationPlan,
|
||||
loadMigrationPlan,
|
||||
@@ -29,9 +30,11 @@ const expectedTables = [
|
||||
'qipai_audit_logs',
|
||||
'qipai_auth_sessions',
|
||||
'qipai_devices',
|
||||
'qipai_holiday_calendar',
|
||||
'qipai_legacy_table_mappings',
|
||||
'qipai_media_assets',
|
||||
'qipai_members',
|
||||
'qipai_order_price_snapshots',
|
||||
'qipai_order_user_access',
|
||||
'qipai_orders',
|
||||
'qipai_outbox_events',
|
||||
@@ -42,6 +45,7 @@ const expectedTables = [
|
||||
'qipai_roles',
|
||||
'qipai_room_categories',
|
||||
'qipai_room_disabled_periods',
|
||||
'qipai_room_reservations',
|
||||
'qipai_rooms',
|
||||
'qipai_scene_codes',
|
||||
'qipai_scene_scan_events',
|
||||
@@ -77,11 +81,11 @@ async function readMigrationVersions(pool) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT version, name
|
||||
FROM qipai_schema_migrations
|
||||
WHERE version IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
WHERE version IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ORDER BY version`,
|
||||
['2026061601', '2026061802', '2026061803', '2026061804',
|
||||
'2026061805', '2026061806', '2026061807', '2026061808', '2026061809',
|
||||
'2026061810']
|
||||
'2026061810', '2026061811']
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
@@ -527,6 +531,102 @@ async function assertSceneAndWifiAccess(pool, context) {
|
||||
assert.doesNotMatch(auditRows[0].metadata, /sanitized-password/);
|
||||
}
|
||||
|
||||
async function assertPricingAndReservations(pool, context) {
|
||||
const [customerRows] = await pool.query(
|
||||
`SELECT u.id FROM qipai_users u
|
||||
INNER JOIN qipai_user_identities i
|
||||
ON i.tenant_id = u.tenant_id AND i.user_id = u.id
|
||||
WHERE u.tenant_id = ? AND i.openid = 'm02b-openid-a' LIMIT 1`,
|
||||
[context.tenantId]
|
||||
);
|
||||
const [targetRows] = await pool.query(
|
||||
`SELECT s.id AS storeId, r.id AS roomId
|
||||
FROM qipai_stores s
|
||||
INNER JOIN qipai_rooms r ON r.tenant_id = s.tenant_id AND r.store_id = s.id
|
||||
WHERE s.tenant_id = ? AND s.name = 'M03A Store' LIMIT 1`,
|
||||
[context.tenantId]
|
||||
);
|
||||
const customerId = String(customerRows[0].id);
|
||||
const roomId = String(targetRows[0].roomId);
|
||||
await pool.query(
|
||||
`UPDATE qipai_rooms SET base_price_cents = 1200, weekday_price_cents = 1000,
|
||||
holiday_price_cents = 1800, overnight_price_cents = 5000,
|
||||
full_day_price_cents = 9000, minimum_spend_cents = 2500,
|
||||
deposit_cents = 500, minimum_minutes = 60, max_advance_days = 30,
|
||||
configuration_status = 'ENABLED', operational_status = 'AVAILABLE'
|
||||
WHERE tenant_id = ? AND id = ?`,
|
||||
[context.tenantId, roomId]
|
||||
);
|
||||
const startAt = new Date(Date.now() + 5 * 86400000);
|
||||
startAt.setUTCHours(2, 0, 0, 0);
|
||||
const endAt = new Date(startAt.getTime() + 2 * 3600000);
|
||||
const holidayDate = new Intl.DateTimeFormat('en-CA', {
|
||||
timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit'
|
||||
}).format(startAt);
|
||||
await pool.query(
|
||||
`INSERT INTO qipai_holiday_calendar (tenant_id, holiday_date, name)
|
||||
VALUES (?, ?, 'M04-A Test Holiday')`,
|
||||
[context.tenantId, holidayDate]
|
||||
);
|
||||
const repository = new PricingRepository(pool);
|
||||
const quote = await repository.quote({
|
||||
tenantId: context.tenantId,
|
||||
roomId,
|
||||
startAt,
|
||||
endAt,
|
||||
pricingMode: 'HOURLY',
|
||||
adjustment: { discountCents: 300, packageCreditCents: 200 }
|
||||
});
|
||||
assert.equal(quote.rules.priceSource, 'holiday');
|
||||
assert.equal(quote.subtotalCents, 3600);
|
||||
assert.equal(quote.totalCents, 3600);
|
||||
|
||||
const attempts = await Promise.allSettled([
|
||||
repository.reserve({
|
||||
tenantId: context.tenantId, userId: customerId, roomId,
|
||||
startAt, endAt, pricingMode: 'HOURLY'
|
||||
}),
|
||||
repository.reserve({
|
||||
tenantId: context.tenantId, userId: customerId, roomId,
|
||||
startAt, endAt, pricingMode: 'HOURLY'
|
||||
})
|
||||
]);
|
||||
assert.equal(attempts.filter((item) => item.status === 'fulfilled').length, 1);
|
||||
assert.equal(attempts.filter((item) =>
|
||||
item.status === 'rejected'
|
||||
&& item.reason instanceof PricingError
|
||||
&& item.reason.code === 'TIME_SLOT_CONFLICT'
|
||||
).length, 1);
|
||||
const first = attempts.find((item) => item.status === 'fulfilled').value;
|
||||
const [snapshotBefore] = await pool.query(
|
||||
`SELECT total_cents AS totalCents, rules
|
||||
FROM qipai_order_price_snapshots WHERE tenant_id = ? AND order_id = ?`,
|
||||
[context.tenantId, first.orderId]
|
||||
);
|
||||
await pool.query(
|
||||
`UPDATE qipai_rooms SET holiday_price_cents = 9900
|
||||
WHERE tenant_id = ? AND id = ?`,
|
||||
[context.tenantId, roomId]
|
||||
);
|
||||
const [snapshotAfter] = await pool.query(
|
||||
`SELECT total_cents AS totalCents, rules
|
||||
FROM qipai_order_price_snapshots WHERE tenant_id = ? AND order_id = ?`,
|
||||
[context.tenantId, first.orderId]
|
||||
);
|
||||
assert.deepEqual(snapshotAfter, snapshotBefore);
|
||||
await pool.query(
|
||||
`UPDATE qipai_room_reservations SET expires_at = DATE_SUB(UTC_TIMESTAMP(3), INTERVAL 1 SECOND)
|
||||
WHERE tenant_id = ? AND order_id = ?`,
|
||||
[context.tenantId, first.orderId]
|
||||
);
|
||||
assert.equal((await repository.releaseExpired(context.tenantId, roomId)).released, 1);
|
||||
const replacement = await repository.reserve({
|
||||
tenantId: context.tenantId, userId: customerId, roomId,
|
||||
startAt, endAt, pricingMode: 'FULL_DAY'
|
||||
});
|
||||
assert.equal(replacement.quote.unitPriceCents, 9000);
|
||||
}
|
||||
|
||||
async function assertContentManagement(pool, context) {
|
||||
const [adminRows] = await pool.query(
|
||||
`SELECT u.id FROM qipai_users u
|
||||
@@ -624,7 +724,8 @@ try {
|
||||
{ version: '2026061807', name: 'm03a_store_room_domain' },
|
||||
{ version: '2026061808', name: 'm03b_decoration_ads_media' },
|
||||
{ version: '2026061809', name: 'm03c_store_discovery' },
|
||||
{ version: '2026061810', name: 'm03d_scene_wifi_access' }
|
||||
{ version: '2026061810', name: 'm03d_scene_wifi_access' },
|
||||
{ version: '2026061811', name: 'm04a_pricing_reservations' }
|
||||
]);
|
||||
await assertTaskDurability(pool);
|
||||
const loginContext = await assertPlatformTenantIsolation(pool);
|
||||
@@ -634,13 +735,14 @@ try {
|
||||
await assertContentManagement(pool, loginContext);
|
||||
await assertStoreDiscovery(pool, loginContext);
|
||||
await assertSceneAndWifiAccess(pool, loginContext);
|
||||
await assertPricingAndReservations(pool, loginContext);
|
||||
await assertLegacyCompatibility(pool);
|
||||
console.log('PASS: first up, verify, tenant isolation and revocable auth checks completed.');
|
||||
|
||||
await executeMigrationPlan(pool, plans.down);
|
||||
assert.deepEqual(await readCoreTables(pool), []);
|
||||
await assertLegacyCompatibility(pool);
|
||||
console.log('PASS: down removed all M01-B/M01-C tables.');
|
||||
console.log('PASS: down removed all M01-B through M04-A tables.');
|
||||
|
||||
await executeMigrationPlan(pool, plans.up);
|
||||
await executeMigrationPlan(pool, plans.verify);
|
||||
@@ -655,7 +757,8 @@ try {
|
||||
{ version: '2026061807', name: 'm03a_store_room_domain' },
|
||||
{ version: '2026061808', name: 'm03b_decoration_ads_media' },
|
||||
{ version: '2026061809', name: 'm03c_store_discovery' },
|
||||
{ version: '2026061810', name: 'm03d_scene_wifi_access' }
|
||||
{ version: '2026061810', name: 'm03d_scene_wifi_access' },
|
||||
{ version: '2026061811', name: 'm04a_pricing_reservations' }
|
||||
]);
|
||||
await assertLegacyCompatibility(pool);
|
||||
console.log('PASS: second up and verify restored the schema.');
|
||||
@@ -709,7 +812,11 @@ try {
|
||||
'scene scan statistics',
|
||||
'Wi-Fi denied without active order',
|
||||
'Wi-Fi allowed by active order grant',
|
||||
'Wi-Fi audit excludes password'
|
||||
'Wi-Fi audit excludes password',
|
||||
'holiday and minimum-spend pricing',
|
||||
'immutable order price snapshot',
|
||||
'concurrent room hold conflict',
|
||||
'expired hold release'
|
||||
]
|
||||
}, null, 2));
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user