← Quay lại mặt cắt 01ENVITRI.DEV · SECTION A–A

A-101 · Case study · Mặt cắt 01

Banh Mi Daily

Nền tảng đặt món & vận hành cửa hàng

Sơ đồ vị trí · mặt cắt 01 / 8

Đọc nhanh trong 30 giây

Vai trò
Full-stack — đảm nhận end-to-end FE, BE, DB & tự quản lý deployment
Giao diện
Website · Kiosk tự phục vụ · POS trên tablet · Trang quản trị
Thời gian
06/2026 — Hiện tại
Stack
Nuxt 4 · Vue 3 · TypeScript · Feature-Sliced Design · TanStack Query · Zod · Ant Design Vue · PWA · Turborepo · NestJS · Prisma · PostgreSQL · JWT · RBAC · Mollie · Webhooks · Docker · Nginx · Cloudflare Pages/R2 · GitHub Actions
Liên kết

Màn hình

  • Menu trên website Banh Mi Daily: các món bán chạy và thẻ bánh mì Việt Nam kèm giá, nút giỏ hàng đang có 3 món và nút chọn ngôn ngữ đang ở tiếng Anh.
    Website: menu, cùng giỏ hàng và nút chọn ngôn ngữ.
  • Màn hình kiosk có tiêu đề Pick your dishes: lưới món ăn và đồ uống kèm giá, các tab danh mục, và thanh Go to basket đang có 1 món giá €2.95.
    Kiosk tự phục vụ: chọn món, giỏ hàng nằm ở dưới cùng.
  • POS trên tablet ở dark mode: các thẻ đơn, mỗi thẻ ghi Being prepared, Paid và Cash, kèm các nút Ready, Receipt, Cancel & refund và Refund.
    POS trên tablet: các đơn đang xử lý. Mỗi thẻ hiện trạng thái đơn (Being prepared) và trạng thái thanh toán (Paid · Cash) cạnh nhau.
  • Bảng đơn hàng trong trang quản trị gồm mã đơn, thời gian tạo, kênh, số món và tổng tiền, cùng hai cột riêng Status (Expired, Cancelled) và Payment status (Payment failed, Not paid).
    Trang quản trị: mỗi đơn có một cột Status và một cột Payment status — hai trục của state machine đơn hàng (D1).

Vấn đề

Một cửa hàng cần một nền tảng chung cho cả đặt món online lẫn vận hành tại quán — website, kiosk tự phục vụ, POS trên tablet và trang quản trị — do một developer duy nhất thiết kế, xây dựng và vận hành.

Ràng buộc

  • Developer duy nhất cho cả frontend, backend, database và deployment
  • Checkout đồng thời nhưng không có race condition
  • Thanh toán thật bằng iDEAL và thẻ
  • Website ba ngôn ngữ cho khách hàng
  • In hóa đơn trên máy in ESC/POS từ trình duyệt

Mặt cắt 01 · vị trí các quyết định

Mặt cắt qua Banh Mi Daily: từng tầng dự án chạm tới, các thành phần trên mỗi tầng và các tag quyết định D1 đến D7.
  1. D1

    State machine đơn hàng hai trục

    Trạng thái đơn được mô hình hóa thành một state machine tường minh và do API kiểm soát.

    Trade-off Mọi lần đổi trạng thái đều phải đi qua một transition khai báo sẵn, nên một trường hợp mới cần một transition mới chứ không phải sửa nhanh một field. Đổi lại, những tổ hợp không hợp lệ — như COMPLETED trước READY — không thể được ghi xuống.

  2. D2

    Idempotency key + advisory lock

    Loại bỏ race condition khi nhiều checkout đến cùng lúc.

    Trade-off Mỗi checkout lấy một lock và ghi một row vào ledger trước khi đơn được tạo. Chỉ dùng unique constraint thì đơn giản hơn, nhưng nó chỉ chặn request trùng ở bước cuối, khi cả hai request đã xử lý xong.

    Thử ngay — gửi cùng một đơn hai lần →
  3. D3

    Giá & VAT do server quyết định

    Tổng tiền và VAT do API tính, không bao giờ tin giá trị gửi từ phía client.

    Trade-off Mọi giao diện đều gọi API để lấy tổng tiền thay vì tự tính, nên quy tắc tính giá chỉ nằm ở một chỗ, nhưng mỗi lần tính là một round trip. Đổi lại, không kiosk, POS hay website nào có thể thay đổi số tiền khách phải trả.

  4. D4

    Payment webhook được verify và idempotent

    Mollie webhook được verify chữ ký, dùng để đối soát trạng thái thanh toán và xác nhận đơn.

    Trade-off Một khoản thanh toán chỉ được chốt khi webhook có chữ ký của Mollie tới, nên đơn chờ ở PENDING thay vì tin vào redirect của trình duyệt. Đổi lại, mỗi khoản thanh toán được xác nhận đúng một lần, kể cả khi Mollie retry webhook.

  5. D5

    JWT refresh token rotation + RBAC

    Session dùng refresh token rotation; quyền truy cập phân theo role.

    Trade-off Refresh token rotation nghĩa là phải lưu và thu hồi token ở server, và mọi endpoint phải khai báo các role được phép. Đổi lại, một refresh token bị đánh cắp sẽ mất hiệu lực sau một lần dùng, và nhân viên chỉ truy cập được những gì role của mình cho phép.

  6. D6

    Validate tại ranh giới API

    TanStack Query kết hợp Zod validation tại điểm dữ liệu đi vào frontend.

    Trade-off Zod schema phải được duy trì song song với type của API. Đổi lại, dữ liệu không khớp sẽ báo lỗi rõ ràng ngay tại ranh giới, thay vì làm hỏng một màn hình về sau.

  7. D7

    Pipeline xử lý ảnh

    Ảnh upload được xử lý bằng Sharp và lưu trên Cloudflare R2.

    Trade-off Ảnh upload phải qua một bước xử lý trước khi lưu, thay vì giữ nguyên file gốc. Đổi lại, mọi giao diện đều serve ảnh đã đúng kích thước và đã được nén.

Triển khai · mặt bằng tại ▽ −11.00

PLAN AT ▽ −11.00 · DELIVERY · BANH MI DAILY · 1:50PROPERTY LINE · SELF-MANAGEDBrowser4 client surfacesCloudflare Pagesstatic site hostingCloudflare R2images via SharpNginxreverse proxyNestJS API · Dockerorders · VAT · authPostgreSQL · Prismahost not documentedMollieiDEAL & cardsGitHub Actionslint · test · E2E※ Schematic of documented responsibilities. Not live traffic.
  • GitHub Actions CI chạy lint, typecheck, test và E2E test trên PostgreSQL
  • API chạy trong Docker, phía sau Nginx, trên một VPS
  • Website và ảnh được serve từ Cloudflare Pages và R2