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: —
Bağlam (Context)
Bölüm başlığı “Bağlam (Context)”ECU Tuning Portal prisma/schema.prisma içinde dört ayrı tablo aynı gelir/ödeme alanını farklı açılardan modelliyor:
| Tablo | Amaç | Veri kaynağı |
|---|---|---|
Order | Stripe kart ödemesi (anlık checkout) | app/api/payment/webhook/route.ts (Stripe webhook) |
PendingOrder | SEPA / Crypto bekleyen kuyruk | app/api/payment/confirm-sepa/route.ts, confirm-crypto/route.ts |
Invoice | Admin tarafından kesilen yasal fatura belgesi | app/lib/admin-customer-actions.ts (manuel) + lib/invoice-generator.ts (Order/PendingOrder’dan otomatik) |
PaymentSubmission | Müşterinin Invoice’a yüklediği ödeme kanıtı | lib/payment/verify-orchestrator.ts (Wise/Crypto) |
Tespit edilen problemler
Bölüm başlığı “Tespit edilen problemler”- Çift gelir hesabı:
app/api/admin/revenue-chart/route.tsveapp/[locale]/admin/(panel)/dashboard/page.tsxüç ayrı tablodan paralel_sumaggregate ediyor +Invoice.externalTransactionIdileOrder.stripePaymentIntentarasında manuel duplicate-guard yapıyor. Bu hesap modeli kırılgan. - Tip uyumsuzluğu:
Order.id String (cuid)vsInvoice.id Int (autoincrement);Order.total Int (cents)vsInvoice.amount Decimal(10,2). Para birimi dönüşümü her aggregate’te yapılmak zorunda. - Refund asimetrisi:
Orderiçinapp/api/admin/customers/[id]/orders/[oid]/refund/route.tsvar;Invoiceiçin refund flow yok. Webhookcharge.refundedyalnızcaOrdergünceller. - Audit dağınık: Her tablonun kendi update path’i ayrı,
AdminAuditLogreferansları tutarsız string formatlarda. - Schema bağımlılığı: Refactor zorlaşıyor, yeni ödeme yöntemi eklemek (örn. PayPal) 4 tabloya da müdahale gerektiriyor.
Araştırma özeti (2026-05-15)
Bölüm başlığı “Araştırma özeti (2026-05-15)”İ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+paymentkö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.
Karar (Decision)
Bölüm başlığı “Karar (Decision)”Payment Collection Bridge pattern uygulanır. Yeni bir Payment modeli eklenir; mevcut Order, Invoice, PendingOrder, PaymentSubmission tabloları silinmez — Order ve Invoice belge olarak kalır, PendingOrder ve PaymentSubmission ilerleyen fazlarda Payment içine merge edilir.
Yeni şema
Bölüm başlığı “Yeni şema”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).
Sorumlulukların ayrımı
Bölüm başlığı “Sorumlulukların ayrımı”| Sorumluluk | Hangi tablo |
|---|---|
| Stripe checkout payload + agreement PDF | Order |
| Yasal fatura belgesi (EU/DE UStG §14) | Invoice |
| Müşteri-facing ödeme akışı state machine | PendingOrder → fazlı olarak Payment’a |
| Müşteri ödeme kanıtı (Wise/Crypto) | PaymentSubmission → fazlı olarak Payment.evidence JSON’a |
| Gerçek para hareketi, raporlama kanonu | Payment (yeni) |
Sonuçlar (Consequences)
Bölüm başlığı “Sonuçlar (Consequences)”Olumlu
Bölüm başlığı “Olumlu”- 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
Paymentiçinstatus=REFUNDEDveevidence.refundReason - Audit log tutarlı format:
Payment.reconciledByadmin ID +Payment.idreferansı - 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
Olumsuz
Bölüm başlığı “Olumsuz”- 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 Paymentyeterli, sonraki fazlarpg_dumprestore) - 10-12 oturumluk iş yükü, parça parça yapılır
Reddedilen alternatifler
Bölüm başlığı “Reddedilen alternatifler”A — Tam unified Transaction (tek tablo)
Bölüm başlığı “A — Tam unified Transaction (tek tablo)”- 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
C — Sadece PendingOrder → Order birleşmesi
Bölüm başlığı “C — Sadece PendingOrder → Order birleşmesi”- 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.
D — Yalnızca helper yaz, refactor yok
Bölüm başlığı “D — Yalnızca helper yaz, refactor yok”- 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.
Uygulama Haritası
Bölüm başlığı “Uygulama Haritası”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
İzleme metrikleri (Faz 7 sonrası)
Bölüm başlığı “İzleme metrikleri (Faz 7 sonrası)”- Dashboard p99 latency: Faz 1 öncesi baseline → Faz 4 sonrası karşılaştır
- Duplicate-guard kod karmaşıklığı: revenue-chart
route.tssatı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)
Atıflar (research)
Bölüm başlığı “Atıflar (research)”- Stripe — PaymentIntents and SetupIntents lifecycle
- Stripe — Invoicing best practices for Germany
- Stripe — E-invoicing in 2025: What German companies need to know
- Medusa — Transactions docs
- Medusa GitHub — payment_collection schema discussion
- Prisma — Expand-Contract Pattern
- Prisma — Table Inheritance / Discriminator
- Prisma — Customizing Migrations (—create-only)
- DoltHub — Polymorphic Associations 2024 anti-pattern analysis
- Stripe — Webhook idempotency