Files
qipai/backend/tests/migration-contract.test.mjs
T

454 lines
27 KiB
JavaScript

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 deviceTopologyUpSql = read('database/migrations/2026062219_m06b_device_topology.up.sql');
const deviceTopologyDownSql = read('database/migrations/2026062219_m06b_device_topology.down.sql');
const deviceTopologyVerifySql = read('database/migrations/2026062219_m06b_device_topology.verify.sql');
const iotMessagesUpSql = read('database/migrations/2026062220_m06c_iot_messages.up.sql');
const iotMessagesDownSql = read('database/migrations/2026062220_m06c_iot_messages.down.sql');
const iotMessagesVerifySql = read('database/migrations/2026062220_m06c_iot_messages.verify.sql');
const walletUpSql = read('database/migrations/2026062421_m07a_wallet_ledger.up.sql');
const walletDownSql = read('database/migrations/2026062421_m07a_wallet_ledger.down.sql');
const walletVerifySql = read('database/migrations/2026062421_m07a_wallet_ledger.verify.sql');
const rechargeUpSql = read('database/migrations/2026062422_m07b_recharge_plans.up.sql');
const rechargeDownSql = read('database/migrations/2026062422_m07b_recharge_plans.down.sql');
const rechargeVerifySql = read('database/migrations/2026062422_m07b_recharge_plans.verify.sql');
const benefitUpSql = read('database/migrations/2026062423_m07c_benefits.up.sql');
const benefitDownSql = read('database/migrations/2026062423_m07c_benefits.down.sql');
const benefitVerifySql = read('database/migrations/2026062423_m07c_benefits.verify.sql');
const rechargeWechatUpSql = read('database/migrations/2026062524_m08a_recharge_wechat.up.sql');
const rechargeWechatDownSql = read('database/migrations/2026062524_m08a_recharge_wechat.down.sql');
const rechargeWechatVerifySql = read('database/migrations/2026062524_m08a_recharge_wechat.verify.sql');
const cleaningUpSql = read('database/migrations/2026062525_m08b_cleaner_tasks.up.sql');
const cleaningDownSql = read('database/migrations/2026062525_m08b_cleaner_tasks.down.sql');
const cleaningVerifySql = read('database/migrations/2026062525_m08b_cleaner_tasks.verify.sql');
const cleaningSettlementUpSql = read('database/migrations/2026062626_m08b_cleaning_settlements.up.sql');
const cleaningSettlementDownSql = read('database/migrations/2026062626_m08b_cleaning_settlements.down.sql');
const cleaningSettlementVerifySql = read('database/migrations/2026062626_m08b_cleaning_settlements.verify.sql');
const cleaningCollaborationUpSql = read('database/migrations/2026062627_m08b_cleaning_collaboration.up.sql');
const cleaningCollaborationDownSql = read('database/migrations/2026062627_m08b_cleaning_collaboration.down.sql');
const cleaningCollaborationVerifySql = read('database/migrations/2026062627_m08b_cleaning_collaboration.verify.sql');
const cleaningPayoutUpSql = read('database/migrations/2026062728_m08b_cleaning_payouts.up.sql');
const cleaningPayoutDownSql = read('database/migrations/2026062728_m08b_cleaning_payouts.down.sql');
const cleaningPayoutVerifySql = read('database/migrations/2026062728_m08b_cleaning_payouts.verify.sql');
const cleaningTransferStateUpSql = read('database/migrations/2026062729_m08b_cleaning_transfer_state.up.sql');
const cleaningTransferStateDownSql = read('database/migrations/2026062729_m08b_cleaning_transfer_state.down.sql');
const cleaningTransferStateVerifySql = read('database/migrations/2026062729_m08b_cleaning_transfer_state.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
);
for (const table of [
'qipai_device_alerts', 'qipai_device_channels', 'qipai_device_links',
'qipai_device_status_snapshots', 'qipai_device_maintenance_records'
]) {
assert.match(deviceTopologyUpSql, new RegExp(`CREATE TABLE IF NOT EXISTS ${table}`));
assert.match(deviceTopologyDownSql, new RegExp(`DROP TABLE IF EXISTS ${table}`));
assert.match(deviceTopologyVerifySql, new RegExp(`'${table}'`));
}
assert.match(deviceTopologyUpSql, /uq_qipai_device_channel_target/);
assert.match(deviceTopologyUpSql, /uq_qipai_device_link_child/);
assert.match(deviceTopologyUpSql, /uq_qipai_device_link_room_type/);
assert.match(deviceTopologyUpSql, /device\.read/);
assert.match(deviceTopologyUpSql, /device\.write/);
for (const table of [
'qipai_iot_commands', 'qipai_iot_device_events', 'qipai_iot_dead_letters'
]) {
assert.match(iotMessagesUpSql, new RegExp(`CREATE TABLE IF NOT EXISTS ${table}`));
assert.match(iotMessagesDownSql, new RegExp(`DROP TABLE IF EXISTS ${table}`));
assert.match(iotMessagesVerifySql, new RegExp(`'${table}'`));
}
assert.match(iotMessagesUpSql, /command_id VARCHAR\(13\)/);
assert.match(iotMessagesUpSql, /uq_qipai_iot_event_dedup/);
assert.match(iotMessagesUpSql, /receive_count INT UNSIGNED/);
assert.match(iotMessagesUpSql, /PENDING/);
for (const table of [
'qipai_wallet_scope_policies', 'qipai_wallet_accounts', 'qipai_wallet_ledger_entries'
]) {
assert.match(walletUpSql, new RegExp(`CREATE TABLE IF NOT EXISTS ${table}`));
assert.match(walletDownSql, new RegExp(`DROP TABLE IF EXISTS ${table}`));
assert.match(walletVerifySql, new RegExp(`'${table}'`));
}
assert.match(walletUpSql, /cash_balance_cents INT NOT NULL DEFAULT 0/);
assert.match(walletUpSql, /gift_balance_cents INT NOT NULL DEFAULT 0/);
assert.match(walletUpSql, /uq_qipai_wallet_ledger_business/);
assert.match(walletUpSql, /cash_delta_cents INT NOT NULL DEFAULT 0/);
assert.match(walletUpSql, /gift_delta_cents INT NOT NULL DEFAULT 0/);
for (const table of ['qipai_recharge_plans', 'qipai_recharge_orders']) {
assert.match(rechargeUpSql, new RegExp(`CREATE TABLE IF NOT EXISTS ${table}`));
assert.match(rechargeDownSql, new RegExp(`DROP TABLE IF EXISTS ${table}`));
assert.match(rechargeVerifySql, new RegExp(`'${table}'`));
}
assert.match(rechargeUpSql, /pay_amount_cents INT UNSIGNED NOT NULL/);
assert.match(rechargeUpSql, /gift_amount_cents INT UNSIGNED NOT NULL DEFAULT 0/);
assert.match(rechargeUpSql, /purchase_limit_per_user INT UNSIGNED/);
assert.match(rechargeUpSql, /UNIQUE KEY uq_qipai_recharge_order_request/);
assert.match(rechargeUpSql, /payment_id BIGINT UNSIGNED NULL/);
assert.match(rechargeUpSql, /credited_at DATETIME\(3\) NULL/);
for (const table of [
'qipai_coupon_templates', 'qipai_coupon_grants', 'qipai_package_plans',
'qipai_package_holdings', 'qipai_benefit_usages'
]) {
assert.match(benefitUpSql, new RegExp(`CREATE TABLE IF NOT EXISTS ${table}`));
assert.match(benefitDownSql, new RegExp(`DROP TABLE IF EXISTS ${table}`));
assert.match(benefitVerifySql, new RegExp(`'${table}'`));
}
assert.match(benefitUpSql, /coupon_type VARCHAR/);
assert.match(benefitUpSql, /weekdays_json JSON/);
assert.match(benefitUpSql, /remaining_minutes INT UNSIGNED/);
assert.match(benefitUpSql, /remaining_amount_cents INT UNSIGNED/);
assert.match(benefitUpSql, /UNIQUE KEY uq_qipai_benefit_usage_request/);
assert.match(benefitUpSql, /UNIQUE KEY uq_qipai_benefit_usage_order_benefit/);
assert.match(rechargeWechatUpSql, /ALTER TABLE qipai_recharge_orders/);
assert.match(rechargeWechatUpSql, /provider_prepay_id VARCHAR\(128\) NULL/);
assert.match(rechargeWechatUpSql, /provider_payment_id VARCHAR\(128\) NULL/);
assert.match(rechargeWechatUpSql, /provider_callback_id VARCHAR\(128\) NULL/);
assert.match(rechargeWechatUpSql, /raw_notify JSON NULL/);
assert.match(rechargeWechatUpSql, /UNIQUE KEY uq_qipai_recharge_provider_payment/);
assert.match(rechargeWechatUpSql, /UNIQUE KEY uq_qipai_recharge_provider_callback/);
assert.match(rechargeWechatDownSql, /DROP COLUMN provider_payment_id/);
assert.match(rechargeWechatVerifySql, /'provider_payment_id'/);
assert.match(rechargeWechatVerifySql, /'uq_qipai_recharge_provider_callback'/);
for (const table of ['qipai_cleaning_tasks', 'qipai_cleaning_task_events']) {
assert.match(cleaningUpSql, new RegExp(`CREATE TABLE IF NOT EXISTS ${table}`));
assert.match(cleaningDownSql, new RegExp(`DROP TABLE IF EXISTS ${table}`));
assert.match(cleaningVerifySql, new RegExp(`'${table}'`));
}
for (const status of [
'WAITING', 'CLAIMED', 'STARTED', 'SUBMITTED', 'COMPLETED',
'REJECTED', 'EXEMPT', 'SETTLED', 'CANCELLED'
]) {
assert.match(cleaningUpSql, new RegExp(status));
}
assert.match(cleaningUpSql, /cleaner_user_id BIGINT UNSIGNED NULL/);
assert.match(cleaningUpSql, /photo_urls_json JSON NOT NULL/);
assert.match(cleaningUpSql, /uq_qipai_cleaning_task_order/);
assert.match(cleaningUpSql, /cleaning\.task\.write/);
assert.match(cleaningUpSql, /cleaning\.statistics\.read/);
for (const table of ['qipai_cleaning_settlements', 'qipai_cleaning_settlement_items']) {
assert.match(cleaningSettlementUpSql, new RegExp(`CREATE TABLE IF NOT EXISTS ${table}`));
assert.match(cleaningSettlementDownSql, new RegExp(`DROP TABLE IF EXISTS ${table}`));
assert.match(cleaningSettlementVerifySql, new RegExp(`'${table}'`));
}
assert.match(cleaningSettlementUpSql, /settlement_no VARCHAR\(64\) NOT NULL/);
assert.match(cleaningSettlementUpSql, /total_reward_cents INT UNSIGNED NOT NULL/);
assert.match(cleaningSettlementUpSql, /uq_qipai_cleaning_settlement_item_task/);
assert.match(cleaningSettlementUpSql, /cleaning\.settlement\.read/);
assert.match(cleaningSettlementUpSql, /cleaning\.settlement\.write/);
assert.match(cleaningCollaborationUpSql, /CREATE TABLE IF NOT EXISTS qipai_cleaning_task_members/);
assert.match(cleaningCollaborationUpSql, /member_role VARCHAR\(16\) NOT NULL DEFAULT 'ASSIST'/);
assert.match(cleaningCollaborationUpSql, /settled_at DATETIME\(3\) NULL/);
assert.match(cleaningCollaborationUpSql, /DROP INDEX uq_qipai_cleaning_settlement_item_task/);
assert.match(cleaningCollaborationUpSql, /cleaner_user_id BIGINT UNSIGNED NOT NULL/);
assert.match(cleaningCollaborationUpSql, /uq_qipai_cleaning_settlement_item_task_user/);
assert.match(cleaningCollaborationDownSql, /DROP TABLE IF EXISTS qipai_cleaning_task_members/);
assert.match(cleaningCollaborationDownSql, /ADD UNIQUE KEY uq_qipai_cleaning_settlement_item_task/);
assert.match(cleaningCollaborationVerifySql, /'qipai_cleaning_task_members'/);
assert.match(cleaningCollaborationVerifySql, /'cleaner_user_id'/);
assert.match(cleaningPayoutUpSql, /ADD COLUMN paid_by BIGINT UNSIGNED NULL/);
assert.match(cleaningPayoutUpSql, /ADD COLUMN payout_channel VARCHAR\(32\) NOT NULL DEFAULT ''/);
assert.match(cleaningPayoutUpSql, /ADD COLUMN payout_reference VARCHAR\(128\) NOT NULL DEFAULT ''/);
assert.match(cleaningPayoutUpSql, /ADD COLUMN payout_error VARCHAR\(512\) NOT NULL DEFAULT ''/);
assert.match(cleaningPayoutUpSql, /fk_qipai_cleaning_settlements_paid_by/);
assert.match(cleaningPayoutUpSql, /idx_qipai_cleaning_settlement_payout/);
assert.match(cleaningPayoutDownSql, /DROP FOREIGN KEY fk_qipai_cleaning_settlements_paid_by/);
assert.match(cleaningPayoutDownSql, /DROP COLUMN payout_error/);
assert.match(cleaningPayoutVerifySql, /'payout_reference'/);
assert.match(cleaningPayoutVerifySql, /'2026062728'/);
assert.match(cleaningTransferStateUpSql, /ADD COLUMN payout_state VARCHAR\(32\) NOT NULL DEFAULT ''/);
assert.match(cleaningTransferStateUpSql, /ADD COLUMN payout_package_info VARCHAR\(1024\) NOT NULL DEFAULT ''/);
assert.match(cleaningTransferStateUpSql, /idx_qipai_cleaning_settlement_transfer_state/);
assert.match(cleaningTransferStateDownSql, /DROP COLUMN payout_package_info/);
assert.match(cleaningTransferStateVerifySql, /'payout_state'/);
assert.match(cleaningTransferStateVerifySql, /'2026062729'/);
console.log('PASS: M01-B through M08-B migration contracts are present.');