Skip to content
All work

Case study

KartaTunisie

An online shop where buyers in Tunisia order game credits, gift cards and AI subscriptions, pay in dinars by bank transfer or in cash, and get their code by email once the payment is confirmed.

Role
Sole designer and developer
Type
Independent product
Timeline
2026 – today
Status
Live
  • Next.js
  • TypeScript
  • MySQL
  • Drizzle ORM
  • next-intl
  • Tailwind CSS
KartaTunisie’s French storefront on a laptop, showing gaming gift cards priced in Tunisian dinars.

Problem

Many people in Tunisia can’t pay foreign online stores with their bank card, which makes buying game credits or a ChatGPT subscription hard. KartaTunisie sells these as codes and takes payment in dinars, by bank transfer or in cash.

Ownership

I designed, built and launched KartaTunisie on my own, in French, English and Arabic: the storefront, the order flow, the admin panel, the emails, and the deployment and backups on a VPS.

Constraints

  • No card payments: buyers pay by bank transfer or in cash in Monastir, and staff check each payment before sending the code. D17 mobile payments are built but stay off until the merchant account is ready.
  • Prices are in dinars, stored as whole millimes and shown the Tunisian way: “1.250 DT” is one dinar and 250 millimes, so the shop never uses a thousands separator.
  • It runs on a small VPS shared with other sites, as a single Node.js process, with MySQL tuned for the memory available.

Architecture

KartaTunisie is a Next.js app (App Router, TypeScript) with Drizzle ORM on MySQL, and next-intl for the three languages.

The order form is validated with zod and submitted through a server action. The server rebuilds the order from the live catalog, saves it under a unique reference (retrying if a reference is already taken) and sends the confirmation emails after the response. Buyers paying by bank transfer upload their receipt, which is checked by its file signature and size before it’s stored.

In the admin panel, each staff role has its own permissions. A staff member with the right permission pastes the gift card code, which goes to the buyer by email and is never written to the database.

Deploys run from a script: copy the code, install, build, migrate the database, swap in the new folder (keeping the old one) and restart the systemd service behind Nginx. A nightly database dump keeps 14 days of backups.

Tradeoff

Admins change prices and stock from the panel while the shop is open, so the page a buyer has open can be older than the latest price. The simple option is to trust the price the form sends, but then a stale page or an edited request decides the total.

Instead, the form sends the product, the amount and the price the buyer saw. The server looks up the live price in the catalog and, if the two differ, stops the order and asks the buyer to reload the page. The buyer sees one extra step when a price changes, and never pays an amount they didn’t see.

Testing & reliability

  • 298 Vitest tests in 25 files cover pricing, money, building and validating orders, payments, receipts, order status rules, permissions and email templates. They are unit tests without a browser or database, and they all pass.
  • Orders are limited to 3 per email every 10 minutes and 10 per IP address an hour. Five failed admin logins lock that IP address for 15 minutes.
  • Every admin page and action checks the user’s permission, and an order’s status only changes if nobody else changed it first.
  • Not done yet: there is no CI pipeline and no end-to-end tests.

Outcome

KartaTunisie is live at kartatunisie.com in French, English and Arabic. Buyers can browse the catalog, order and upload a payment receipt; staff confirm payments and send codes from the admin panel.

Evidence

The screenshot below is the public product page with its order form. The code is in a private repository.

KartaTunisie’s Valorant product page in English, with amounts priced in dinars and the order form. (full-size image, opens in a new tab)
Product page and order form. The buyer picks an amount, enters their details and chooses how to pay.

More case studies