Payment Collection Bridge — Uygulama Planı
DerinKarar belgesi:
docs/adr/0001-payment-collection-bridge.mdOluşturuldu: 2026-05-15 Stack: Next.js 14 + Prisma + PostgreSQL 18 + Stripe (live mode aktif)
Faz Haritası (özet)
Bölüm başlığı “Faz Haritası (özet)”| Faz | İçerik | Süre | Risk |
|---|---|---|---|
| 0 | pg_dump + ADR | 0.5 gün | 🟢 |
| 1 | Schema + migration | 1 gün | 🟢 |
| 2 | Dual-write (Stripe + Wise + Crypto) | 2 gün | 🟡 |
| 3 | Backfill (2 LIVE Order + 1 LIVE Invoice) | 0.5 gün | 🟡 |
| 4 | Read switch (revenue-chart, dashboard, customer) | 3 gün | 🟡 |
| 5 | PendingOrder → Payment migrate | 2 gün | 🟡 |
| 6 | PaymentSubmission → Payment.evidence merge | 2 gün | 🟡 |
| 7 | Contract (sadece PaymentSubmission DROP; PendingOrder kalır — D1) | 1 gün | 🔴 |
Toplam: 10-12 oturum.
Faz 0 — Ön Hazırlık ✓
Bölüm başlığı “Faz 0 — Ön Hazırlık ✓”-
pg_dumpsnapshot:/var/backups/etp-pre-payment-bridge-YYYYMMDD-HHMMSS.dump - ADR:
docs/adr/0001-payment-collection-bridge.md(170 satır) - Plan: bu dosya
Faz 1 — EXPAND (Schema)
Bölüm başlığı “Faz 1 — EXPAND (Schema)”Görev 1.1: prisma/schema.prisma güncelle
Bölüm başlığı “Görev 1.1: prisma/schema.prisma güncelle”Eklenecek:
Paymentmodel (ADR’daki şema)PaymentTypevePaymentStatusenumCustomer,Order,Invoicemodellerinepayments Payment[]relation alanı
Bağımsız bölüm — diğer modellere ekleme:
model Customer { // ... mevcut alanlar payments Payment[]}model Order { // ... mevcut alanlar payments Payment[]}model Invoice { // ... mevcut alanlar payments Payment[]}Görev 1.2: Migration üret, gözden geçir, deploy
Bölüm başlığı “Görev 1.2: Migration üret, gözden geçir, deploy”pnpm prisma migrate dev --create-only --name payment_bridge_expand# Üretilen SQL'i Read et:# - CREATE TABLE "Payment" doğru mu?# - Index'ler oluşturuldu mu?# - Manuel CHECK constraint ekle:# ALTER TABLE "Payment" ADD CONSTRAINT payment_must_reference# CHECK ("orderId" IS NOT NULL OR "invoiceId" IS NOT NULL);pnpm prisma migrate deploypnpm prisma generatepnpm next build && chown -R yigit:yigit .next/ .env && pm2 restart ecutuningportalDoğrulama:
PGPASSWORD=... psql -c "\d \"Payment\"" # tablo görünmeliPGPASSWORD=... psql -c "SELECT COUNT(*) FROM \"Payment\";" # = 0pnpm next build # build hatasızTest kategorisi: [infra] — schema migration, app davranışı değişmedi.
Rollback: DROP TABLE "Payment" CASCADE; + relation alanlarını schema’dan geri al + pnpm prisma migrate resolve --rolled-back payment_bridge_expand.
Faz 2 — DUAL-WRITE
Bölüm başlığı “Faz 2 — DUAL-WRITE”Görev 2.1: lib/payment/payment-bridge.ts helper
Bölüm başlığı “Görev 2.1: lib/payment/payment-bridge.ts helper”createPayment(input)— idempotent (externalRef varsa upsert)- Detay kod: ADR ve plan-yaz çıktısında mevcut
Görev 2.2: Stripe webhook dual-write
Bölüm başlığı “Görev 2.2: Stripe webhook dual-write”Dosya: app/api/payment/webhook/route.ts
Eklenen satırlar:
payment_intent.succeeded(line ~92-637):Order.updatesonrasıcreatePayment({ type:'STRIPE_CARD', status:'CONFIRMED', orderId, externalRef: pi.id, amountCents: pi.amount, customerId })checkout.session.completed(line ~120-133): metadata.type=‘invoice_payment’ isecreatePayment({ type:'STRIPE_CARD', status:'CONFIRMED', invoiceId, externalRef: session.payment_intent })charge.refunded(line ~760-785): mevcutPayment WHERE externalRef = pi.id’i bul,status = REFUNDEDyappayment_intent.payment_failed(line ~639-676):Payment(status=FAILED)yaz veya pending’i FAILED’a güncelle
Görev 2.3: Wise / Crypto / PendingOrder akışı
Bölüm başlığı “Görev 2.3: Wise / Crypto / PendingOrder akışı”Dosyalar:
app/api/admin/invoices/[id]/submissions/[subId]/approve/route.ts:56— submission approved →createPayment({ type:'WISE_SEPA', status:'CONFIRMED', invoiceId, externalRef: submission.txHash, evidence })lib/payment/verify-orchestrator.ts:146,212— orchestrator update pathapp/api/admin/orders/pending/[id]/confirm/route.ts:62— PendingOrder.confirmed →createPayment({ type: pendingOrder.paymentType.toUpperCase(), status:'CONFIRMED', invoiceId, ... })
Test: __tests__/payment/payment-bridge.test.ts — Vitest + MSW Stripe webhook simulation.
Faz 3 — BACKFILL
Bölüm başlığı “Faz 3 — BACKFILL”Görev 3.1: scripts/backfill-payments.ts
Bölüm başlığı “Görev 3.1: scripts/backfill-payments.ts”- Tüm
Order.status='paid'+payments NONE→ Payment(STRIPE_CARD/CONFIRMED) yarat - Tüm
Invoice.status='paid'+payments NONE→ Payment(WISE_SEPA veya MANUAL/CONFIRMED) yarat evidence.backfilled: trueflag — gerçek dual-write kayıtlarından ayırmak için
Çalıştırma: pnpm tsx scripts/backfill-payments.ts
Doğrulama SQL:
SELECT (SELECT COALESCE(SUM("amountCents"),0) FROM "Payment" WHERE status='CONFIRMED') / 100.0 AS payment_eur, (SELECT COALESCE(SUM("total"),0) FROM "Order" WHERE status='paid') / 100.0 + (SELECT COALESCE(SUM("amount"),0) FROM "Invoice" WHERE status='paid') AS legacy_eur;-- İki sütun eşit olmalı; sapma > 0.01 EUR ise STOP + araştırZORUNLU: Faz 3 öncesi taze pg_dump. Backfill başarısız olursa snapshot restore.
Faz 4 — READ SWITCH
Bölüm başlığı “Faz 4 — READ SWITCH”Görev 4.1: lib/payment/payment-queries.ts
Bölüm başlığı “Görev 4.1: lib/payment/payment-queries.ts”getCustomerTotalRevenue(customerId)— tek querygetRevenueChartBuckets(from, to, granularity)— dashboard ve revenue-chart içingetRevenueByPeriod(from, to)— dashboard summary
Görev 4.2-4.4: Refactor
Bölüm başlığı “Görev 4.2-4.4: Refactor”| Dosya | Önce | Sonra |
|---|---|---|
app/api/admin/revenue-chart/route.ts | 4 paralel query + duplicate-guard | 1 query (getRevenueChartBuckets) |
app/[locale]/admin/(panel)/dashboard/page.tsx | 9 paralel query | 3 query |
app/[locale]/admin/(panel)/customers/[id]/page.tsx | Order + Invoice ayrı aggregate | Payment tek aggregate |
app/[locale]/admin/(panel)/customers/[id]/orders/page.tsx | findMany + aggregate | findMany (Order kalır, sayım Payment’tan) |
app/[locale]/admin/(panel)/customers/[id]/invoices/page.tsx | findMany + aggregate | findMany (Invoice kalır, sayım Payment’tan) |
app/[locale]/admin/(panel)/customers/[id]/payment-submissions/page.tsx | findMany | findMany (Submission kalır Faz 5’e kadar) |
Doğrulama:
- Her sayfa: önceki/sonraki gelir tutarı eşit
- Browser MCP zorunlu görsel audit: 3-agent paralel ordu (design-reviewer + a11y-debugging + frontend-design skill)
- Dashboard p99 latency ölç (önce/sonra)
Test kategorisi: Integration + Visual
Faz 5 — PendingOrder → Payment
Bölüm başlığı “Faz 5 — PendingOrder → Payment”Görev 5.1-5.3
Bölüm başlığı “Görev 5.1-5.3”confirm-sepa/route.tsveconfirm-crypto/route.ts:Payment(status=PENDING, type=WISE_SEPA|CRYPTO_TRC20)yarat + PendingOrder paralel yazılmaya devam (rollback kapısı)admin/orders/pending/[id]/confirm:Payment.status = CONFIRMED- Admin orders sayfasında “pending payments” listesi
Paymentkaynağından
Faz 6 — PaymentSubmission → Payment.evidence
Bölüm başlığı “Faz 6 — PaymentSubmission → Payment.evidence”Görev 6.1-6.3
Bölüm başlığı “Görev 6.1-6.3”Payment.evidenceJSON alanınaclaimedSenderName,txHash,apiResponseSnapshot,matchScoretaşı- Wise webhook
app/api/webhooks/wise/route.ts→ Payment üzerinde işlem - Cron
app/api/cron/retry-payment-verification/route.ts→ Payment üzerinde retry - Customer kanıt yükleme UI → Payment yaratır
Faz 7 — CONTRACT ✓ (uygulandı 2026-05-15, commit 6154b7b)
Bölüm başlığı “Faz 7 — CONTRACT ✓ (uygulandı 2026-05-15, commit 6154b7b)”Önkoşullar (uygulanan)
Bölüm başlığı “Önkoşullar (uygulanan)”- ❌ 7-gün monitoring atlandı — kullanıcı kararı 2026-05-15 (A kati emir), Faz 6 sonrası aynı gün Faz 7’ye geçildi
- ✅ Backup taze:
/var/backups/postgres/etp-pre-faz7-20260515-211215.dump(1.4MB) - ✅ Migration
--create-onlyreview + staging test atlandı, doğrudan prod (kabul edilen risk)
D1 Kararı: Sadece PaymentSubmission DROP
Bölüm başlığı “D1 Kararı: Sadece PaymentSubmission DROP”Plan iç çelişkisi tespit edildi (FAZ 5 minimal seçilmişti → PendingOrder kanon kaynak). Kullanıcı D1 kararı (2026-05-15): sadece PaymentSubmission düşürüldü, PendingOrder kalır. Müşteri SEPA/Crypto sipariş başlatma akışı hala PendingOrder kullanıyor.
Görev 7.1: Dual-write kaldır ✓
Bölüm başlığı “Görev 7.1: Dual-write kaldır ✓”- Wise webhook + cron retry’da PaymentSubmission yazımları kaldırıldı
- Stripe webhook zaten Payment kanon yazıyordu (Faz 2’den)
Görev 7.2: Migration ✓
Bölüm başlığı “Görev 7.2: Migration ✓”-- Migration: 20260515210000_drop_payment_submission-- PaymentSubmission tablosu CASCADE düşürüldü (FK constraint'leri otomatik)-- PendingOrder KALIR — D1 kararıGörev 7.3: Kod cleanup ✓ (18 dosya, plan kapsamı 3 dosyaydı, gerçek 18)
Bölüm başlığı “Görev 7.3: Kod cleanup ✓ (18 dosya, plan kapsamı 3 dosyaydı, gerçek 18)”- Admin pages (4):
customers/[id]/{layout,page,payment-submissions},invoices/[id] - Admin API write (2):
approve,reject— null-safety + reconciledBy set - Async match paths (2):
webhooks/wise,cron/retry-payment-verification(race guard eklendi) - Dev/customer API (3):
dev-complete,dev-reset,submit/wise(yorum) - Lib (2):
verify-orchestrator(paymentId cuid string),finalize-paid(obsolete params) - Scripts (2):
seed-payment-test,verify-payment-flow - Component (1):
PaymentSubmissionsPanel
Kalite iyileştirmeleri (code-review + design-review sonrası)
Bölüm başlığı “Kalite iyileştirmeleri (code-review + design-review sonrası)”- Race guard:
tx.payment.update where status: PENDING— webhook + cron çift CONFIRMED + çift mail riski engelleniyor - Status mapping:
CONFIRMED + evidence.apiVerifiedAt yoksa admin_verified—PaymentSubmissionsPanel ADMIN ONAYLADIetiketi yeniden işlevsel - reviewedBy/reviewedAt:
Payment.reconciledByalanından propagate, hardcoded null kalktı - rejectReason:
evidence.rejectReason→ FAILED status’ünde göster - Typo fix:
REDDEDILDI→REDDEDİLDİ(büyük noktalı İ, UTF-8)
derin-dogrula yerine: Build + smoke + 2 paralel ajan code-review
Bölüm başlığı “derin-dogrula yerine: Build + smoke + 2 paralel ajan code-review”- ✅ Next.js build sorunsuz
- ✅ PM2 restart + smoke:
/200,/admin307,/customer307 - ✅
feature-dev:code-revieweragent: 2 BLOKER + 3 öneri, bloker’lar düzeltildi - ✅
design-revieweragent: 1 KRİTİK (FAZ 7 dışı, pre-existing) + 2 UYARI, FAZ 7 uyarıları düzeltildi - ⚠️ Browser MCP audit yapılamadı (SSH tunnel ölü) — manuel smoke admin/customer panel ileride
Public API contract değişiklik
Bölüm başlığı “Public API contract değişiklik”orchestratePaymentSubmission return submissionId: number → paymentId: string (cuid). Frontend ...result spread olduğu için non-breaking.
Doğrulama Komutları (her faz)
Bölüm başlığı “Doğrulama Komutları (her faz)”# Buildpnpm next build && chown -R yigit:yigit .next/ .env
# Migration durumupnpm prisma migrate status
# Testpnpm test __tests__/payment/
# DB doğrulamaPGPASSWORD=... psql -c "SELECT type, status, COUNT(*) FROM \"Payment\" GROUP BY 1,2;"
# PM2 restartpm2 restart ecutuningportal && pm2 logs ecutuningportal --lines 50Risk Matrisi
Bölüm başlığı “Risk Matrisi”| Risk | Faz | Mitigation |
|---|---|---|
| LIVE 2 Stripe + 1 Wise ödeme kayıp | 1, 3, 7 | pg_dump her faz başı + Faz 3 SUM eşitlik + Faz 7 öncesi 7 gün monitor |
| Stripe webhook idempotency bozulur | 2 | Payment.externalRef UNIQUE + StripeEvent (mevcut) |
| Dashboard p99 artar | 4 | Payment yeni + boş; index’ler hazır (customerId, type+status, createdAt) |
| Migration prod’da takılır | 1, 7 | --create-only review + staging test |
| Customer view gelir sapması | 4 | Her customer için önce/sonra SUM karşılaştırma script’i |
| FAZ 7 erken çalışır | 7 | Taze kullanıcı onayı + derin-dogrula scorecard |
Test Sözleşmesi (her faz)
Bölüm başlığı “Test Sözleşmesi (her faz)”| Faz | Kategori | Test çıktısı |
|---|---|---|
| 0 | [docs] + [infra] | backup boyutu ✓, ADR satır sayısı ✓ |
| 1 | [infra] | prisma migrate status clean, Payment tablosu var |
| 2 | Integration (Vitest+MSW) | Stripe webhook → Payment yaratıldı |
| 3 | [infra] | SQL SUM eşitliği |
| 4 | Integration + Visual | Dashboard önce/sonra aynı tutar + browser audit pass |
| 5 | Integration | Pending payment akışı |
| 6 | Integration | Evidence merge |
| 7 | E2E (Playwright) | Tam ödeme akışı: cart → Stripe → Payment + audit log |