İçeriğe geç

Payment Collection Bridge — Uygulama Planı

Derin

Karar belgesi: docs/adr/0001-payment-collection-bridge.md Oluşturuldu: 2026-05-15 Stack: Next.js 14 + Prisma + PostgreSQL 18 + Stripe (live mode aktif)

FazİçerikSüreRisk
0pg_dump + ADR0.5 gün🟢
1Schema + migration1 gün🟢
2Dual-write (Stripe + Wise + Crypto)2 gün🟡
3Backfill (2 LIVE Order + 1 LIVE Invoice)0.5 gün🟡
4Read switch (revenue-chart, dashboard, customer)3 gün🟡
5PendingOrder → Payment migrate2 gün🟡
6PaymentSubmission → Payment.evidence merge2 gün🟡
7Contract (sadece PaymentSubmission DROP; PendingOrder kalır — D1)1 gün🔴

Toplam: 10-12 oturum.

  • pg_dump snapshot: /var/backups/etp-pre-payment-bridge-YYYYMMDD-HHMMSS.dump
  • ADR: docs/adr/0001-payment-collection-bridge.md (170 satır)
  • Plan: bu dosya

Eklenecek:

  • Payment model (ADR’daki şema)
  • PaymentType ve PaymentStatus enum
  • Customer, Order, Invoice modellerine payments 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[]
}
Terminal window
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 deploy
pnpm prisma generate
pnpm next build && chown -R yigit:yigit .next/ .env && pm2 restart ecutuningportal

Doğrulama:

Terminal window
PGPASSWORD=... psql -c "\d \"Payment\"" # tablo görünmeli
PGPASSWORD=... psql -c "SELECT COUNT(*) FROM \"Payment\";" # = 0
pnpm next build # build hatasız

Test 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.

  • createPayment(input) — idempotent (externalRef varsa upsert)
  • Detay kod: ADR ve plan-yaz çıktısında mevcut

Dosya: app/api/payment/webhook/route.ts

Eklenen satırlar:

  • payment_intent.succeeded (line ~92-637): Order.update sonrası createPayment({ type:'STRIPE_CARD', status:'CONFIRMED', orderId, externalRef: pi.id, amountCents: pi.amount, customerId })
  • checkout.session.completed (line ~120-133): metadata.type=‘invoice_payment’ ise createPayment({ type:'STRIPE_CARD', status:'CONFIRMED', invoiceId, externalRef: session.payment_intent })
  • charge.refunded (line ~760-785): mevcut Payment WHERE externalRef = pi.id’i bul, status = REFUNDED yap
  • payment_intent.payment_failed (line ~639-676): Payment(status=FAILED) yaz veya pending’i FAILED’a güncelle

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 path
  • app/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.

  • 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: true flag — 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ır

ZORUNLU: Faz 3 öncesi taze pg_dump. Backfill başarısız olursa snapshot restore.

  • getCustomerTotalRevenue(customerId) — tek query
  • getRevenueChartBuckets(from, to, granularity) — dashboard ve revenue-chart için
  • getRevenueByPeriod(from, to) — dashboard summary
DosyaÖnceSonra
app/api/admin/revenue-chart/route.ts4 paralel query + duplicate-guard1 query (getRevenueChartBuckets)
app/[locale]/admin/(panel)/dashboard/page.tsx9 paralel query3 query
app/[locale]/admin/(panel)/customers/[id]/page.tsxOrder + Invoice ayrı aggregatePayment tek aggregate
app/[locale]/admin/(panel)/customers/[id]/orders/page.tsxfindMany + aggregatefindMany (Order kalır, sayım Payment’tan)
app/[locale]/admin/(panel)/customers/[id]/invoices/page.tsxfindMany + aggregatefindMany (Invoice kalır, sayım Payment’tan)
app/[locale]/admin/(panel)/customers/[id]/payment-submissions/page.tsxfindManyfindMany (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

  • confirm-sepa/route.ts ve confirm-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 Payment kaynağından
  • Payment.evidence JSON alanına claimedSenderName, txHash, apiResponseSnapshot, matchScore taşı
  • 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)”
  • 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-only review + staging test atlandı, doğrudan prod (kabul edilen risk)

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.

  • Wise webhook + cron retry’da PaymentSubmission yazımları kaldırıldı
  • Stripe webhook zaten Payment kanon yazıyordu (Faz 2’den)
-- 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_verifiedPaymentSubmissionsPanel ADMIN ONAYLADI etiketi yeniden işlevsel
  • reviewedBy/reviewedAt: Payment.reconciledBy alanından propagate, hardcoded null kalktı
  • rejectReason: evidence.rejectReason → FAILED status’ünde göster
  • Typo fix: REDDEDILDIREDDEDİ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, /admin 307, /customer 307
  • feature-dev:code-reviewer agent: 2 BLOKER + 3 öneri, bloker’lar düzeltildi
  • design-reviewer agent: 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

orchestratePaymentSubmission return submissionId: numberpaymentId: string (cuid). Frontend ...result spread olduğu için non-breaking.

Terminal window
# Build
pnpm next build && chown -R yigit:yigit .next/ .env
# Migration durumu
pnpm prisma migrate status
# Test
pnpm test __tests__/payment/
# DB doğrulama
PGPASSWORD=... psql -c "SELECT type, status, COUNT(*) FROM \"Payment\" GROUP BY 1,2;"
# PM2 restart
pm2 restart ecutuningportal && pm2 logs ecutuningportal --lines 50
RiskFazMitigation
LIVE 2 Stripe + 1 Wise ödeme kayıp1, 3, 7pg_dump her faz başı + Faz 3 SUM eşitlik + Faz 7 öncesi 7 gün monitor
Stripe webhook idempotency bozulur2Payment.externalRef UNIQUE + StripeEvent (mevcut)
Dashboard p99 artar4Payment yeni + boş; index’ler hazır (customerId, type+status, createdAt)
Migration prod’da takılır1, 7--create-only review + staging test
Customer view gelir sapması4Her customer için önce/sonra SUM karşılaştırma script’i
FAZ 7 erken çalışır7Taze kullanıcı onayı + derin-dogrula scorecard
FazKategoriTest çıktısı
0[docs] + [infra]backup boyutu ✓, ADR satır sayısı ✓
1[infra]prisma migrate status clean, Payment tablosu var
2Integration (Vitest+MSW)Stripe webhook → Payment yaratıldı
3[infra]SQL SUM eşitliği
4Integration + VisualDashboard önce/sonra aynı tutar + browser audit pass
5IntegrationPending payment akışı
6IntegrationEvidence merge
7E2E (Playwright)Tam ödeme akışı: cart → Stripe → Payment + audit log