feat(payments): remove EFT/havale, Stripe-only checkout + Turkish locale

EFT/Havale was retired; Stripe is now the sole payment method. Remove the
EFT code surface (shared PaymentMethod "eft" + EftPaymentInput + eftReceiptUrl,
EFT_RECEIPT_REQUIRED error code, billing UI receipt/filter/label paths,
payments.service eft read paths). DB columns (eft_receipt_url, bank_account_id,
bank_accounts) are kept and marked @deprecated to preserve historical records
and avoid a destructive migration — same pattern as the retired iyzico column.

Faz 3 conversion lever: set locale "tr" on the Stripe Checkout session. The
audience is Turkish B2B and ~60% of sessions reached the foreign-language
hosted page but never started a payment intent (pure abandonment, not decline).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 19:17:10 +03:00
parent d6c88ede73
commit 11975d8b5a
10 changed files with 29 additions and 44 deletions

View File

@@ -184,7 +184,9 @@ export const userBrands = pgTable(
],
);
// ─── Bank Accounts (EFT/Havale destination accounts, one active at a time) ────
// ─── Bank Accounts ───────────────────────────────────
// @deprecated EFT/Havale was retired (Stripe is the sole payment method). Table
// kept only to preserve historical FK integrity; no rows are created anymore.
export const bankAccounts = pgTable(
"bank_accounts",
{
@@ -225,7 +227,9 @@ export const payments = pgTable(
iyzicoPaymentId: text("iyzico_payment_id"),
stripeSessionId: text("stripe_session_id"),
stripePaymentIntentId: text("stripe_payment_intent_id"),
/** @deprecated EFT/Havale retired (Stripe-only). Kept for historical data. */
bankAccountId: uuid("bank_account_id").references(() => bankAccounts.id),
/** @deprecated EFT/Havale retired (Stripe-only). Kept for historical data. */
eftReceiptUrl: text("eft_receipt_url"),
adminNote: text("admin_note"),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),

View File

@@ -16,9 +16,9 @@ export class PaymentsService {
/**
* Payment history for the billing page. Returns a curated projection — never
* the raw row — so internal columns (adminNote, iyzicoPaymentId,
* bankAccountId, session/intent ids) are not leaked to the client. planName
* is joined from the subscription's plan; Stripe receipt availability is
* surfaced as a boolean rather than exposing the payment intent id.
* session/intent ids) are not leaked to the client. planName is joined from
* the subscription's plan; Stripe receipt availability is surfaced as a
* boolean rather than exposing the payment intent id.
*/
async getMyPayments(userId: string) {
const rows = await this.db
@@ -29,7 +29,6 @@ export class PaymentsService {
status: payments.status,
createdAt: payments.createdAt,
planName: plans.name,
eftReceiptUrl: payments.eftReceiptUrl,
stripePaymentIntentId: payments.stripePaymentIntentId,
})
.from(payments)
@@ -45,16 +44,15 @@ export class PaymentsService {
status: row.status,
createdAt: row.createdAt,
planName: row.planName ?? null,
eftReceiptUrl: row.eftReceiptUrl ?? null,
hasStripeReceipt:
row.method === "stripe" && row.status === "completed" && !!row.stripePaymentIntentId,
}));
}
/**
* Resolve a receipt URL for one of the current user's payments.
* EFT receipts are stored locally; Stripe receipts are fetched live from the
* payment intent's latest charge. Returns { url: null } when none exists.
* Resolve a receipt URL for one of the current user's payments. Stripe is the
* sole payment method; receipts are fetched live from the payment intent's
* latest charge. Returns { url: null } when none exists.
*/
async getReceiptUrl(userId: string, paymentId: string): Promise<{ url: string | null }> {
const [payment] = await this.db
@@ -67,10 +65,6 @@ export class PaymentsService {
throw new NotFoundException("Payment not found");
}
if (payment.eftReceiptUrl) {
return { url: payment.eftReceiptUrl };
}
if (payment.method === "stripe" && payment.stripePaymentIntentId) {
return { url: await this.stripeService.getReceiptUrl(payment.stripePaymentIntentId) };
}

View File

@@ -143,6 +143,10 @@ export class StripeService {
const session = await this.stripe.checkout.sessions.create({
mode: "payment",
// Render Stripe's hosted page in Turkish. The audience is Turkish B2B; a
// foreign-language checkout is a known abandonment driver (~60% of sessions
// reached the page but never started a payment intent).
locale: "tr",
payment_method_types: ["card"],
customer_email: userEmail,
line_items: [