# Zizu: A delivery platform for every shop in a city, with online payments.

> The customer orders, the shop prepares, the courier delivers and the admin oversees everything, each on their own screen, with the same order updating live.

Industry: Multi-merchant delivery, Argentina · Status: In production · Developed by Ricardo Brossard
URL: https://ricardobrossard.com/en/projects/zizu/

- zizu.com.ar: https://zizu.com.ar

## By the numbers

- **152** end-to-end tests on every change, plus 57 unit tests
- **118** versioned migrations that rebuild the whole database
- **30** tables with role-based permissions, with 64 policies
- **4** stages delivered, audited and billed

## The problem

In a small city, ordering food for delivery means calling or messaging each shop separately on WhatsApp. The shop writes it down by hand, looks for someone to take the order and collects payment however it can. The customer has no idea where their order is, and nobody knows how much each shop is owed.

The brief was a single app for every shop in the city, with its own couriers and payment through Mercado Pago or in cash.

## What I built

Zizu, a delivery platform where **the customer orders, the shop prepares, the courier delivers and the admin oversees everything**, each on their own screen and watching the same order update live. I built it from scratch, end to end, and delivered it in four stages, each one audited and billed.

It is a single web application in **React 18 with Vite and TypeScript**, installable on a phone, with four areas depending on the role. Each area loads separately and is turned on with a switch per stage: the code of an area that is turned off never even reaches the browser.

The entire backend lives in **Supabase**, with 90 database functions, 5 Edge Functions and 3 scheduled jobs with pg_cron:

- **The rules live in the database, not in the screens.** Row Level Security on all 30 tables, with 64 policies: a shop cannot read another shop's orders, and a courier who is removed instantly loses access to their customers' data.
- **The order state machine runs on the server.** Pending, accepted, preparing, ready, on the way, delivered: each role can only run its own transitions, even if someone calls the API by hand.
- **Real time.** A new order rings on the shop's screen without reloading, "ready" shows up in the couriers' list and the customer sees every step.
- **The database calculates the price**, with options and extras per product. The total always comes from the server.

It also handles the day-to-day operation: batches of up to 4 orders per courier, opening hours with exceptions that expire on their own, settlements that count only what was collected, and append-only auditing on 9 tables.

## The hardest part

**Charging only once, with every peso accounted for.** The payment notice reaches an Edge Function that validates the signature, discards duplicates (Mercado Pago retries every 15 minutes), checks the payment directly with the Mercado Pago API and hands the fact to a database function that decides: credit it, reject it, record a chargeback or a refund. The notice carries; the database decides.

If the customer retries the payment or opens two tabs, they get the same charge: I store the live attempt in the database and, if it exists, Mercado Pago is not even called. I decided this after testing it, because Mercado Pago's idempotency key does not cover creating the charge. Everything ends up in an **append-only payment ledger**, protected by triggers: an entry cannot be edited or deleted, not even with direct database access, and the commission is sealed into each order.

**Getting the courier to the right door.** To turn a typed address into a point on the map, I built two layers with a cache: OpenStreetMap first and Google as a fallback. In a system like this, the dangerous failure is the false positive: a query with only a house number returned a door in another city 500 km away, and Google flagged it with its highest precision. That is why **every result is validated** against the town, the distance to the center, the street name and the house number, and if it does not pass, it moves to the next layer. No house number, no pin: a doubtful point is worse than none, because the courier trusts it. The checks are pure functions, with unit tests on the city's real street map.

## How it is tested

Every change runs on **GitHub Actions**: 57 unit tests, type checking, a build and **152 end-to-end tests with Playwright** in a real browser. They run against a separate test environment identical to production, built with the same 118 migrations. Every push to the main branch is published automatically on **Vercel**.

- **Real concurrency.** One test has two couriers take the same order at once: taking it is a single atomic operation in the database, and only one wins.
- **Payments, case by case.** Underpayment and overpayment, chargebacks, refunds that respect the valid payment, retries and the abandoned checkout that cancels itself after 30 minutes.
- **Isolated data.** The suite creates its own test data, deletes it when it finishes and fails if the real payment ledger grew during the run.

## Stack

- Application: React 18, Vite, TypeScript, Tailwind CSS, TanStack Query, Zustand, Leaflet, PWA with Workbox
- Data and backend: Supabase, PostgreSQL, Row Level Security, Realtime, Auth, Storage, Edge Functions on Deno, pg_cron
- Integrations: Mercado Pago, OpenStreetMap, Google Geocoding, Resend
- Quality and deployment: Playwright, node:test, GitHub Actions, Vercel
