From c9915a8315ba04abe17c9d28d71643852acd59fd Mon Sep 17 00:00:00 2001 From: root Date: Sun, 16 Aug 2026 22:36:05 -0400 Subject: [PATCH] remove stripe --- .../payments/payment.schemas.edge.test.ts | 2 +- .../src/modules/payments/payment.schemas.ts | 2 +- .../modules/payments/payment.service.test.ts | 38 ---- .../src/modules/payments/payment.service.ts | 21 +- .../subscription.payment-config.test.ts | 10 +- .../subscription.payment-config.ts | 4 +- .../subscription.service.edge.test.ts | 30 +-- .../subscriptions/subscription.service.ts | 64 +----- .../src/tests/api/subscriptions.api.test.ts | 6 +- .../e2e/subscriptions-public.e2e.test.ts | 2 +- .../src/app/(dashboard)/subscription/page.tsx | 202 +++++++----------- 11 files changed, 93 insertions(+), 288 deletions(-) diff --git a/apps/api/src/modules/payments/payment.schemas.edge.test.ts b/apps/api/src/modules/payments/payment.schemas.edge.test.ts index ee1b83a..8986fb0 100644 --- a/apps/api/src/modules/payments/payment.schemas.edge.test.ts +++ b/apps/api/src/modules/payments/payment.schemas.edge.test.ts @@ -8,7 +8,7 @@ describe('payment schemas edge cases', () => { type: 'CHARGE', currency: 'MAD', }) - expect(chargeSchema.safeParse({ provider: 'STRIPE', successUrl: 'https://ok.example.test', failureUrl: 'https://fail.example.test' }).success).toBe(true) + expect(chargeSchema.safeParse({ provider: 'STRIPE', successUrl: 'https://ok.example.test', failureUrl: 'https://fail.example.test' }).success).toBe(false) expect(chargeSchema.safeParse({ provider: 'UNKNOWN', successUrl: 'https://ok.example.test', failureUrl: 'https://fail.example.test' }).success).toBe(false) expect(manualPaymentSchema.parse({ amount: 500, paymentMethod: 'CASH' })).toMatchObject({ amount: 500, currency: 'MAD', type: 'CHARGE' }) expect(manualPaymentSchema.safeParse({ amount: 0, paymentMethod: 'CASH' }).success).toBe(false) diff --git a/apps/api/src/modules/payments/payment.schemas.ts b/apps/api/src/modules/payments/payment.schemas.ts index bef4dd3..6fe5edf 100644 --- a/apps/api/src/modules/payments/payment.schemas.ts +++ b/apps/api/src/modules/payments/payment.schemas.ts @@ -1,7 +1,7 @@ import { z } from 'zod' export const chargeSchema = z.object({ - provider: z.enum(['AMANPAY', 'PAYPAL', 'STRIPE']), + provider: z.enum(['AMANPAY', 'PAYPAL']), type: z.enum(['CHARGE', 'DEPOSIT']).default('CHARGE'), currency: z.literal('MAD').default('MAD'), successUrl: z.string().url(), diff --git a/apps/api/src/modules/payments/payment.service.test.ts b/apps/api/src/modules/payments/payment.service.test.ts index c216740..0fa7b32 100644 --- a/apps/api/src/modules/payments/payment.service.test.ts +++ b/apps/api/src/modules/payments/payment.service.test.ts @@ -139,44 +139,6 @@ describe('payment.service', () => { expect(repo.createPayment).not.toHaveBeenCalled() }) - it('creates a Stripe Checkout Session for an outstanding rental charge', async () => { - vi.mocked(repo.findReservationOrThrow).mockResolvedValue(reservation as never) - vi.mocked(stripe.isConfigured).mockReturnValue(true) - vi.mocked(stripe.createCheckoutSession).mockResolvedValue({ checkoutUrl: 'https://checkout.stripe.test/session', sessionId: 'cs_test_123' } as never) - vi.mocked(repo.createPayment).mockResolvedValue({ id: 'payment_1', status: 'PENDING' } as never) - - const result = await initCharge('reservation_1', 'company_1', { - provider: 'STRIPE', - type: 'CHARGE', - currency: 'MAD', - successUrl: 'https://app.example/success', - failureUrl: 'https://app.example/failure', - }) - - expect(stripe.createCheckoutSession).toHaveBeenCalledWith(expect.objectContaining({ - amount: 1000, - currency: 'MAD', - orderId: 'reservation_1-CHARGE-1780913700000', - description: 'Rental: Dacia Duster', - customerEmail: 'nora@example.com', - successUrl: 'https://app.example/success', - cancelUrl: 'https://app.example/failure', - reservationId: 'reservation_1', - companyId: 'company_1', - type: 'CHARGE', - })) - expect(repo.createPayment).toHaveBeenCalledWith(expect.objectContaining({ - companyId: 'company_1', - reservationId: 'reservation_1', - amount: 1000, - status: 'PENDING', - type: 'CHARGE', - paymentProvider: 'STRIPE', - stripeCheckoutSessionId: 'cs_test_123', - })) - expect(result).toEqual({ payment: { id: 'payment_1', status: 'PENDING' }, checkoutUrl: 'https://checkout.stripe.test/session' }) - }) - it('allows an outstanding deposit when the rental invoice is fully paid', async () => { vi.mocked(repo.findReservationOrThrow).mockResolvedValue({ ...reservation, diff --git a/apps/api/src/modules/payments/payment.service.ts b/apps/api/src/modules/payments/payment.service.ts index 5c195e8..27c2247 100644 --- a/apps/api/src/modules/payments/payment.service.ts +++ b/apps/api/src/modules/payments/payment.service.ts @@ -106,7 +106,7 @@ export async function handleStripeWebhook(event: any, rawBody: string | Buffer = } export async function initCharge(reservationId: string, companyId: string, body: { - provider: 'AMANPAY' | 'PAYPAL' | 'STRIPE'; type: 'CHARGE' | 'DEPOSIT' + provider: 'AMANPAY' | 'PAYPAL'; type: 'CHARGE' | 'DEPOSIT' currency: 'MAD'; successUrl: string; failureUrl: string }) { const reservation = await repo.findReservationOrThrow(reservationId, companyId) @@ -132,7 +132,6 @@ export async function initCharge(reservationId: string, companyId: string, body: let checkoutUrl: string let amanpayTransactionId: string | null = null let paypalCaptureId: string | null = null - let stripeCheckoutSessionId: string | null = null if (body.provider === 'AMANPAY') { if (!amanpay.isConfigured()) throw new ValidationError('AmanPay is not configured') @@ -151,24 +150,10 @@ export async function initCharge(reservationId: string, companyId: string, body: checkoutUrl = result.approveUrl paypalCaptureId = result.orderId } else { - if (!stripe.isConfigured()) throw new ValidationError('Stripe is not configured') - const result = await stripe.createCheckoutSession({ - amount, - currency: body.currency, - orderId, - description, - customerEmail: reservation.customer.email, - successUrl: body.successUrl, - cancelUrl: body.failureUrl, - reservationId, - companyId, - type: body.type, - }) - checkoutUrl = result.checkoutUrl - stripeCheckoutSessionId = result.sessionId + throw new ValidationError('Unsupported payment provider') } - const payment = await repo.createPayment({ companyId, reservationId, amount, currency: body.currency, status: 'PENDING', type: body.type, paymentProvider: body.provider, amanpayTransactionId, paypalCaptureId, stripeCheckoutSessionId }) + const payment = await repo.createPayment({ companyId, reservationId, amount, currency: body.currency, status: 'PENDING', type: body.type, paymentProvider: body.provider, amanpayTransactionId, paypalCaptureId }) return { payment, checkoutUrl } } diff --git a/apps/api/src/modules/subscriptions/subscription.payment-config.test.ts b/apps/api/src/modules/subscriptions/subscription.payment-config.test.ts index b0fdea2..57b1407 100644 --- a/apps/api/src/modules/subscriptions/subscription.payment-config.test.ts +++ b/apps/api/src/modules/subscriptions/subscription.payment-config.test.ts @@ -1,10 +1,4 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' - -vi.mock('../../services/stripeService', () => ({ - getConfigurationStatus: vi.fn(), -})) - -import { getConfigurationStatus } from '../../services/stripeService' import { getPaymentOptions } from './subscription.payment-config' const ENV_KEYS = [ @@ -26,7 +20,6 @@ describe('subscription payment configuration', () => { const originalEnv = new Map() beforeEach(() => { - vi.mocked(getConfigurationStatus).mockReturnValue({ configured: true, problems: [] }) for (const key of ENV_KEYS) originalEnv.set(key, process.env[key]) }) @@ -44,7 +37,6 @@ describe('subscription payment configuration', () => { process.env.NODE_ENV = 'development' expect(getPaymentOptions('en').methods).toEqual([ - { method: 'STRIPE', enabled: true }, { method: 'BANK_TRANSFER', enabled: true, @@ -74,6 +66,6 @@ describe('subscription payment configuration', () => { process.env.BANK_TRANSFER_ENABLED = 'true' process.env.CHECK_PAYMENT_ENABLED = 'true' - expect(getPaymentOptions('en').methods).toEqual([{ method: 'STRIPE', enabled: true }]) + expect(getPaymentOptions('en').methods).toEqual([]) }) }) diff --git a/apps/api/src/modules/subscriptions/subscription.payment-config.ts b/apps/api/src/modules/subscriptions/subscription.payment-config.ts index 3c592a4..c8a60ac 100644 --- a/apps/api/src/modules/subscriptions/subscription.payment-config.ts +++ b/apps/api/src/modules/subscriptions/subscription.payment-config.ts @@ -1,5 +1,4 @@ import { ValidationError } from '../../http/errors' -import { getConfigurationStatus as getStripeConfigurationStatus } from '../../services/stripeService' import type { NotificationLocale } from '../../services/notificationLocalizationService' export type ManualCollectionMethod = 'BANK_TRANSFER' | 'CHECK' @@ -39,8 +38,7 @@ export function paymentEvidencePipelineReady() { } export function getPaymentOptions(locale: NotificationLocale = 'en') { - const stripeStatus = getStripeConfigurationStatus() - const methods: Array> = [{ method: 'STRIPE', enabled: stripeStatus.configured }] + const methods: Array> = [] // A manual method is unusable unless evidence can be scanned and submitted. // Keeping it hidden also makes the rollout fail closed when the scanner is diff --git a/apps/api/src/modules/subscriptions/subscription.service.edge.test.ts b/apps/api/src/modules/subscriptions/subscription.service.edge.test.ts index 02fb4e1..b3a892b 100644 --- a/apps/api/src/modules/subscriptions/subscription.service.edge.test.ts +++ b/apps/api/src/modules/subscriptions/subscription.service.edge.test.ts @@ -134,14 +134,7 @@ describe('subscription.service operational edges', () => { })) }) - it('creates Stripe checkout invoices with session metadata and due dates', async () => { - vi.mocked(prisma.pricingConfig.findUnique).mockResolvedValue({ amount: 19900 } as never) - vi.mocked(prisma.company.findUniqueOrThrow).mockResolvedValue({ email: 'owner@example.test', name: 'Atlas Cars' } as never) - vi.mocked(repo.findOrCreateSubscription).mockResolvedValue({ id: 'sub_1' } as never) - vi.mocked(stripe.isConfigured).mockReturnValue(true) - vi.mocked(stripe.createCheckoutSession).mockResolvedValue({ checkoutUrl: 'https://checkout.stripe.test/session', sessionId: 'cs_test_123' } as never) - vi.mocked(manualService.createCanonicalStripeCheckoutInvoice).mockResolvedValue({ id: 'invoice_1' } as never) - + it('rejects Stripe subscription checkout before creating sessions or invoices', async () => { await expect(service.checkout('company_1', { plan: 'GROWTH', billingPeriod: 'MONTHLY', @@ -149,25 +142,10 @@ describe('subscription.service operational edges', () => { provider: 'STRIPE', successUrl: 'https://app.example.test/success', failureUrl: 'https://app.example.test/failure', - })).resolves.toEqual({ invoice: { id: 'invoice_1' }, checkoutUrl: 'https://checkout.stripe.test/session' }) + })).rejects.toThrow('Stripe subscription checkout is disabled') - expect(stripe.createCheckoutSession).toHaveBeenCalledWith(expect.objectContaining({ - amount: 19900, - currency: 'MAD', - customerEmail: 'owner@example.test', - companyId: 'company_1', - subscriptionId: 'sub_1', - type: 'SUBSCRIPTION', - })) - expect(manualService.createCanonicalStripeCheckoutInvoice).toHaveBeenCalledWith(expect.objectContaining({ - companyId: 'company_1', - subscriptionId: 'sub_1', - plan: 'GROWTH', - billingPeriod: 'MONTHLY', - amount: 19900, - stripeCheckoutSessionId: 'cs_test_123', - dueAt: new Date('2026-06-08T00:00:00.000Z'), - })) + expect(stripe.createCheckoutSession).not.toHaveBeenCalled() + expect(manualService.createCanonicalStripeCheckoutInvoice).not.toHaveBeenCalled() }) it('activates the plan purchased through Stripe instead of keeping the previous subscription plan', async () => { diff --git a/apps/api/src/modules/subscriptions/subscription.service.ts b/apps/api/src/modules/subscriptions/subscription.service.ts index b53079c..7d537c2 100644 --- a/apps/api/src/modules/subscriptions/subscription.service.ts +++ b/apps/api/src/modules/subscriptions/subscription.service.ts @@ -7,9 +7,7 @@ import * as stripe from '../../services/stripeService' import * as repo from './subscription.repo' import { SUBSCRIPTION_POLICY, getAccessLevel } from './subscription.policy' import { getWebhookEventId, processWebhookOnce } from '../../security/webhookIdempotency' -import { assertAllowedPaymentRedirect } from '../../security/paymentRedirects' import { - createCanonicalStripeCheckoutInvoice, finalizeCanonicalOnlinePayment, recordCanonicalOnlinePaymentFailure, } from './subscription.manual.service' @@ -36,10 +34,9 @@ export async function getPlans() { } export function getProviders() { - const stripeStatus = stripe.getConfigurationStatus() return { - stripe: stripeStatus.configured, - stripeProblems: stripeStatus.problems, + stripe: false, + stripeProblems: [], } } @@ -271,43 +268,7 @@ export async function checkout(companyId: string, body: { currency: 'MAD'; provider: 'STRIPE' successUrl: string; failureUrl: string }) { - const dbPrice = await prisma.pricingConfig.findUnique({ - where: { plan_billingPeriod: { plan: body.plan, billingPeriod: body.billingPeriod } }, - }) - const amount = dbPrice?.amount ?? (PLAN_PRICES[body.plan]?.[body.billingPeriod] as any)?.[body.currency] - if (!amount) throw new ValidationError('Invalid plan or billing period') - - const company = await prisma.company.findUniqueOrThrow({ where: { id: companyId } }) - const subscription = await repo.findOrCreateSubscription(companyId, body.plan, body.billingPeriod, body.currency) - - assertAllowedPaymentRedirect(body.successUrl) - assertAllowedPaymentRedirect(body.failureUrl) - - const orderId = `sub-${companyId}-${Date.now()}` - const description = `${body.plan} plan — ${body.billingPeriod}` - if (!stripe.isConfigured()) throw new ValidationError('Stripe is not configured on this platform') - const result = await stripe.createCheckoutSession({ - amount, currency: body.currency, orderId, description, - customerEmail: company.email, - successUrl: body.successUrl, - cancelUrl: body.failureUrl, - companyId, - subscriptionId: subscription.id, - type: 'SUBSCRIPTION', - }) - - const dueAt = new Date(Date.now() + SUBSCRIPTION_POLICY.payment.paymentPendingTimeoutDays * 24 * 60 * 60 * 1000) - const invoice = await createCanonicalStripeCheckoutInvoice({ - companyId, - subscriptionId: subscription.id, - plan: body.plan, - billingPeriod: body.billingPeriod, - amount, - currency: body.currency, - stripeCheckoutSessionId: result.sessionId, - dueAt, - }) - return { invoice, checkoutUrl: result.checkoutUrl } + throw new ValidationError('Stripe subscription checkout is disabled') } export async function capturePaypal(companyId: string, paypalOrderId: string) { @@ -378,24 +339,7 @@ export async function reactivate(companyId: string, body: { currency: 'MAD'; provider: 'STRIPE' successUrl: string; failureUrl: string }) { - const sub = await repo.findByCompany(companyId) - if (!sub) throw new ValidationError('No subscription found') - - const allowedStatuses = ['CANCELLED', 'EXPIRED', 'SUSPENDED', 'PAST_DUE', 'PAYMENT_PENDING'] - if (!allowedStatuses.includes(sub.status)) { - throw new ValidationError(`Cannot reactivate a subscription with status ${sub.status}`) - } - - await repo.setPaymentPending(sub.id) - await repo.createEvent({ - subscriptionId: sub.id, - companyId, - eventType: 'subscription.reactivated', - source: 'user', - payload: { plan: body.plan, billingPeriod: body.billingPeriod }, - }) - - return checkout(companyId, body) + throw new ValidationError('Stripe subscription checkout is disabled') } // ─── Scheduled job actions ──────────────────────────────────── diff --git a/apps/api/src/tests/api/subscriptions.api.test.ts b/apps/api/src/tests/api/subscriptions.api.test.ts index e160e5e..48ca53a 100644 --- a/apps/api/src/tests/api/subscriptions.api.test.ts +++ b/apps/api/src/tests/api/subscriptions.api.test.ts @@ -50,18 +50,18 @@ const app = createApp() describe('subscriptions public API', () => { beforeEach(() => { vi.clearAllMocks() - vi.mocked(service.getProviders).mockReturnValue({ stripe: true, stripeProblems: [] }) + vi.mocked(service.getProviders).mockReturnValue({ stripe: false, stripeProblems: [] }) vi.mocked(service.getPlans).mockResolvedValue({ STARTER: { MONTHLY: { MAD: 9900 } } } as never) vi.mocked(service.getPlanFeatures).mockResolvedValue([ { id: 'feature_1', plan: 'STARTER', label: 'Vehicles', sortOrder: 1 }, ] as never) }) - it('GET /api/v1/subscriptions/providers exposes configured payment provider availability', async () => { + it('GET /api/v1/subscriptions/providers reports Stripe unavailable', async () => { const res = await request(app).get('/api/v1/subscriptions/providers') expect(res.status).toBe(200) - expect(res.body).toEqual({ data: { stripe: true, stripeProblems: [] } }) + expect(res.body).toEqual({ data: { stripe: false, stripeProblems: [] } }) expect(service.getProviders).toHaveBeenCalledOnce() }) diff --git a/apps/api/src/tests/e2e/subscriptions-public.e2e.test.ts b/apps/api/src/tests/e2e/subscriptions-public.e2e.test.ts index 6041016..34c154b 100644 --- a/apps/api/src/tests/e2e/subscriptions-public.e2e.test.ts +++ b/apps/api/src/tests/e2e/subscriptions-public.e2e.test.ts @@ -53,7 +53,7 @@ describe('subscriptions public e2e smoke', () => { expect(providers.body).toEqual({ data: { stripe: false, - stripeProblems: ['STRIPE_API_KEY is missing', 'STRIPE_WEBHOOK_SECRET is missing'], + stripeProblems: [], }, }) diff --git a/apps/dashboard/src/app/(dashboard)/subscription/page.tsx b/apps/dashboard/src/app/(dashboard)/subscription/page.tsx index 833e8a0..d4222d1 100644 --- a/apps/dashboard/src/app/(dashboard)/subscription/page.tsx +++ b/apps/dashboard/src/app/(dashboard)/subscription/page.tsx @@ -112,11 +112,6 @@ interface CommunicationSettings { }> } -interface ProviderAvailability { - stripe: boolean - stripeProblems?: string[] -} - interface PlanFeature { id: string plan: Plan @@ -247,9 +242,8 @@ export default function SubscriptionPage() { const [selectedPlan, setSelectedPlan] = useState('STARTER') const [billingPeriod, setBillingPeriod] = useState('MONTHLY') const currency = 'MAD' - const [selectedMethod, setSelectedMethod] = useState('STRIPE') + const [selectedMethod, setSelectedMethod] = useState('BANK_TRANSFER') const [paymentOptions, setPaymentOptions] = useState([]) - const [providerAvailability, setProviderAvailability] = useState({ stripe: false }) const [planPrices, setPlanPrices] = useState>>>(PLAN_PRICES) const [planFeaturesList, setPlanFeaturesList] = useState([]) const [paying, setPaying] = useState(false) @@ -267,7 +261,7 @@ export default function SubscriptionPage() { const copy = { en: { title: 'Subscription', - subtitle: 'Manage your plan, Stripe payment, and subscription invoices.', + subtitle: 'Manage your plan, payment evidence, and subscription invoices.', trial: 'Free trial', remaining: 'remaining. Subscribe before it ends to keep access.', currentPlan: 'Current plan', @@ -288,8 +282,8 @@ export default function SubscriptionPage() { remainingDays: 'Remaining days', finalQuoteNote: 'Taxes and rounding are confirmed by the server quote.', subscribe: 'Subscribe', - selectPlan: 'Select a plan to continue to Stripe checkout.', - selectPayment: 'Choose Stripe, bank transfer, or check. The server calculates the final amount.', + selectPlan: 'Select a plan and submit payment evidence.', + selectPayment: 'Choose bank transfer or check. The server calculates the final amount.', bankTransfer: 'Bank transfer', check: 'Check', creatingInvoice: 'Creating invoice…', @@ -347,7 +341,7 @@ export default function SubscriptionPage() { retry: 'Retry', accessUnavailable: 'Unable to verify your access right now. Please try again.', noInvoices: 'No invoices yet.', - noProviderConfigured: 'Stripe is not configured.', + noProviderConfigured: 'No manual payment method is configured.', providerUnavailable: 'This payment provider is not configured.', statusLabels: { TRIALING: 'Trialing', ACTIVE: 'Active', PAST_DUE: 'Past due', CANCELLED: 'Cancelled', CANCELED: 'Canceled', UNPAID: 'Unpaid', EXPIRED: 'Expired', SUSPENDED: 'Suspended' } as Record, invoiceStatusLabels: { PAID: 'Paid', PENDING: 'Pending', FAILED: 'Failed', REFUNDED: 'Refunded' } as Record, @@ -360,7 +354,7 @@ export default function SubscriptionPage() { }, fr: { title: 'Abonnement', - subtitle: 'Gérez votre plan, le paiement Stripe et les factures d’abonnement.', + subtitle: 'Gérez votre plan, les justificatifs de paiement et les factures d’abonnement.', trial: 'Essai gratuit', remaining: 'restants. Abonnez-vous avant la fin pour garder l’accès.', currentPlan: 'Plan actuel', @@ -381,8 +375,8 @@ export default function SubscriptionPage() { remainingDays: 'Jours restants', finalQuoteNote: 'Les taxes et l’arrondi sont confirmés par le devis serveur.', subscribe: 'S’abonner', - selectPlan: 'Sélectionnez un plan pour continuer vers Stripe Checkout.', - selectPayment: 'Choisissez Stripe, virement bancaire ou chèque. Le serveur calcule le montant final.', + selectPlan: 'Sélectionnez un plan et envoyez un justificatif de paiement.', + selectPayment: 'Choisissez virement bancaire ou chèque. Le serveur calcule le montant final.', bankTransfer: 'Virement bancaire', check: 'Chèque', creatingInvoice: 'Création de la facture…', @@ -440,7 +434,7 @@ export default function SubscriptionPage() { retry: 'Réessayer', accessUnavailable: 'Impossible de vérifier votre accès pour le moment. Veuillez réessayer.', noInvoices: 'Aucune facture pour le moment.', - noProviderConfigured: 'Stripe n’est pas configuré.', + noProviderConfigured: 'Aucun mode de paiement manuel n’est configuré.', providerUnavailable: 'Ce prestataire de paiement n’est pas configuré.', statusLabels: { TRIALING: 'Essai', ACTIVE: 'Actif', PAST_DUE: 'En retard', CANCELLED: 'Annulé', CANCELED: 'Annulé', UNPAID: 'Impayé', EXPIRED: 'Expiré', SUSPENDED: 'Suspendu' } as Record, invoiceStatusLabels: { PAID: 'Payé', PENDING: 'En attente', FAILED: 'Échec', REFUNDED: 'Remboursé' } as Record, @@ -453,7 +447,7 @@ export default function SubscriptionPage() { }, ar: { title: 'الاشتراك', - subtitle: 'إدارة الخطة والدفع عبر Stripe وفواتير الاشتراك.', + subtitle: 'إدارة الخطة وإثباتات الدفع وفواتير الاشتراك.', trial: 'تجربة مجانية', remaining: 'متبقية. اشترك قبل انتهائها للحفاظ على الوصول.', currentPlan: 'الخطة الحالية', @@ -474,8 +468,8 @@ export default function SubscriptionPage() { remainingDays: 'الأيام المتبقية', finalQuoteNote: 'يؤكد عرض الخادم الضرائب والتقريب.', subscribe: 'اشتراك', - selectPlan: 'اختر خطة للمتابعة إلى Stripe Checkout.', - selectPayment: 'اختر Stripe أو التحويل البنكي أو الشيك. يحسب الخادم المبلغ النهائي.', + selectPlan: 'اختر خطة وأرسل إثبات الدفع.', + selectPayment: 'اختر التحويل البنكي أو الشيك. يحسب الخادم المبلغ النهائي.', bankTransfer: 'تحويل بنكي', check: 'شيك', creatingInvoice: 'جارٍ إنشاء الفاتورة…', @@ -533,7 +527,7 @@ export default function SubscriptionPage() { retry: 'إعادة المحاولة', accessUnavailable: 'تعذر التحقق من وصولك الآن. يرجى المحاولة مرة أخرى.', noInvoices: 'لا توجد فواتير حتى الآن.', - noProviderConfigured: 'Stripe غير مهيأ.', + noProviderConfigured: 'لا توجد طريقة دفع يدوية مهيأة.', providerUnavailable: 'مزوّد الدفع هذا غير مهيأ.', statusLabels: { TRIALING: 'تجريبي', ACTIVE: 'نشط', PAST_DUE: 'متأخر', CANCELLED: 'ملغى', CANCELED: 'ملغى', UNPAID: 'غير مدفوع', EXPIRED: 'منتهي', SUSPENDED: 'معلّق' } as Record, invoiceStatusLabels: { PAID: 'مدفوع', PENDING: 'قيد الانتظار', FAILED: 'فشل', REFUNDED: 'مسترد' } as Record, @@ -591,16 +585,14 @@ export default function SubscriptionPage() { Promise.all([ apiFetch('/subscriptions/me'), apiFetch('/subscriptions/invoices'), - apiFetch('/subscriptions/providers'), apiFetch<{ methods: PaymentOption[] }>('/subscriptions/payment-options'), apiFetch('/subscriptions/communication-settings'), fetchPlanData(), ]) - .then(([sub, inv, availability, options, settings]) => { - setProviderAvailability(availability) + .then(([sub, inv, options, settings]) => { setPaymentOptions(options.methods ?? []) setCommunicationSettings(settings) - const firstEnabled = options.methods?.find((option) => option.enabled) + const firstEnabled = options.methods?.find((option) => option.enabled && option.method !== 'STRIPE') if (firstEnabled) setSelectedMethod(firstEnabled.method) if (sub) { setSubscription(sub) @@ -635,14 +627,6 @@ export default function SubscriptionPage() { } }, [canViewPage, fetchPlanData]) - useEffect(() => { - if (subscription?.status !== 'ACTIVE') return - if (PLAN_RANK[selectedPlan] <= PLAN_RANK[subscription.plan]) return - if (selectedMethod !== 'STRIPE') return - const manualOption = paymentOptions.find((option) => option.enabled && option.method !== 'STRIPE') - if (manualOption) setSelectedMethod(manualOption.method) - }, [paymentOptions, selectedMethod, selectedPlan, subscription]) - if (verificationError) { return (
@@ -693,85 +677,63 @@ export default function SubscriptionPage() { if (isPlanUpgrade && billingPeriod !== subscription?.billingPeriod) { throw new Error(copy.selectHigherPlan) } - if (isPlanUpgrade && selectedMethod === 'STRIPE') { - throw new Error(copy.upgradeManualOnly) - } - if (selectedMethod !== 'STRIPE') { - if (paymentReference.trim().length < 3) throw new Error(`${manualReferenceLabel} is required`) - if (evidenceFiles.length === 0) throw new Error(manualEvidenceLabel) - const fileError = validatePaymentEvidenceFiles(evidenceFiles) - if (fileError) throw new Error(fileError) - const idempotencyKey = checkoutIdempotencyKey ?? crypto.randomUUID() - setCheckoutIdempotencyKey(idempotencyKey) - const result = isPlanUpgrade - ? await (async () => { - const quoted = await apiFetch('/subscriptions/upgrade-quotes', { - method: 'POST', - body: JSON.stringify({ targetPlan: selectedPlan, requestType: 'IMMEDIATE_PRORATED', idempotencyKey }), - }) - const accepted = await apiFetch(`/subscriptions/upgrade-requests/${quoted.request.id}/accept`, { - method: 'POST', - body: JSON.stringify({ method: selectedMethod, acceptedTermsVersion: 'subscription-upgrade-terms-v1', idempotencyKey }), - }) - if (!accepted.invoice || !accepted.instructions) throw new Error('Upgrade payment invoice was not created.') - return { invoice: accepted.invoice, instructions: accepted.instructions } - })() - : await apiFetch('/subscriptions/manual-checkout', { + if (paymentReference.trim().length < 3) throw new Error(`${manualReferenceLabel} is required`) + if (evidenceFiles.length === 0) throw new Error(manualEvidenceLabel) + const fileError = validatePaymentEvidenceFiles(evidenceFiles) + if (fileError) throw new Error(fileError) + const idempotencyKey = checkoutIdempotencyKey ?? crypto.randomUUID() + setCheckoutIdempotencyKey(idempotencyKey) + const result = isPlanUpgrade + ? await (async () => { + const quoted = await apiFetch('/subscriptions/upgrade-quotes', { method: 'POST', - body: JSON.stringify({ plan: selectedPlan, billingPeriod, currency, method: selectedMethod, idempotencyKey }), + body: JSON.stringify({ targetPlan: selectedPlan, requestType: 'IMMEDIATE_PRORATED', idempotencyKey }), }) - setManualPaymentRequestNumber(result.invoice.invoiceNumber ?? result.invoice.id) - - const key = submissionIdempotencyKey ?? crypto.randomUUID() - setSubmissionIdempotencyKey(key) - const submission = await apiFetch( - `/subscriptions/invoices/${result.invoice.id}/manual-payment-submissions`, - { + const accepted = await apiFetch(`/subscriptions/upgrade-requests/${quoted.request.id}/accept`, { + method: 'POST', + body: JSON.stringify({ method: selectedMethod, acceptedTermsVersion: 'subscription-upgrade-terms-v1', idempotencyKey }), + }) + if (!accepted.invoice || !accepted.instructions) throw new Error('Upgrade payment invoice was not created.') + return { invoice: accepted.invoice, instructions: accepted.instructions } + })() + : await apiFetch('/subscriptions/manual-checkout', { method: 'POST', - body: JSON.stringify({ method: selectedMethod, submittedReference: paymentReference, idempotencyKey: key }), - }, + body: JSON.stringify({ plan: selectedPlan, billingPeriod, currency, method: selectedMethod, idempotencyKey }), + }) + setManualPaymentRequestNumber(result.invoice.invoiceNumber ?? result.invoice.id) + + const key = submissionIdempotencyKey ?? crypto.randomUUID() + setSubmissionIdempotencyKey(key) + const submission = await apiFetch( + `/subscriptions/invoices/${result.invoice.id}/manual-payment-submissions`, + { + method: 'POST', + body: JSON.stringify({ method: selectedMethod, submittedReference: paymentReference, idempotencyKey: key }), + }, + ) + let current = submission + for (const file of evidenceFiles) { + const form = new FormData() + form.append('kind', selectedMethod === 'CHECK' ? 'CHECK_COPY' : 'BANK_TRANSFER_RECEIPT') + form.append('file', file) + const document = await apiFetch( + `/subscriptions/manual-payment-submissions/${submission.id}/documents`, + { method: 'POST', body: form }, ) - let current = submission - for (const file of evidenceFiles) { - const form = new FormData() - form.append('kind', selectedMethod === 'CHECK' ? 'CHECK_COPY' : 'BANK_TRANSFER_RECEIPT') - form.append('file', file) - const document = await apiFetch( - `/subscriptions/manual-payment-submissions/${submission.id}/documents`, - { method: 'POST', body: form }, - ) - current = { ...current, documents: [...current.documents.filter((item) => item.id !== document.id), document] } - setPaymentSubmission(current) - if (document.scanStatus !== 'CLEAN') throw new Error(`Evidence scan status: ${document.scanStatus}`) - } - const submitted = await apiFetch( - `/subscriptions/manual-payment-submissions/${submission.id}/submit`, - { method: 'POST' }, - ) - setPaymentSubmission(submitted) - setManualCheckout(null) - setEvidenceFiles([]) - setCheckoutIdempotencyKey(null) - setSubmissionIdempotencyKey(null) - setPaying(false) - return + current = { ...current, documents: [...current.documents.filter((item) => item.id !== document.id), document] } + setPaymentSubmission(current) + if (document.scanStatus !== 'CLEAN') throw new Error(`Evidence scan status: ${document.scanStatus}`) } - if (!providerAvailability.stripe) throw new Error(copy.providerUnavailable) - const currentUrl = new URL(window.location.href) - currentUrl.search = '' - currentUrl.hash = '' - const result = await apiFetch<{ checkoutUrl: string }>('/subscriptions/checkout', { - method: 'POST', - body: JSON.stringify({ - plan: selectedPlan, - billingPeriod, - currency, - provider: 'STRIPE', - successUrl: `${currentUrl.toString()}?payment=success`, - failureUrl: `${currentUrl.toString()}?payment=failed`, - }), - }) - window.location.href = result.checkoutUrl + const submitted = await apiFetch( + `/subscriptions/manual-payment-submissions/${submission.id}/submit`, + { method: 'POST' }, + ) + setPaymentSubmission(submitted) + setManualCheckout(null) + setEvidenceFiles([]) + setCheckoutIdempotencyKey(null) + setSubmissionIdempotencyKey(null) + setPaying(false) } catch (err: any) { setError(err.message) setPaying(false) @@ -896,7 +858,6 @@ export default function SubscriptionPage() { const isActiveSubscription = subscription?.status === 'ACTIVE' const isPlanUpgradeSelection = Boolean(isActiveSubscription && subscription && PLAN_RANK[selectedPlan] > PLAN_RANK[subscription.plan]) const isInvalidActiveUpgradeSelection = Boolean(isActiveSubscription && subscription && PLAN_RANK[selectedPlan] <= PLAN_RANK[subscription.plan]) - const activeUpgradeManualRequired = Boolean(isPlanUpgradeSelection && selectedMethod === 'STRIPE') const upgradeProration = buildUpgradeProrationPreview({ subscription, selectedPlan, billingPeriod, planPrices, currency }) const payableAmount = upgradeProration?.subtotal ?? planPrice const manualReferenceLabel = selectedMethod === 'CHECK' ? copy.checkNumber : copy.bankTransferReference @@ -1014,16 +975,9 @@ export default function SubscriptionPage() { {/* Plan selector + checkout */}
- {paymentOptions.length > 0 && !paymentOptions.some((option) => option.enabled) ? ( + {!paymentOptions.some((option) => option.enabled && option.method !== 'STRIPE') ? (
{copy.noProviderConfigured} - {providerAvailability.stripeProblems && providerAvailability.stripeProblems.length > 0 ? ( -
    - {providerAvailability.stripeProblems.map((problem) => ( -
  • {problem}
  • - ))} -
- ) : null}
) : null}
@@ -1034,9 +988,6 @@ export default function SubscriptionPage() { {isInvalidActiveUpgradeSelection ? (

{copy.selectHigherPlan}

) : null} - {activeUpgradeManualRequired ? ( -

{copy.upgradeManualOnly}

- ) : null}
{/* Billing period toggle */} @@ -1130,13 +1081,11 @@ export default function SubscriptionPage() {

{copy.paymentProvider}

- {paymentOptions.filter((option) => option.enabled).map((option) => ( + {paymentOptions.filter((option) => option.enabled && option.method !== 'STRIPE').map((option) => ( ))}
@@ -1174,18 +1123,15 @@ export default function SubscriptionPage() { disabled={ paying || loading - || !paymentOptions.some((option) => option.method === selectedMethod && option.enabled) + || !paymentOptions.some((option) => option.method === selectedMethod && option.enabled && option.method !== 'STRIPE') || isInvalidActiveUpgradeSelection - || activeUpgradeManualRequired || (isManualMethod && (paymentReference.trim().length < 3 || evidenceFiles.length === 0)) } className="btn-primary px-8 py-3" > {paying - ? (selectedMethod === 'STRIPE' ? copy.redirecting : copy.submittingReview) - : selectedMethod === 'STRIPE' - ? (isActiveSubscription ? copy.planUpgrade : copy.subscribeNow) - : (isActiveSubscription ? copy.planUpgrade : copy.submitPaymentEvidence)} + ? copy.submittingReview + : (isActiveSubscription ? copy.planUpgrade : copy.submitPaymentEvidence)}