import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; const backendRoot = dirname(dirname(fileURLToPath(import.meta.url))); const repoRoot = dirname(backendRoot); const read = (path) => readFileSync(join(repoRoot, path), 'utf8'); const upSql = read('database/migrations/2026061601_m01b_core_schema.up.sql'); const downSql = read('database/migrations/2026061601_m01b_core_schema.down.sql'); const verifySql = read('database/migrations/2026061601_m01b_core_schema.verify.sql'); const seedSql = read('database/seeds/2026061601_m01b_minimal_seed.sql'); const legacyFixtureSql = read('database/fixtures/2026061801_m01b_legacy_schema.sql'); const asyncUpSql = read('database/migrations/2026061802_m01c_async_tasks.up.sql'); const asyncDownSql = read('database/migrations/2026061802_m01c_async_tasks.down.sql'); const asyncVerifySql = read('database/migrations/2026061802_m01c_async_tasks.verify.sql'); const tenantAppsUpSql = read('database/migrations/2026061803_m02a_tenant_apps.up.sql'); const tenantAppsDownSql = read('database/migrations/2026061803_m02a_tenant_apps.down.sql'); const tenantAppsVerifySql = read('database/migrations/2026061803_m02a_tenant_apps.verify.sql'); const authUpSql = read('database/migrations/2026061804_m02b_wechat_auth.up.sql'); const authDownSql = read('database/migrations/2026061804_m02b_wechat_auth.down.sql'); const authVerifySql = read('database/migrations/2026061804_m02b_wechat_auth.verify.sql'); const rbacUpSql = read('database/migrations/2026061805_m02c_rbac.up.sql'); const rbacDownSql = read('database/migrations/2026061805_m02c_rbac.down.sql'); const rbacVerifySql = read('database/migrations/2026061805_m02c_rbac.verify.sql'); const userManagementUpSql = read('database/migrations/2026061806_m02d_user_management.up.sql'); const userManagementDownSql = read('database/migrations/2026061806_m02d_user_management.down.sql'); const userManagementVerifySql = read('database/migrations/2026061806_m02d_user_management.verify.sql'); const storeRoomUpSql = read('database/migrations/2026061807_m03a_store_room_domain.up.sql'); const storeRoomDownSql = read('database/migrations/2026061807_m03a_store_room_domain.down.sql'); const storeRoomVerifySql = read('database/migrations/2026061807_m03a_store_room_domain.verify.sql'); const contentUpSql = read('database/migrations/2026061808_m03b_decoration_ads_media.up.sql'); const contentDownSql = read('database/migrations/2026061808_m03b_decoration_ads_media.down.sql'); const contentVerifySql = read('database/migrations/2026061808_m03b_decoration_ads_media.verify.sql'); const discoveryUpSql = read('database/migrations/2026061809_m03c_store_discovery.up.sql'); const discoveryDownSql = read('database/migrations/2026061809_m03c_store_discovery.down.sql'); const discoveryVerifySql = read('database/migrations/2026061809_m03c_store_discovery.verify.sql'); const accessUpSql = read('database/migrations/2026061810_m03d_scene_wifi_access.up.sql'); const accessDownSql = read('database/migrations/2026061810_m03d_scene_wifi_access.down.sql'); const accessVerifySql = read('database/migrations/2026061810_m03d_scene_wifi_access.verify.sql'); const pricingUpSql = read('database/migrations/2026061811_m04a_pricing_reservations.up.sql'); const pricingDownSql = read('database/migrations/2026061811_m04a_pricing_reservations.down.sql'); const pricingVerifySql = read('database/migrations/2026061811_m04a_pricing_reservations.verify.sql'); const orderStateUpSql = read('database/migrations/2026062012_m04b_order_state_machine.up.sql'); const orderStateDownSql = read('database/migrations/2026062012_m04b_order_state_machine.down.sql'); const orderStateVerifySql = read('database/migrations/2026062012_m04b_order_state_machine.verify.sql'); const adjustmentUpSql = read('database/migrations/2026062013_m04c_order_adjustments.up.sql'); const adjustmentDownSql = read('database/migrations/2026062013_m04c_order_adjustments.down.sql'); const adjustmentVerifySql = read('database/migrations/2026062013_m04c_order_adjustments.verify.sql'); const shareUpSql = read('database/migrations/2026062014_m04d_order_shares.up.sql'); const shareDownSql = read('database/migrations/2026062014_m04d_order_shares.down.sql'); const shareVerifySql = read('database/migrations/2026062014_m04d_order_shares.verify.sql'); const paymentUpSql = read('database/migrations/2026062015_m05a_payment_domain.up.sql'); const paymentDownSql = read('database/migrations/2026062015_m05a_payment_domain.down.sql'); const paymentVerifySql = read('database/migrations/2026062015_m05a_payment_domain.verify.sql'); const wechatRefundUpSql = read('database/migrations/2026062216_m05b_wechat_refunds.up.sql'); const wechatRefundDownSql = read('database/migrations/2026062216_m05b_wechat_refunds.down.sql'); const wechatRefundVerifySql = read('database/migrations/2026062216_m05b_wechat_refunds.verify.sql'); const thirdPartyUpSql = read('database/migrations/2026062217_m05c_third_party.up.sql'); const thirdPartyDownSql = read('database/migrations/2026062217_m05c_third_party.down.sql'); const thirdPartyVerifySql = read('database/migrations/2026062217_m05c_third_party.verify.sql'); const profitSharingUpSql = read('database/migrations/2026062218_m05d_profit_sharing.up.sql'); const profitSharingDownSql = read('database/migrations/2026062218_m05d_profit_sharing.down.sql'); const profitSharingVerifySql = read('database/migrations/2026062218_m05d_profit_sharing.verify.sql'); const coreTables = [ 'qipai_schema_migrations', 'qipai_tenants', 'qipai_stores', 'qipai_rooms', 'qipai_members', 'qipai_orders', 'qipai_payments', 'qipai_devices', 'qipai_audit_logs', 'qipai_legacy_table_mappings' ]; for (const table of coreTables) { assert.match(upSql, new RegExp(`CREATE TABLE IF NOT EXISTS ${table}\\b`)); assert.match(downSql, new RegExp(`DROP TABLE IF EXISTS ${table}\\b`)); assert.match(verifySql, new RegExp(`'${table}'`)); } for (const table of [ 'qipai_stores', 'qipai_rooms', 'qipai_members', 'qipai_orders', 'qipai_payments', 'qipai_devices', 'qipai_audit_logs' ]) { const block = upSql.slice(upSql.indexOf(`CREATE TABLE IF NOT EXISTS ${table}`)); assert.match(block, /\btenant_id BIGINT UNSIGNED NOT NULL\b/); } for (const amountColumn of [ 'base_price_cents', 'balance_cents', 'total_amount_cents', 'paid_amount_cents', 'amount_cents' ]) { assert.match(upSql, new RegExp(`\\b${amountColumn}\\b`)); } assert.doesNotMatch(upSql, /\bDECIMAL\b/i); assert.match(upSql, /DATETIME\(3\)/); assert.match(upSql, /legacy_store_id/); assert.match(upSql, /legacy_order_id/); assert.match(seedSql, /INSERT IGNORE INTO qipai_legacy_table_mappings/); assert.match(seedSql, /member_order_info/); for (const table of [ 'member_store_info', 'member_room_info', 'member_order_info', 'member_device_info' ]) { assert.match(legacyFixtureSql, new RegExp(`CREATE TABLE ${table}`)); } assert.match(legacyFixtureSql, /DECIMAL\(10,\s*2\)/); assert.match(legacyFixtureSql, /LEGACY-SANITIZED-001/); assert.doesNotMatch(legacyFixtureSql, /(?:https?:\/\/|@|password|secret|token)/i); for (const table of ['qipai_outbox_events', 'qipai_async_tasks']) { assert.match(asyncUpSql, new RegExp(`CREATE TABLE IF NOT EXISTS ${table}\\b`)); assert.match(asyncDownSql, new RegExp(`DROP TABLE IF EXISTS ${table}\\b`)); assert.match(asyncVerifySql, new RegExp(`'${table}'`)); } assert.match(asyncUpSql, /UNIQUE KEY uq_qipai_outbox_events_idempotency/); assert.match(asyncUpSql, /UNIQUE KEY uq_qipai_async_tasks_idempotency/); assert.match(asyncUpSql, /lease_expires_at DATETIME\(3\)/); assert.match(asyncUpSql, /COMPENSATION_REQUIRED|status VARCHAR/); for (const table of ['qipai_platform_apps', 'qipai_tenant_apps', 'qipai_tenant_configs']) { assert.match(tenantAppsUpSql, new RegExp(`CREATE TABLE IF NOT EXISTS ${table}\\b`)); assert.match(tenantAppsDownSql, new RegExp(`DROP TABLE IF EXISTS ${table}\\b`)); assert.match(tenantAppsVerifySql, new RegExp(`'${table}'`)); } assert.match(tenantAppsUpSql, /UNIQUE KEY uq_qipai_tenant_apps_tenant_app \(tenant_id, platform_app_id\)/); assert.match(tenantAppsUpSql, /UNIQUE KEY uq_qipai_tenant_configs_tenant_app \(tenant_id, platform_app_id\)/); assert.match(tenantAppsUpSql, /brand_name VARCHAR/); assert.match(tenantAppsUpSql, /theme_color VARCHAR/); for (const table of ['qipai_users', 'qipai_user_identities', 'qipai_auth_sessions']) { assert.match(authUpSql, new RegExp(`CREATE TABLE IF NOT EXISTS ${table}\\b`)); assert.match(authDownSql, new RegExp(`DROP TABLE IF EXISTS ${table}\\b`)); assert.match(authVerifySql, new RegExp(`'${table}'`)); } assert.match(authUpSql, /role_version INT UNSIGNED/); assert.match(authUpSql, /UNIQUE KEY uq_qipai_user_identities_tenant_app_openid/); assert.match(authUpSql, /revoked_at DATETIME\(3\)/); assert.match(authUpSql, /expires_at DATETIME\(3\)/); for (const table of [ 'qipai_permissions', 'qipai_roles', 'qipai_role_permissions', 'qipai_user_roles', 'qipai_user_store_scopes' ]) { assert.match(rbacUpSql, new RegExp(`CREATE TABLE IF NOT EXISTS ${table}\\b`)); assert.match(rbacDownSql, new RegExp(`DROP TABLE IF EXISTS ${table}\\b`)); assert.match(rbacVerifySql, new RegExp(`'${table}'`)); } assert.match(rbacUpSql, /PRIMARY KEY \(tenant_id, user_id, store_id, scope_type\)/); assert.match(userManagementUpSql, /CREATE TABLE IF NOT EXISTS qipai_user_admin_profiles/); assert.match(userManagementDownSql, /DROP TABLE IF EXISTS qipai_user_admin_profiles/); assert.match(userManagementVerifySql, /'qipai_user_admin_profiles'/); for (const permission of ['user.read', 'staff.manage', 'session.reset']) { assert.match(userManagementUpSql, new RegExp(permission.replace('.', '\\.'))); } for (const table of [ 'qipai_store_business_hours', 'qipai_room_categories', 'qipai_room_disabled_periods' ]) { assert.match(storeRoomUpSql, new RegExp(`CREATE TABLE IF NOT EXISTS ${table}`)); assert.match(storeRoomDownSql, new RegExp(`DROP TABLE IF EXISTS ${table}`)); assert.match(storeRoomVerifySql, new RegExp(`'${table}'`)); } assert.match(storeRoomUpSql, /configuration_status VARCHAR/); assert.match(storeRoomUpSql, /operational_status VARCHAR/); assert.match(storeRoomUpSql, /weekday_price_cents INT UNSIGNED/); assert.match(storeRoomUpSql, /CHECK \(ends_at > starts_at\)/); for (const table of [ 'qipai_media_assets', 'qipai_store_decorations', 'qipai_advertisements' ]) { assert.match(contentUpSql, new RegExp(`CREATE TABLE IF NOT EXISTS ${table}`)); assert.match(contentDownSql, new RegExp(`DROP TABLE IF EXISTS ${table}`)); assert.match(contentVerifySql, new RegExp(`'${table}'`)); } assert.match(contentUpSql, /schema_version INT UNSIGNED/); assert.match(contentUpSql, /scope_type VARCHAR/); assert.match(contentUpSql, /checksum_sha256 CHAR\(64\)/); for (const column of ['city', 'district']) { assert.match(discoveryUpSql, new RegExp(`ADD COLUMN ${column}`)); assert.match(discoveryDownSql, new RegExp(`DROP COLUMN ${column}`)); assert.match(discoveryVerifySql, new RegExp(`'${column}'`)); } assert.match(discoveryUpSql, /idx_qipai_stores_tenant_city_status/); assert.match(discoveryUpSql, /idx_qipai_stores_tenant_coordinates/); for (const table of [ 'qipai_scene_codes', 'qipai_scene_scan_events', 'qipai_order_user_access' ]) { assert.match(accessUpSql, new RegExp(`CREATE TABLE IF NOT EXISTS ${table}`)); assert.match(accessDownSql, new RegExp(`DROP TABLE IF EXISTS ${table}`)); assert.match(accessVerifySql, new RegExp(`'${table}'`)); } assert.match(accessUpSql, /scan_count BIGINT UNSIGNED/); assert.match(accessUpSql, /target_type = 'STORE'/); assert.match(accessUpSql, /PRIMARY KEY \(tenant_id, order_id, user_id\)/); for (const table of [ 'qipai_holiday_calendar', 'qipai_order_price_snapshots', 'qipai_room_reservations' ]) { assert.match(pricingUpSql, new RegExp(`CREATE TABLE IF NOT EXISTS ${table}`)); assert.match(pricingDownSql, new RegExp(`DROP TABLE IF EXISTS ${table}`)); assert.match(pricingVerifySql, new RegExp(`'${table}'`)); } assert.match(pricingUpSql, /full_day_price_cents/); assert.match(pricingUpSql, /minimum_spend_cents/); assert.match(pricingUpSql, /idx_qipai_reservation_overlap/); assert.match(orderStateUpSql, /CREATE TABLE IF NOT EXISTS qipai_order_status_history/); assert.match(orderStateDownSql, /DROP TABLE IF EXISTS qipai_order_status_history/); assert.match(orderStateVerifySql, /'qipai_order_status_history'/); assert.match(orderStateUpSql, /status_version INT UNSIGNED/); assert.match(orderStateUpSql, /UNIQUE KEY uq_qipai_order_history_trace/); assert.match(orderStateUpSql, /actor_type VARCHAR/); assert.match(orderStateUpSql, /source VARCHAR/); assert.match(orderStateUpSql, /trace_id VARCHAR/); assert.match(adjustmentUpSql, /CREATE TABLE IF NOT EXISTS qipai_order_adjustments/); assert.match(adjustmentDownSql, /DROP TABLE IF EXISTS qipai_order_adjustments/); assert.match(adjustmentVerifySql, /'qipai_order_adjustments'/); assert.match(adjustmentUpSql, /cancellation_cutoff_minutes/); assert.match(adjustmentUpSql, /amount_delta_cents INT/); assert.match(adjustmentUpSql, /UNIQUE KEY uq_qipai_order_adjustment_trace/); assert.match(shareUpSql, /CREATE TABLE IF NOT EXISTS qipai_order_shares/); assert.match(shareDownSql, /DROP TABLE IF EXISTS qipai_order_shares/); assert.match(shareVerifySql, /'qipai_order_shares'/); assert.match(shareUpSql, /token_hash CHAR\(64\)/); assert.match(shareUpSql, /allow_open_door TINYINT/); assert.match(shareUpSql, /allow_renew TINYINT/); assert.match(shareUpSql, /UNIQUE KEY uq_qipai_order_share_token_hash/); for (const table of [ 'qipai_payment_attempts', 'qipai_payment_callbacks', 'qipai_refunds', 'qipai_profit_shares', 'qipai_payment_configs' ]) { assert.match(paymentUpSql, new RegExp(`CREATE TABLE IF NOT EXISTS ${table}`)); assert.match(paymentDownSql, new RegExp(`DROP TABLE IF EXISTS ${table}`)); assert.match(paymentVerifySql, new RegExp(`'${table}'`)); } assert.match(paymentUpSql, /client_request_id VARCHAR/); assert.match(paymentUpSql, /UNIQUE KEY uq_qipai_payment_callback_provider/); assert.match(paymentUpSql, /credential_ref VARCHAR/); assert.match(paymentUpSql, /scope_key VARCHAR/); assert.doesNotMatch(paymentUpSql, /credential_secret|private_key|api_secret/i); assert.match(wechatRefundUpSql, /CREATE TABLE IF NOT EXISTS qipai_reconciliation_runs/); assert.match(wechatRefundDownSql, /DROP TABLE IF EXISTS qipai_reconciliation_runs/); assert.match(wechatRefundVerifySql, /'qipai_reconciliation_runs'/); assert.match(wechatRefundUpSql, /UNIQUE KEY uq_qipai_refund_client_request/); assert.match(wechatRefundUpSql, /UNIQUE KEY uq_qipai_refund_callback/); assert.match(wechatRefundUpSql, /UNIQUE KEY uq_qipai_reconciliation_request/); assert.doesNotMatch(wechatRefundUpSql, /private_key|api_v3_key|certificate_pem/i); for (const table of [ 'qipai_third_party_configs', 'qipai_third_party_mappings', 'qipai_group_vouchers', 'qipai_group_redemptions', 'qipai_direct_bookings' ]) { assert.match(thirdPartyUpSql, new RegExp(`CREATE TABLE IF NOT EXISTS ${table}`)); assert.match(thirdPartyDownSql, new RegExp(`DROP TABLE IF EXISTS ${table}`)); assert.match(thirdPartyVerifySql, new RegExp(`'${table}'`)); } assert.match(thirdPartyUpSql, /voucher_hash CHAR\(64\)/); assert.match(thirdPartyUpSql, /UNIQUE KEY uq_qipai_group_redemption_voucher/); assert.match(thirdPartyUpSql, /UNIQUE KEY uq_qipai_direct_booking_event/); assert.doesNotMatch(thirdPartyUpSql, /voucher_code|api_token|webhook_secret/i); for (const table of [ 'qipai_collection_accounts', 'qipai_profit_share_receivers', 'qipai_profit_share_policies' ]) { assert.match(profitSharingUpSql, new RegExp(`CREATE TABLE IF NOT EXISTS ${table}`)); assert.match(profitSharingDownSql, new RegExp(`DROP TABLE IF EXISTS ${table}`)); assert.match(profitSharingVerifySql, new RegExp(`'${table}'`)); } assert.match(profitSharingUpSql, /receiver_hash CHAR\(64\)/); assert.match(profitSharingUpSql, /percentage_bps SMALLINT UNSIGNED/); assert.match(profitSharingUpSql, /uq_qipai_profit_share_payment_receiver/); assert.doesNotMatch( profitSharingUpSql, /\bprivate_key\b|\bapi_v3_key\b|\breceiver_account\b/i ); console.log('PASS: M01-B through M05-D migration contracts are present.');