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:
@@ -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(),
|
||||
|
||||
@@ -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) };
|
||||
}
|
||||
|
||||
@@ -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: [
|
||||
|
||||
Reference in New Issue
Block a user