Skip to content

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Chinatwang ordering API

Express + Postgres backend for online ordering at chinatwang.no, with an ESC/POS receipt printer in the restaurant.

Built to be read and extended by you — every file explains why it exists, not just what it does. Start with docs/ARCHITECTURE.md, then src/routes.js.


Quick start

npm install
cp .env.example .env          # then fill in DATABASE_URL + the two tokens
npm run migrate               # create the tables
npm run seed                  # a few PLACEHOLDER menu rows
npm run dev                   # http://localhost:3000

Generate the tokens:

node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

Verify without a database:

node scripts/selftest.js      # 31 checks: validation, money, receipt rendering
node scripts/routes.js        # boots the app, lists all 22 routes

routes.js is the quickest smoke test — it imports every module, so a broken import or typo'd export fails there instead of at request time.


Where things live

src/
├── server.js              start/stop the HTTP server
├── app.js                 middleware order (read this second)
├── routes.js              THE API MAP — read this first
├── config.js              every env var, read in one place
│
├── middleware/
│   ├── validate.js        ★ the Zod middleware you asked for
│   ├── auth.js            bearer tokens for printer + staff
│   ├── errorHandler.js    one place that shapes every error response
│   ├── asyncHandler.js    makes async routes throw correctly
│   └── requestId.js       per-request id + access log
│
├── schemas/
│   ├── common.js          ★ reusable cleaners: phone, email, text, ints
│   └── order.js           the order request shapes
│
├── modules/               one folder per feature
│   ├── menu/              simplest module — read this to learn the pattern
│   ├── orders/            routes → service → repository → presenter
│   ├── printer/           queue/ack API + ticket text builder
│   └── bookings/          STUB, deliberately — rules are yours
│
├── db/pool.js             connection pool + withTransaction()
└── lib/                   AppError, logger, money

Each module is the same four files:

File Job Rule
*.routes.js HTTP in/out no business logic, no SQL
*.service.js business rules no SQL, no req/res
*.repository.js SQL no business rules
*.presenter.js DB row → JSON the public API boundary

Adding a route (the loop you'll repeat)

  1. Schema — add to src/schemas/, built from common.js cleaners
  2. Service — the actual rule, in *.service.js
  3. Repository — SQL, if the DB is involved
  4. Route — wire it up:
router.post('/thing',
  validate({ body: myThingSchema }),      // cleans + rejects bad input
  asyncHandler(async (req, res) => {
    const result = await service.doThing(req.valid.body);
    res.status(201).json({ data: result });
  })
);

Always read req.valid.body, never req.body. The first is cleaned and type-guaranteed; the second is raw internet input. Different names so you can't mix them up by accident.


Endpoints

22 routes. node scripts/routes.js prints the live list any time.

Public (no token)

Method Path Notes
GET /health pings Postgres too
GET /api/menu categories + available dishes
POST /api/orders place an order
GET /api/orders/:reference track by reference
POST /api/orders/:reference/cancel only before the kitchen starts
POST /api/bookings 501 — not implemented

Staff (Authorization: Bearer <ADMIN_TOKEN>)

Method Path Notes
GET/POST /api/menu/categories list / create
GET/PATCH/DELETE /api/menu/categories/:id delete refuses if dishes remain
GET/POST /api/menu/items list (filterable) / create
GET/PATCH/DELETE /api/menu/items/:id delete refuses if ever ordered
PATCH /api/menu/items/:id/availability one-tap "sold out tonight"
GET /api/orders kitchen list, filter by status
PATCH /api/orders/:reference/status enforces legal transitions

Printer bridge (Authorization: Bearer <PRINTER_TOKEN>)

Method Path Notes
GET /api/printer/queue claim up to limit unprinted tickets (default 5, max 20)
GET /api/printer/status queue health — watch oldestUnprintedAgeSeconds
POST /api/printer/:ref/ack paper came out; stop re-sending
POST /api/printer/:ref/nack printing failed; retry on next poll

Claims expire after PRINT_CLAIM_TIMEOUT_SECONDS (120s). A bridge that dies mid-print never acks or nacks, so without expiry its order would sit unprinted forever while the queue looked empty. Retrying risks a duplicate ticket; not retrying risks a customer who paid and got no food.

Responses are always { "data": ... } or { "error": { code, message, details } }.

Deleting is mostly refused, on purpose

DELETE on a dish that appears in past orders returns 409, because order_items references it with ON DELETE RESTRICT. Same for a category that still holds dishes. Use isAvailable: false to hide something — that is almost always what was actually wanted, and it keeps your order history and accounting intact. Cancelling an order likewise sets status = 'cancelled' rather than deleting the row.


Two rules worth not breaking

1. The client never sends prices. createOrderSchema has no price field. The server looks every price up in the database and computes the total itself. Accept a price from the browser and a customer can order dinner for 1 øre.

2. Money is integer øre, everywhere. 149,50 kr is 14950. Floats drift (0.1 + 0.2 !== 0.3); over a 30-item order that becomes a real mismatch against your accounting. See src/lib/money.js.


What I did NOT build, on purpose

  • Booking logic — opening hours, table count, sitting length and double-booking policy are business decisions. Schema, validation and route are ready; implement booking.service.js.
  • Payment — no provider chosen. Vipps/Stripe slots in as its own module.
  • The real menu — chinatwang.no/menu.html hardcodes dishes in HTML with no machine-readable prices. The seed is clearly-labelled placeholders rather than guesses at your parents' prices.
  • The printer bridge — runs in the restaurant, not on the server. Starting script in docs/PRINTER_BRIDGE.md.
  • Accounting — see the note at the bottom of src/routes.js. The data model already stores net/VAT/total and per-line snapshots, which is what Tripletex needs.

Verified working

node --check passes on every source file. scripts/selftest.js: 31/31. scripts/routes.js: all 30 routes register, every module imports cleanly.

Live HTTP checks against a running server: per-field 422 validation errors, 401 on missing/wrong tokens for all staff and printer routes, 403 on a disallowed CORS origin, 404 unknown route, 501 bookings stub, helmet security headers and X-Request-Id present, and /health correctly reporting 503 while Postgres is unreachable.

Not yet run against your Postgres. The SQL and migrations are unexercised — no instance was reachable from this machine. Point DATABASE_URL at your Pi and run npm run migrate; that is the first thing to try, and the one part of this I cannot vouch for from testing.

About

Expressjs server for managing online orders

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages