remove stripe
Build & Push / Pipeline Tests (push) Successful in 1m28s
Test / Type Check (all packages) (push) Successful in 32s
Build & Push / Build & Push Docker Image (push) Failing after 1m23s
Test / API Unit Tests (push) Successful in 50s
Test / Homepage Unit Tests (push) Successful in 27s
Test / Carplace Unit Tests (push) Successful in 24s
Test / Admin Unit Tests (push) Successful in 24s
Test / Dashboard Unit Tests (push) Successful in 25s
Test / API Integration Tests (push) Successful in 49s

This commit is contained in:
root
2026-08-16 22:36:05 -04:00
parent 85ff9d492b
commit c9915a8315
11 changed files with 93 additions and 288 deletions
@@ -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<Plan>('STARTER')
const [billingPeriod, setBillingPeriod] = useState<BillingPeriod>('MONTHLY')
const currency = 'MAD'
const [selectedMethod, setSelectedMethod] = useState<CollectionMethod>('STRIPE')
const [selectedMethod, setSelectedMethod] = useState<CollectionMethod>('BANK_TRANSFER')
const [paymentOptions, setPaymentOptions] = useState<PaymentOption[]>([])
const [providerAvailability, setProviderAvailability] = useState<ProviderAvailability>({ stripe: false })
const [planPrices, setPlanPrices] = useState<Record<string, Record<string, Record<string, number>>>>(PLAN_PRICES)
const [planFeaturesList, setPlanFeaturesList] = useState<PlanFeature[]>([])
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<string, string>,
invoiceStatusLabels: { PAID: 'Paid', PENDING: 'Pending', FAILED: 'Failed', REFUNDED: 'Refunded' } as Record<string, string>,
@@ -360,7 +354,7 @@ export default function SubscriptionPage() {
},
fr: {
title: 'Abonnement',
subtitle: 'Gérez votre plan, le paiement Stripe et les factures dabonnement.',
subtitle: 'Gérez votre plan, les justificatifs de paiement et les factures dabonnement.',
trial: 'Essai gratuit',
remaining: 'restants. Abonnez-vous avant la fin pour garder laccès.',
currentPlan: 'Plan actuel',
@@ -381,8 +375,8 @@ export default function SubscriptionPage() {
remainingDays: 'Jours restants',
finalQuoteNote: 'Les taxes et larrondi sont confirmés par le devis serveur.',
subscribe: 'Sabonner',
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 nest pas configuré.',
noProviderConfigured: 'Aucun mode de paiement manuel nest configuré.',
providerUnavailable: 'Ce prestataire de paiement nest pas configuré.',
statusLabels: { TRIALING: 'Essai', ACTIVE: 'Actif', PAST_DUE: 'En retard', CANCELLED: 'Annulé', CANCELED: 'Annulé', UNPAID: 'Impayé', EXPIRED: 'Expiré', SUSPENDED: 'Suspendu' } as Record<string, string>,
invoiceStatusLabels: { PAID: 'Payé', PENDING: 'En attente', FAILED: 'Échec', REFUNDED: 'Remboursé' } as Record<string, string>,
@@ -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<string, string>,
invoiceStatusLabels: { PAID: 'مدفوع', PENDING: 'قيد الانتظار', FAILED: 'فشل', REFUNDED: 'مسترد' } as Record<string, string>,
@@ -591,16 +585,14 @@ export default function SubscriptionPage() {
Promise.all([
apiFetch<Subscription | null>('/subscriptions/me'),
apiFetch<Invoice[]>('/subscriptions/invoices'),
apiFetch<ProviderAvailability>('/subscriptions/providers'),
apiFetch<{ methods: PaymentOption[] }>('/subscriptions/payment-options'),
apiFetch<CommunicationSettings>('/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 (
<div className="flex min-h-[40vh] items-center justify-center px-6">
@@ -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<UpgradeQuoteResult>('/subscriptions/upgrade-quotes', {
method: 'POST',
body: JSON.stringify({ targetPlan: selectedPlan, requestType: 'IMMEDIATE_PRORATED', idempotencyKey }),
})
const accepted = await apiFetch<UpgradeAcceptResult>(`/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<ManualCheckoutResult>('/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<UpgradeQuoteResult>('/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<ManualPaymentSubmission>(
`/subscriptions/invoices/${result.invoice.id}/manual-payment-submissions`,
{
const accepted = await apiFetch<UpgradeAcceptResult>(`/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<ManualCheckoutResult>('/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<ManualPaymentSubmission>(
`/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<ManualPaymentDocument>(
`/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<ManualPaymentDocument>(
`/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<ManualPaymentSubmission>(
`/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<ManualPaymentSubmission>(
`/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 */}
<div className="card p-6 space-y-6">
{paymentOptions.length > 0 && !paymentOptions.some((option) => option.enabled) ? (
{!paymentOptions.some((option) => option.enabled && option.method !== 'STRIPE') ? (
<div className="rounded-xl border border-orange-200 bg-orange-50 px-4 py-3 text-sm text-orange-700">
{copy.noProviderConfigured}
{providerAvailability.stripeProblems && providerAvailability.stripeProblems.length > 0 ? (
<ul className="mt-2 list-disc space-y-1 pl-5">
{providerAvailability.stripeProblems.map((problem) => (
<li key={problem}>{problem}</li>
))}
</ul>
) : null}
</div>
) : null}
<div>
@@ -1034,9 +988,6 @@ export default function SubscriptionPage() {
{isInvalidActiveUpgradeSelection ? (
<p className="mt-2 text-sm text-amber-700 dark:text-amber-300">{copy.selectHigherPlan}</p>
) : null}
{activeUpgradeManualRequired ? (
<p className="mt-2 text-sm text-amber-700 dark:text-amber-300">{copy.upgradeManualOnly}</p>
) : null}
</div>
{/* Billing period toggle */}
@@ -1130,13 +1081,11 @@ export default function SubscriptionPage() {
<div>
<p className="text-sm font-medium text-slate-700 mb-2 dark:text-zinc-300">{copy.paymentProvider}</p>
<div className="flex flex-wrap gap-3">
{paymentOptions.filter((option) => option.enabled).map((option) => (
{paymentOptions.filter((option) => option.enabled && option.method !== 'STRIPE').map((option) => (
<button
type="button"
key={option.method}
disabled={isPlanUpgradeSelection && option.method === 'STRIPE'}
onClick={() => {
if (isPlanUpgradeSelection && option.method === 'STRIPE') return
setSelectedMethod(option.method)
setManualCheckout(null)
setManualPaymentRequestNumber(null)
@@ -1148,9 +1097,9 @@ export default function SubscriptionPage() {
}}
className={`flex items-center gap-2 rounded-xl border-2 px-4 py-2.5 text-sm font-medium ${
selectedMethod === option.method ? 'border-blue-500 bg-blue-50 text-blue-700 dark:bg-blue-950/50 dark:text-blue-200' : 'border-slate-200 text-slate-600 dark:border-zinc-700 dark:text-zinc-300'
} ${isPlanUpgradeSelection && option.method === 'STRIPE' ? 'cursor-not-allowed opacity-50' : ''}`}
}`}
>
{option.method === 'STRIPE' ? 'Stripe' : option.method === 'BANK_TRANSFER' ? copy.bankTransfer : copy.check}
{option.method === 'BANK_TRANSFER' ? copy.bankTransfer : copy.check}
</button>
))}
</div>
@@ -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)}
</button>
</div>