- Update Activity Log with 108 missing commits (48 backend + 60 frontend) - Update version references: backend v2.8.79, frontend v2.8.94 - Update migration count: 18 migrations (0000-0017) - Update Telegram Mini App Flow to v2.8.94 - Update Payment Flow - Scanner to 2026-06-05 - Update all architectural and database references - Add MongoDB removal handoff document with updated versions Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
24 KiB
DB Migration Audit Report — Amanat Escrow (Mongo→PG)
Date: 2026-06-02 | Scope: Full Mongo→PG migration audit — schemas, indexes, constraints, dual-write coverage, backfill, verify harness, and service-layer Mongo-idiomatic patterns
Executive Summary
The migration is ~50% complete and NOT ready for PG-primary cutover. Schema and backfill scaffolding are mature (all 13 in-scope Mongo collections have Drizzle tables and backfill scripts), but three categories block cutover:
- Migration correctness —
0004_funds_ledger_entries.sqlis unjournaled (silently skipsfunds_ledger_entrieson fresh DBs);shadowRead()exists but is never called from any read path so the soak window is completely blind. - Financial integrity gaps — missing
CHECK (amount > 0)/fx_rate > 0constraints, ~20 FK columns declared inrelations()only (never as physical FKs), backfill that silently writesamount = '0'for NULL Mongo amounts. - Service-layer rework is far bigger than the schema work — the factory (
createRepositories) has zero callers; 30+ services still import Mongoose directly and contain ~50 Mongo-idiomatic patterns (N+1 loops, full-fetch+JS-filter, read-modify-write without locking, multi-table writes with no transaction) that will cause real money errors and lost updates under concurrent load.
Critical Issues (must fix before cutover)
| # | Dimension | Table/File → Issue | Fix |
|---|---|---|---|
| CR-1 | Migration | 0004_funds_ledger_entries.sql has NO _journal.json entry; funds_ledger_entries DDL conflicts between 0003 and 0004 — silently skipped on fresh DB, ALTER fails with "constraint already exists" if run |
Reduce 0004 to trigger-only DDL; register in journal |
| CR-2 | Arch | shadowRead.ts exists and is complete, but no DualWrite*Repo read method ever calls it — soak window measures zero signal |
Wire shadowRead() + ShadowReadMetrics into all 4 DualWrite read paths |
| CR-3 | Arch | Factory createRepositories has zero callers outside src/db/repositories/ — every REPO_*=dual env flag changes env but routes zero traffic |
Inject factory into 30+ service files (paymentController, paymentCoordinator, marketplace/user/points services) |
| CR-4 | Backfill | backfill-payments.ts: extractDecimalString(null) returns '0' — NULL Mongo payment amount silently inserted as amount = 0 (money integrity violation) |
Make extractDecimalString(null) throw, or skip + warn |
| CR-5 | Backfill | backfill-derivedDestinations.ts: require() failure falls back to strict:false model — all fields become undefined, all rows skipped silently, exits 0 |
Make import failure throw and exit 1; never fall back to schema-less model |
| CR-6 | Backfill | backfill-fundsLedger.ts + backfill-pointTransactions.ts: upsertIdMap + INSERT not in one transaction — interruption leaves orphan id_map rows; re-run DO NOTHING never inserts data row → unrecoverable |
Wrap upsertIdMap + data INSERT in one PG transaction |
| CR-7 | Schema | payment_quotes: no CHECK (offer_amount > 0 AND fx_rate > 0 AND token_price_usd > 0) — zero/negative FX rate → divide-by-zero in settlement |
Add three CHECK constraints |
| CR-8 | Schema | payments: no CHECK (amount > 0) |
ALTER TABLE payments ADD CONSTRAINT ck_payments_amount_pos CHECK (amount > 0) |
| CR-9 | Verify | checksums.ts: .catch(() => []) silently returns [] on DB connection failure → hasMismatch=false, gate passes green |
Propagate errors; never swallow in comparison path |
| CR-10 | Verify | shadowRead.ts: Decimal128 detection (constructor.name === 'Decimal128') breaks on .lean() POJO results ({$numberDecimal:...}) — every numeric field appears equal, silent false-negative |
Normalize amounts to strings before compare; detect $numberDecimal key |
| CR-11 | Verify | migration-fk-idmap.test.ts: skipIfUnreachable returns early without test.skip() — money-safety tests PASS when DB is unreachable; CI exit 0 |
Call test.skip() when !isReachable; CI must assert MONEY_SAFETY_TESTS_SKIPPED absence |
| CR-12 | Verify | rowCounts.ts: id_map coverage check exists for payments only — dropped id_map entry for other collections → dangling FK silently |
Add id_map coverage checks for users/purchaseRequests/sellerOffers/fundsLedger/pointTransactions |
| CR-13 | Code | paymentCoordinator.ts executePaymentUpdate: read status → JS guard → write — two concurrent webhooks both read pending, both write (lost update) |
UPDATE payments SET status=... WHERE id=... AND status NOT IN ('completed','cancelled','refunded') RETURNING *; 0 rows → abort |
| CR-14 | Code | paymentCoordinator.ts dispute gate: isReleaseBlockedById(prId) read, then payment update — dispute raised in the gap bypasses gate |
SELECT ... FOR UPDATE on PR row + payment update in one transaction |
| CR-15 | Code | paymentCoordinator.ts executePaymentUpdate: payment update + ledger append + PR backfill + acceptOffer + duplicate cancel + template delete run with NO transaction — step 3/4 failure leaves payment completed but offer not accepted |
Wrap all side effects in one DB transaction |
| CR-16 | Code | DisputeService.ts createDispute: dispute create → chat create → save → setChatId with no transaction — partial failure → orphaned dispute/chat, UI crashes |
Wrap all four ops in one PG transaction |
| CR-17 | Code | SellerOfferService.ts withdrawOffer + marketplaceController.ts validateStatusTransition: read-validate-write status machine with no atomic guard |
UPDATE ... WHERE id=... AND status=... RETURNING *; 0 rows → 409 Conflict |
| CR-18 | Code | PointsService.ts getReferrals/collectDeliveredReferralOrders: per-referred-user while(true) skip/limit loop + per-row offer lookup → 510+ queries/user; 14+ s per leaderboard on WAN |
Replace with single CTE: LEFT JOIN purchase_requests/seller_offers/point_transactions ... GROUP BY u.id |
| CR-19 | Code | PurchaseRequestService.ts searchPurchaseRequests: findPurchaseRequests({limit:100}) then JS .filter().slice(0,20) — catastrophic at 10k rows |
WHERE title ILIKE $s OR description ILIKE $s LIMIT 20, or tsvector generated column + GIN |
| CR-20 | Code | Chat model: messages[]/participants[]/unreadCounts[] as JSONB — no FK integrity, unbounded row bloat, non-indexable |
Child tables chat_messages, chat_participants, chat_message_reactions; rewrite ChatService as SQL |
High-Priority Issues
Schema — Missing Physical FKs
All declared via Drizzle relations() only, never as foreignKey()/.references(). Zero referential integrity enforcement in DB.
users.referred_by_id→ add FKON DELETE SET NULLpurchase_requests.buyer_id,category_id,selected_offer_id- All PR child tables (
purchase_request_delivery_info,_delivery_address,_seller_delivery_info,_service_info,_specifications,_preferred_sellers) —purchase_request_id/delivery_info_id delivery_attempts.delivery_info_id,seller_idderived_destinations.buyer_id,seller_id,seller_offer_idderived_destination_sweeps.destination_idtrezor_accounts.user_id;trezor_derived_addresses.trezor_account_idfunds_ledger_entries.purchase_request_id,payment_id(deferred since 0003, never added)point_transactions.user_id,referred_user_id
Schema — Other HIGH
users: noCHECK (points_available >= 0 AND points_available <= points_total)point_transactions: noCHECK (balance >= 0)payment_quotes: noCHECK (settle_amount >= raw_settle_amount)(snap-up invariant unenforced)purchase_request_preferred_sellers: no composite PK, only uniqueIndexseller_offers.price_amount numeric(18,8)vs project-widenumeric(38,18)(precision gap in settlement)
Migration — HIGH
- All 70+
CREATE INDEXare non-CONCURRENTLY(blocking SHARE lock on live data for all) - All FK
ADD CONSTRAINTrun validating (noNOT VALID+ laterVALIDATE) — prolonged ACCESS EXCLUSIVE lock blog_postsandnotificationsexported from schema barrel but no migration creates themdisputes/chatsusetext(notuuid) for FK columns — zero referential integrity- Migration 0009: three sequential
UPDATEDML steps not inBEGIN/COMMIT— partial failure leaves inconsistent category re-parenting
Backfill — HIGH
String(number)fornumericcolumns risks scientific notation inbackfill-purchaseRequests.ts(budget),backfill-sellerOffers.ts(price.amount),backfill-requestTemplates.ts(budget, proposal.price)backfill-users.ts:email ?? nullfails ifusers.emailis NOT NULL for OAuth-only usersbackfill-fundsLedger.ts: missingd.entryType(no default) → NOT NULL violationrun-backfill.ts:requestTemplatesruns in Tier B but runbook documents it last (inconsistency)
Verify — HIGH
reconcile.ts: no double-refund detection; noescrow_state↔last-ledger-entry check;LIMIT 1000silently truncatesrowCounts.ts:estimatedDocumentCount()is approximate — usecountDocuments({})checksums.ts: no Mongo-side per-user points balance comparison during dual-write windowledgerImmutability.ts:TRUNCATEbypasses row-level trigger — addBEFORE TRUNCATEstatement-level trigger- Enum-value completeness verified nowhere
Code — HIGH
SellerOfferService.tsacceptOffer: per-rejected-sellercreateNotificationloop (usecreateNotificationsBulk); multi-UPDATE repo needs transactionRequestTemplateService.tsbatchConvertTemplates: ~50 sequential queries per 10-item cart; no transaction per item → orphan PRs with no offer, oversold templatespaymentService.tscreatePaymentRecord:String(metadata?.sellerId || createLegacyObjectIdString())injects random fake ObjectIds as FKs → PG FK violationuserController.tsgetUsersList:$regexon name/email → PG seqscan; needspg_trgmGIN index +ILIKEPurchaseRequestService.tsupdatePurchaseRequestStatus(completed): non-idempotent double-points risk; no transaction
Medium Issues
Schema: dual unique indexes on categories.name (drop raw, keep partial WHERE is_active); missing payments(purchase_request_id, status) composite index; missing seller_offers(seller_id,status), derived_destinations(address, chain_id), trezor_derived_addresses.address indexes; id_map no PK and new_id no unique constraint; request_templates no CHECK (usage_count <= max_usage).
Migration: wallet_type enum created but used in no column (dead DDL); ALTER TYPE offer_currency ADD VALUE 'TRY' requires PG 12+ in-transaction; ck_pr_budget_currency_crypto add(0006)/drop(0007) round-trip fails on rows with non-crypto values; chats.participants JSONB has no GIN index.
Backfill: enum default mismatches (provider:'request.network' vs request_network); escrow_state ?? null may hit NOT NULL; derivedDestinations.lastKnownBalance via JS Number loses precision above 2^53 for wei.
Code: dataCleanupService.getCollectionStats — 13 sequential countDocuments() (should be single subselect); userController.updateUserProfile writes arbitrary profile.${key} (whitelist needed); paymentCoordinator metadata read-spread-write overwrites concurrent keys (use metadata || jsonb_build_object(...)); skip/limit pagination in getOffersBySeller/getUsersList.
Index & Constraint Punch List
| Table | Missing | Recommended DDL |
|---|---|---|
| payments | CHECK amount > 0 | ALTER TABLE payments ADD CONSTRAINT ck_payments_amount_pos CHECK (amount > 0); |
| payments | (purchase_request_id, status) | CREATE INDEX CONCURRENTLY idx_payments_pr_status ON payments (purchase_request_id, status); |
| payments | disputed partial | CREATE INDEX CONCURRENTLY idx_payments_disputed ON payments (id) WHERE disputed = true; |
| payment_quotes | CHECK money fields | ALTER TABLE payment_quotes ADD CONSTRAINT ck_pq_pos CHECK (offer_amount > 0 AND fx_rate > 0 AND token_price_usd > 0); |
| payment_quotes | CHECK snap-up | ALTER TABLE payment_quotes ADD CONSTRAINT ck_pq_settle CHECK (settle_amount >= raw_settle_amount); |
| users | referred_by_id FK | ALTER TABLE users ADD CONSTRAINT users_referred_by_fk FOREIGN KEY (referred_by_id) REFERENCES users(id) ON DELETE SET NULL NOT VALID; then VALIDATE |
| users | CHECK points | ALTER TABLE users ADD CONSTRAINT ck_users_points CHECK (points_available >= 0 AND points_used >= 0 AND points_total >= 0 AND points_available <= points_total); |
| point_transactions | CHECK balance | ALTER TABLE point_transactions ADD CONSTRAINT ck_pt_balance CHECK (balance >= 0); |
| funds_ledger_entries | FK pr + payment | ALTER TABLE funds_ledger_entries ADD CONSTRAINT fle_pr_fk FOREIGN KEY (purchase_request_id) REFERENCES purchase_requests(id) NOT VALID; then VALIDATE |
| funds_ledger_entries | TRUNCATE trigger | CREATE TRIGGER funds_ledger_no_truncate BEFORE TRUNCATE ON funds_ledger_entries FOR EACH STATEMENT EXECUTE FUNCTION funds_ledger_immutable_fn(); |
| trezor_accounts | user_id FK | ALTER TABLE trezor_accounts ADD CONSTRAINT ta_user_fk FOREIGN KEY (user_id) REFERENCES users(id) NOT VALID; then VALIDATE |
| derived_destinations | buyer/seller/offer FK + (address,chain_id) | add 3 FKs NOT VALID; CREATE INDEX CONCURRENTLY idx_dd_addr_chain ON derived_destinations (address, chain_id); |
| purchase_requests | buyer/category/offer FK + (status,created_at) | add 3 FKs NOT VALID; CREATE INDEX CONCURRENTLY idx_pr_status_created ON purchase_requests (status, created_at DESC); |
| seller_offers | (seller_id,status) + (purchase_request_id,status) | CREATE INDEX CONCURRENTLY idx_so_seller_status ON seller_offers (seller_id, status); |
| id_map | PK + new_id unique | ALTER TABLE id_map ADD PRIMARY KEY (collection, legacy_object_id); CREATE UNIQUE INDEX id_map_new_id_uq ON id_map (new_id); |
| users (search) | trigram | CREATE INDEX CONCURRENTLY idx_users_name_trgm ON users USING GIN (lower(first_name||' '||last_name||' '||coalesce(email,'')) gin_trgm_ops); |
| purchase_requests | tags GIN | CREATE INDEX CONCURRENTLY idx_pr_tags ON purchase_requests USING GIN (tags); |
Repository Coverage Matrix
| Interface | Drizzle Impl | Dual-Write | Status |
|---|---|---|---|
| PaymentRepo | Yes | Yes (shadow read NOT wired) | PARTIAL |
| UserRepo | Yes | Yes (shadow read NOT wired) | PARTIAL |
| MarketplaceRepo | Yes | Yes (shadow read NOT wired) | PARTIAL |
| PointsRepo | Yes | Yes (shadow read NOT wired) | PARTIAL |
| ReleaseHoldRepo | Yes | — | No dual-write |
| TrezorAccountRepo | Yes | — | No dual-write |
| DerivedDestinationRepo | Yes | — | No dual-write |
Factory has zero application callers (CR-3) — most critical architecture gap.
Backfill Coverage Matrix
| Mongo Collection | Backfill Script | Ordering | Status |
|---|---|---|---|
| users | backfill-users.ts | Tier A | OK (email NOT NULL risk) |
| categories | backfill-categories.ts | Tier A | OK |
| requestTemplates | backfill-requestTemplates.ts | Tier B | OK (String() decimals; runbook order mismatch) |
| purchaseRequests | backfill-purchaseRequests.ts (2-pass) | Tier B | OK (String() decimals; silent preferred-seller skips) |
| sellerOffers | backfill-sellerOffers.ts | Tier B | OK (String() price.amount) |
| payments | backfill-payments.ts | Tier C | RISK — NULL amount → '0' (CR-4) |
| fundsLedger | backfill-fundsLedger.ts | Tier C | RISK — non-txn idMap (CR-6); entryType NOT NULL |
| derivedDestinations | backfill-derivedDestinations.ts | Tier C | RISK — schema-less fallback (CR-5); wei precision |
| trezorAccounts | backfill-trezorAccounts.ts | Tier C | OK |
| pointTransactions | backfill-pointTransactions.ts | Tier C | RISK — non-txn idMap (CR-6); String() decimals |
| id_map | (infra — _idMap.ts) |
n/a | CORRECT |
| payment_quotes | (none — runtime-generated) | n/a | EXPECTED |
| pg_dualwrite_gaps | (none — operational log) | n/a | EXPECTED |
Verification Coverage Matrix
| Concern | Covered By | Gap |
|---|---|---|
| Row-count parity | rowCounts.ts (9/~23 collections) | estimatedDocumentCount() approximate; id_map not counted |
| ID-mapping completeness | rowCounts.ts (payments only) | CRITICAL — no check for users/PR/sellerOffers/FLE/pointTransactions |
| FK integrity | rowCounts.ts (7 pairs) | seller_offers.seller_id, trezor_accounts→users missing |
| Money sum accuracy | checksums.ts | .catch(()=>[]) silent pass on conn failure (CR-9) |
| Ledger reconciliation | reconcile.ts | No double-refund; no escrow_state↔last-entry; LIMIT 1000 truncation |
| Ledger immutability | ledgerImmutability.ts | TRUNCATE bypass; no schema filter on pg_proc |
| Shadow read fidelity | shadowRead.ts | Decimal128 lean false-negative (CR-10); not wired (CR-2) |
| Enum completeness | — | Not covered anywhere |
| Timestamp precision/TZ | — | Not covered |
| CI gate output | boolean only | No JSON stdout; tests pass-not-skip on unreachable DB (CR-11) |
Models Not Yet in PG Schema
| Mongo Model | Fields | Actively Used | Effort | Notes |
|---|---|---|---|---|
| Dispute | ~15 + 3 embedded arrays | Yes (DisputeService, releaseHoldService) | L | evidence[]/timeline[]/messages[] → child tables; pre-save timeline hook → service |
| Notification | 11 | Yes (all services, high frequency) | S schema / M migration | userId as text → uuid FK backfill; TTL index → pg_cron |
| ShopSettings | ~12 | Yes (marketplace template pages) | S | paymentConfig.allowedChains int[], socialLinks → 4 columns |
| ConfigSetting (+History) | 4 (+audit) | Yes (walletMonitor, scanner threshold) | S | key-value; history child table |
| LevelConfig | ~10 | Yes (PointsService) | S | flatten benefits{} to 4 columns |
| Address | 10 | Yes (dataCleanup, delivery flows) | S | addressType pgEnum; one-primary partial unique |
| Review | 9 | Admin CMS only | S | polymorphic subjectId → ref_kind discriminator |
| TelegramLink | ~12 | Yes (auth) | S | two unique constraints; (userId, status) idx |
| TelegramSession | ~10 | Yes (auth middleware) | S | TTL expiresAt → pg_cron |
| BlogPost | ~20 | Admin CMS only | S | videos[] child table; slug/publishedAt pre-save → service |
| TempVerification | 8 | Registration only | S | TTL cleanup |
Mongo-Idiomatic Code Refactoring Tracker
| Pattern | File | Function | Severity | Fix |
|---|---|---|---|---|
| N+1 | PointsService.ts | getReferrals / collectDeliveredReferralOrders | CRITICAL | Single CTE with LEFT JOINs + GROUP BY |
| N+1 | SellerOfferService.ts | acceptOffer | HIGH | createNotificationsBulk + single seller-id query |
| N+1 | RequestTemplateService.ts | batchConvertTemplates | HIGH | Batch SELECT ANY($links), batch INSERT…VALUES, single usage UPDATE |
| N+1 | dataCleanupService.ts | getCollectionStats | MEDIUM | Single subselect count query |
| Full-fetch+filter | PurchaseRequestService.ts | searchPurchaseRequests | CRITICAL | ILIKE/tsvector WHERE + LIMIT 20 |
| Full-fetch+filter | PurchaseRequestService.ts | createPurchaseRequest (dup detect) | HIGH | WHERE buyer/title/description/created_at LIMIT 1 |
| Full-fetch+filter | paymentCoordinator.ts | executePaymentUpdate (template cleanup) | HIGH | Push JSONB conditions into WHERE/DELETE |
| Full-fetch+filter | userController.ts | getUsersList | HIGH | pg_trgm GIN + ILIKE |
| JSONB no join table | Chat | messages/participants/unreadCounts | CRITICAL | 3 child tables (CR-20) |
| JSONB no join table | Dispute | evidence/timeline/messages | HIGH | 3 child tables on migration |
| JSONB schemaless | Payment | metadata | HIGH | Promote is_template_checkout, rn_request_id to typed columns |
| In-memory agg | PointsService.ts | sumDeliveredReferralSpend | CRITICAL | SUM in CTE |
| In-memory agg | SellerOfferService.ts | getOfferStatistics | MEDIUM | COUNT(*) OVER() / ROLLUP |
| Lost update | paymentCoordinator.ts | executePaymentUpdate | CRITICAL | UPDATE…WHERE status NOT IN (terminal) RETURNING |
| Lost update | SellerOfferService.ts | updateOffer | HIGH | UPDATE…WHERE status='pending' RETURNING |
| TOCTOU | SellerOfferService.ts | withdrawOffer | CRITICAL | UPDATE…WHERE id AND seller AND status='pending' |
| TOCTOU | marketplaceController.ts | validateStatusTransition | CRITICAL | UPDATE…WHERE status=$expected; 0 rows → 409 |
| TOCTOU | paymentCoordinator.ts | dispute gate | CRITICAL | FOR UPDATE on PR + same txn |
| TOCTOU | PurchaseRequestService.ts | updatePurchaseRequestStatus | HIGH | UPDATE…WHERE status=$old RETURNING |
| Missing txn | paymentCoordinator.ts | executePaymentUpdate | CRITICAL | One txn for all side effects |
| Missing txn | DisputeService.ts | createDispute | CRITICAL | One txn for dispute+chat+link |
| Missing txn | SellerOfferService.ts | acceptOffer (repo) | HIGH | Txn for accept/reject/PR update |
| Missing txn | RequestTemplateService.ts | batchConvertTemplates | HIGH | Txn (or savepoint) per cart item |
| Missing txn | PurchaseRequestService.ts | updatePurchaseRequestStatus (completed) | HIGH | Txn or outbox for referral reward |
| Schemaless write | paymentService.ts | createPaymentRecord | HIGH | Remove fake-ObjectId FK fallback |
| Schemaless write | userController.ts | updateUserProfile | MEDIUM | Whitelist + jsonb || merge |
| Skip/limit pagination | PointsService.ts | collectDeliveredReferralOrders | CRITICAL | Replace loop with aggregate |
| Skip/limit pagination | PurchaseRequestService.ts | searchPurchaseRequests | HIGH | Keyset on (created_at, id) |
| Skip/limit pagination | SellerOfferService.ts / userController.ts | getOffersBySeller / getUsersList | MEDIUM | Keyset + cap limit 100 |
| Virtual/hook | Chat | addMessage/markAsRead/getUnreadCount | CRITICAL | SQL ops in ChatRepository |
| Pre-save hook | FundsLedgerEntry | immutability | HIGH | Apply trigger DDL now |
Migration Completion Assessment
| Layer | % |
|---|---|
| Schema (Drizzle tables vs Mongo collections) | 90% |
| Repository layer | 70% |
| Backfill scripts | 85% |
| Verification harness | 75% |
| Service layer (Mongo→RDBMS patterns) | 5% |
| Overall | ~50% |
Top 5 Blockers for PG-Primary Cutover
- Service-layer rework not started + factory uncalled (CR-3) — flag flips route zero traffic; ~50 patterns including lost-update/missing-txn money bugs
- Transaction + locking defects on payment/escrow paths (CR-13–17) — real money errors and lost updates under concurrent webhooks
- Shadow read unwired (CR-2) — soak window is blind; cutover decision would be based on no signal
- Migration correctness: 0004 unjournaled + duplicate ledger DDL (CR-1) — fresh-DB apply silently omits
funds_ledger_entries - Money-integrity gaps + verification silent-passes (CR-4/7/8/9/10/11/12) — corruption can occur and pass green
Recommended Next Actions
| # | Action | Files | Effort |
|---|---|---|---|
| 1 | Fix 0004 journal collision | 0004_funds_ledger_entries.sql, _journal.json |
S |
| 2 | Add money CHECK constraints + apply ledger TRUNCATE trigger | new migration on payments/payment_quotes/users/point_transactions/funds_ledger_entries | S |
| 3 | Fix backfill money/integrity defects (NULL amount, schema-less fallback, non-txn idMap, _decimal.ts) |
backfill-payments/derivedDestinations/fundsLedger/pointTransactions/purchaseRequests | M |
| 4 | Close verification silent-passes (checksums, shadowRead, test.skip, id-map/enum/FK coverage, --json gate) |
checksums.ts, shadowRead.ts, reconcile.ts, rowCounts.ts, migration-fk-idmap.test.ts | M |
| 5 | Add all deferred physical FKs NOT VALID + VALIDATE; rebuild blocking indexes CONCURRENTLY |
new migration | M |
| 6 | Wire shadow reads into all 4 DualWrite read paths | DualWritePayment/User/Marketplace/PointsRepo | M |
| 7 | Inject factory into services + fix money/escrow concurrency (txn + UPDATE…WHERE…RETURNING) |
paymentCoordinator, DisputeService, SellerOfferService, marketplaceController, PurchaseRequestService | L |
| 8 | Eliminate N+1 / full-fetch / skip-limit hotpaths | PointsService, searchPurchaseRequests, batchConvertTemplates, getUsersList | L |
| 9 | Schema + backfill for unmodeled active models | Dispute (L), Notification (M), ShopSettings/ConfigSetting/Address/Telegram* (S each) | L |