import assert from 'node:assert/strict'; import { existsSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; const root = process.cwd(); const read = (path) => readFileSync(join(root, path), 'utf8'); for (const suffix of ['up', 'down', 'verify']) { assert.ok( existsSync(join(root, 'database/migrations/2026081006_m09c_cleaning_settlement_integrity.' + suffix + '.sql')), 'M09-C ' + suffix + ' migration is missing' ); } const migration = read('database/migrations/2026081006_m09c_cleaning_settlement_integrity.up.sql'); for (const pattern of [ 'qipai_cleaning_settlement_reversals', 'qipai_cleaning_settlement_events', 'amount_delta_cents BIGINT', 'active_share_key', 'uq_qipai_cleaning_settlement_active_share', 'payout_request_no', 'uq_qipai_cleaning_settlement_payout_request', "actor_type IN ('USER', 'SYSTEM')" ]) assert.ok(migration.includes(pattern), 'migration is missing ' + pattern); const repository = read('backend/src/cleaning/cleaning-task-repository.ts'); for (const pattern of [ 'CLEANING_SETTLEMENT_CONCURRENTLY_CLAIMED', 'assertTaskSettlementMutable', 'beginSettlementPayout', 'cancelSettlement', 'recordSettlementEventWithConnection', "status = 'SETTLED'", 'exportSettlements', 'querySettlementRows' ]) assert.ok(repository.includes(pattern), 'repository is missing ' + pattern); assert.match(repository, /payout_state IN \('PROCESSING', 'WAIT_USER_CONFIRM'\)/); assert.match(repository, /reversed_at IS NULL/); const payout = read('backend/src/cleaning/cleaning-payout-service.ts'); assert.match(payout, /await this\.repository\.beginSettlementPayout/); assert.match(payout, /actorType: 'SYSTEM'/); assert.match(payout, /payout_request_no AS payoutRequestNo/); assert.ok(payout.includes('CLP$' + '{settlementId}'), 'stable payout request id is missing'); const routes = read('backend/src/routes/cleaning.ts'); assert.match(routes, /settlements\/export/); assert.match(routes, /settlements\/:settlementId\/cancel/); assert.match(routes, /manualOverride: true/); const admin = read('admin/src/components/CleaningSettlementsPanel.vue'); for (const pattern of [ 'exportCleaningSettlements', 'canCancelSettlement', 'openManualPaid', 'detailDialog.detail.reversals', 'detailDialog.detail.events', 'payoutState: props.filters.payoutState', "['', 'FAIL'].includes(row.payoutState)", '金额(分)' ]) assert.ok(admin.includes(pattern), 'admin settlement UI is missing ' + pattern); for (const [file, pattern] of [ ['admin/src/components/CleaningStatisticsPanel.vue', '待结算(分)'], ['admin/src/components/CleaningTasksPanel.vue', '金额(分)'], ['admin/src/components/CleaningWorkspace.vue', '金额(分)'] ]) assert.ok(read(file).includes(pattern), file + ' must export monetary values as integer cents'); const mysqlTest = read('backend/tests/mysql-migration-roundtrip.test.mjs'); for (const pattern of [ 'm09c-generate-one', 'CLEANING_MEMBER_SETTLEMENT_LOCKED', 'm09c-cancel-processing', 'collaborative task settles only after every member share is paid' ]) assert.ok(mysqlTest.includes(pattern), 'live MySQL test is missing ' + pattern); console.log('PASS: M09-C settlement integrity, reversal, payout and shared-query gates are present.');