İçeriğe geç

ADR 0001 — Payment Collection Bridge Pattern

Derin
  • Status: Accepted
  • Date: 2026-05-15
  • Decision-makers: hello@ygtlabs.ai (Yiğit), Claude (assistant)
  • Supersedes:
  • Superseded-by:

ECU Tuning Portal prisma/schema.prisma içinde dört ayrı tablo aynı gelir/ödeme alanını farklı açılardan modelliyor:

TabloAmaçVeri kaynağı
OrderStripe kart ödemesi (anlık checkout)app/api/payment/webhook/route.ts (Stripe webhook)
PendingOrderSEPA / Crypto bekleyen kuyrukapp/api/payment/confirm-sepa/route.ts, confirm-crypto/route.ts
InvoiceAdmin tarafından kesilen yasal fatura belgesiapp/lib/admin-customer-actions.ts (manuel) + lib/invoice-generator.ts (Order/PendingOrder’dan otomatik)
PaymentSubmissionMüşterinin Invoice’a yüklediği ödeme kanıtılib/payment/verify-orchestrator.ts (Wise/Crypto)
  1. Çift gelir hesabı: app/api/admin/revenue-chart/route.ts ve app/[locale]/admin/(panel)/dashboard/page.tsx üç ayrı tablodan paralel _sum aggregate ediyor + Invoice.externalTransactionId ile Order.stripePaymentIntent arasında manuel duplicate-guard yapıyor. Bu hesap modeli kırılgan.
  2. Tip uyumsuzluğu: Order.id String (cuid) vs Invoice.id Int (autoincrement); Order.total Int (cents) vs Invoice.amount Decimal(10,2). Para birimi dönüşümü her aggregate’te yapılmak zorunda.
  3. Refund asimetrisi: Order için app/api/admin/customers/[id]/orders/[oid]/refund/route.ts var; Invoice için refund flow yok. Webhook charge.refunded yalnızca Order günceller.
  4. Audit dağınık: Her tablonun kendi update path’i ayrı, AdminAuditLog referansları tutarsız string formatlarda.
  5. Schema bağımlılığı: Refactor zorlaşıyor, yeni ödeme yöntemi eklemek (örn. PayPal) 4 tabloya da müdahale gerektiriyor.

İki ayrı paralel araştırma yapıldı:

Codebase haritası: 4 tablo toplam ~30 unique dosya, ~91 DB operasyonu. En yoğun bağımlılık Invoice (21 dosya, 45 işlem).

External best-practice:

  • Stripe resmi modeli: PaymentIntent ≠ Invoice — iki ayrı nesne, Invoice finalize edilince PaymentIntent üretir. Stripe bile birleştirmiyor.
  • Medusa.js, Saleor: Ayrı order + payment_collection + payment köprü pattern. GitHub açık şemalar mevcut.
  • EU/Almanya VAT (UStG §14): Her B2B işlemde ayrı yasal fatura belgesi zorunlu. Stripe PaymentIntent yeterli değil. 2025’ten itibaren e-fatura kademeli zorunlu hale geliyor.
  • Discriminator pattern (single table inheritance): SQL anti-pattern. NULL sütun patlaması, FK referential integrity kaybı, orphan record riski. Sadece küçük ekip + Stripe-only + EU VAT zorunluluğu yoksa kabul edilebilir.

Payment Collection Bridge pattern uygulanır. Yeni bir Payment modeli eklenir; mevcut Order, Invoice, PendingOrder, PaymentSubmission tabloları silinmezOrder ve Invoice belge olarak kalır, PendingOrder ve PaymentSubmission ilerleyen fazlarda Payment içine merge edilir.

model Payment {
id String @id @default(cuid())
type PaymentType
status PaymentStatus
amountCents Int
currency String @default("EUR")
customerId Int?
customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
// Köprü FK'lar — biri veya ikisi dolu; ikisi de boş yasak
orderId String? @unique
order Order? @relation(fields: [orderId], references: [id], onDelete: SetNull)
invoiceId Int?
invoice Invoice? @relation(fields: [invoiceId], references: [id], onDelete: SetNull)
externalRef String? @unique
evidence Json?
reconciledAt DateTime?
reconciledBy Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([customerId])
@@index([type, status])
@@index([createdAt])
@@index([orderId])
@@index([invoiceId])
}
enum PaymentType {
STRIPE_CARD
WISE_SEPA
CRYPTO_TRC20
MANUAL_BANK_TRANSFER
}
enum PaymentStatus {
PENDING
CONFIRMED
FAILED
REFUNDED
DISPUTED
}

Constraint: orderId IS NULL AND invoiceId IS NULL yasak (DB-level CHECK).

SorumlulukHangi tablo
Stripe checkout payload + agreement PDFOrder
Yasal fatura belgesi (EU/DE UStG §14)Invoice
Müşteri-facing ödeme akışı state machinePendingOrder → fazlı olarak Payment’a
Müşteri ödeme kanıtı (Wise/Crypto)PaymentSubmission → fazlı olarak Payment.evidence JSON’a
Gerçek para hareketi, raporlama kanonuPayment (yeni)
  • Dashboard ve raporlama tek tablodan SUM(amountCents) — duplicate-guard ihtiyacı kalkar
  • Yeni ödeme yöntemi eklemek (PayPal, Klarna) tek tabloya enum ekleyip yeni write path; Order/Invoice şeması dokunulmaz
  • Refund flow normalize: her Payment için status=REFUNDED ve evidence.refundReason
  • Audit log tutarlı format: Payment.reconciledBy admin ID + Payment.id referansı
  • EU VAT yasal güvenliği: Invoice belgesi ayrı kaldığı için Almanya UStG §14 ve 2025 e-fatura kuralları etkilenmez
  • Müşteri profili tek query: bir müşterinin tüm para hareketi Payment WHERE customerId = X
  • Geçiş döneminde dual-write yükü (Faz 2-6): her kayıt 2 tabloya yazılır, kod karmaşıklığı geçici olarak artar
  • Backfill riski: mevcut 2 LIVE Order (300 EUR + 2 EUR) + 1 LIVE Invoice (Alexandra Schmidt, 200 EUR) için backfill hatasız olmalı
  • Schema migration LIVE production’da çalışacak — rollback planı zorunlu (Faz 1 sonrası DROP TABLE Payment yeterli, sonraki fazlar pg_dump restore)
  • 10-12 oturumluk iş yükü, parça parça yapılır
  • Red sebebi: EU/DE VAT yasal kısıtı Invoice belgesinin ayrı tutulmasını zorunlu kılıyor. Tek tabloda NULL sütun patlaması olur (Stripe alanları + Wise IBAN + Crypto tx hash + Invoice items + agreement metadata). Discriminator anti-pattern dolaylı olarak FK referential integrity’sini bozar.
  • Kaynak: Stripe — E-invoicing in 2025 Germany, DoltHub — Polymorphic Associations
  • Red sebebi: Asıl ağrı noktası (dashboard’da 3-tablo duplicate-guard’lı aggregate) çözülmez. Sadece kuyruk modelini sadeleştirir, raporlama ağrısı kalır.
  • Kabul senaryosu: Eğer ileride köprü pattern ROI’sı düşük çıkarsa minimum viable refactor olarak başvurulabilir.
  • Red sebebi: Çift yazma yükü hiç başlamaz ama mevcut 4-tablo zaaflarının hepsi (duplicate-guard, refund asimetrisi, audit dağınıklığı) devam eder. Helper kısa vadeli pansumandır.
  • Kabul senaryosu: Faz 1 sonrası ROI ölçülür; köprü pattern’in faydası beklenenden düşük çıkarsa rollback yolu.

Detaylı 7-faz plan: docs/plans/payment-bridge-plan.md

  • Faz 0: pg_dump backup + bu ADR (✓ tamamlandı)
  • Faz 1: Prisma schema + migration deploy
  • Faz 2: Dual-write (Stripe webhook + Wise/Crypto submission)
  • Faz 3: Backfill (LIVE 2 Order + 1 Invoice)
  • Faz 4: Read switch (revenue-chart, dashboard, customer pages)
  • Faz 5: PendingOrder → Payment migrate
  • Faz 6: PaymentSubmission → Payment.evidence merge
  • Faz 7: Contract (eski iki ara tablo DROP; Order ve Invoice kalır) + derin-dogrula
  • Dashboard p99 latency: Faz 1 öncesi baseline → Faz 4 sonrası karşılaştır
  • Duplicate-guard kod karmaşıklığı: revenue-chart route.ts satır sayısı (önce/sonra)
  • Yeni ödeme yöntemi eklemek için dokunulan dosya sayısı (önce: 4 tablo + write path; sonra: 1 enum + 1 write path)