All projects
Featured case study

Checkout Payments and Student Billing

The payment step of a European e-learning checkout (Stripe, Klarna, Alma, PagoLight, SeQura), plus the billing API and student billing UI behind it.

Role
Full-stack engineer (payment step; billing API extensions; student and admin billing UI)
System
EasyPeasyFluent / Edusogno checkout, student app, and admin
A payment card routed to five payment providers

Results at a glance

  • One payment step that routes to five providers: Stripe card payments, a Klarna session flow, and redirect-based Alma, PagoLight, and SeQura.
  • A company-details step for Italian business buyers (company name, VAT number, billing address), with the step count adapting to locale and VAT choice, translated into six languages.
  • Rebuilt the student Contract & Invoices popup on the unified billing API, and exposed the same payload to support through a new admin endpoint.
  • Fixed a production 500 that blocked V2-checkout students from downloading their invoices.
  • Students who paid only a prepayment now see the rest payment, its real due date, and a Pay now link, instead of a misleading 'Installment 1 of 2'.

Overview

A course checkout in Europe isn't just "enter your card". Buy-now-pay-later providers differ by country: PagoLight in Italy, SeQura in Spain, Alma in France, and Klarna across several markets. Business buyers need a VAT invoice. Some students pay a prepayment now and the rest before the course starts.

I built the payment step of the checkout (checkout-v3), then the billing API endpoints (api-v2) and the student and admin billing screens (academy-v2, admin-v3) that make those payments visible afterwards.

The payment step

The checkout keeps its state in a Zustand store. The payment step is a small router: the selected method decides the route, and each route owns its own flow.

  • Stripe. An embedded Stripe Elements card form.
  • Klarna. A session hook creates the Klarna session. It asks for the richest payment category the session offers (pay over time, then pay later, then pay now), so students see instalments whenever Klarna allows them.
  • Alma, PagoLight, SeQura. A redirect flow. The API answers requires_action with either a redirect_url or, for SeQura, a form to render in place.

Starting a payment session is the dangerous part of a React checkout, because effects re-run. The redirect hook guards session creation with a useRef flag and an AbortController, so a re-render or a double click never opens a second session with the provider.

Later I added a company-details step for Italian business buyers: company name, VAT number, and billing information. The number of steps adapts to the user's locale and VAT choice. I also replaced loose any types in the payment hooks with explicit ones.

Billing after the checkout

Once students had paid, the platform had two billing systems side by side: legacy payments and V2 checkout orders with instalments. Students and support saw different, partial views of each.

One billing view. The team's unified billing service returns contracts and invoices (including instalments) from both systems in one payload. I rebuilt the student Contract & Invoices popup on it, typing each invoice's download descriptor ({type: legacy | v2, invoice_id}) as a discriminated union so the PDF download is routed to the right system. I then added GET /api/v2/admin/users/{id}/billing on the same service, so support sees exactly what the student sees.

The invoice download that 500'd. Students with V2-checkout purchases couldn't download invoices. The frontend posted the V2 invoice ID to the legacy endpoint, which found no legacy row and crashed with Attempt to read property "data" on null. I added a V2 download endpoint. It is scoped to the invoice owner through order → lead → user, returns 404 for foreign and unknown IDs alike, and returns 422 for unpaid invoices. It uses the same response format as the legacy endpoint, so the frontend treats both the same way.

Prepayment students. Students who paid only the prepayment saw "Installment 1 of 2" and had no way to pay the rest from the platform. The billing payload now exposes the order's payment structure, the course start date, and its canonical checkout link. The student app labels rows "Prepayment" and "Rest payment", shows a homepage card with the real due date and a Pay now button, and triggers the payment reminder on an outstanding rest invoice. Those labels are localized in all eight locales. The frontend shows nothing until the backend fields exist, so the backend could ship first safely.

Invoices. The student Contract & Invoices popup was rebuilt on the new endpoint, with a custom-invoice generator (company, address, VAT, payment reference) and currency formatting through Intl.NumberFormat.

Results

  • Five payment providers behind one checkout step, with instalments wherever the provider supports them
  • VAT-ready checkout for Italian businesses
  • The same billing view for students and support across legacy and V2 payments
  • A production invoice-download failure fixed with an ownership-scoped endpoint

What I took from it

  • Give each provider its own route in a small state machine. Don't scatter provider checks through the UI.
  • Guard session creation with a ref and an AbortController. A re-render must never start a second payment.
  • When two billing systems coexist, make every row say which one it came from, and route actions on that.
  • Ship the backend first, and make the frontend show nothing until the new fields exist.
Henry Iddirisu

Henry Iddirisu

AI product engineer · Accra, Ghana · Remote

More projects

LaravelPHPReact

IELTS Writing Corrections

Role: Full-stack engineer (API, tutor workspace, student view)

Replaced a Google-Docs correction flow with in-platform IELTS essay marking: tutors highlight and annotate spans and score four ba…

LaravelLaravel ReverbPHP

Lesson Whiteboard

Role: Full-stack engineer (sole developer across all three codebases)

A live whiteboard for online language lessons, built end to end across a Laravel API, Reverb WebSockets, the tutor app, and the st…