feat(docker): multi-stage Dockerfiles with npmmirror registry
Rewrites dashboard and finder Dockerfiles to use a clean multi-stage build (deps → builder → runner) that installs npm packages inside Alpine Linux, avoiding the SWC musl binary issue when building from Windows host. Uses registry.npmmirror.com for reliable installs from restricted networks (Iran). - docker/api/Dockerfile: .NET 10 multi-stage build - docker/web/Dockerfile: Node 20-alpine multi-stage, npmmirror - docker/finder/Dockerfile: Node 20-alpine multi-stage, npmmirror - docker/website/Dockerfile: marketing website build - scripts/: PowerShell helper scripts for local dev Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,393 @@
|
||||
# Meezi — Feature Roadmap Plan
|
||||
|
||||
> **Purpose:** Implementation plan for growth, operations, integrations, platform, and quality items.
|
||||
> **Audience:** Product + engineering (solo/small team with Cursor).
|
||||
> **Conventions:** `.cursorrules`, `ApiResponse<T>`, multi-tenant `CafeId`, `messages/{fa,ar,en}.json`, plan tiers Free / Pro / Business / Enterprise.
|
||||
> **Last updated:** 2026-05-22
|
||||
|
||||
### Implementation status (started)
|
||||
|
||||
| Item | Status |
|
||||
|------|--------|
|
||||
| Q-1 `docs/SECURITY.md` | Done |
|
||||
| Q-2 Playwright (`web/dashboard/e2e/`) | Done (API smoke + discover page) |
|
||||
| Q-3 k6 `tests/load/public-abuse.js` | Done |
|
||||
| G-1 API discover filters | Done |
|
||||
| G-1 UI `/[locale]/discover` | Done (MVP) |
|
||||
| Order `DisplayNumber` (digits-only) | Done |
|
||||
| Table board SignalR (via KDS hub) | Done |
|
||||
| O-4 Terminal Redis enforcement | Done (API + settings UI) |
|
||||
| P-1 Admin split (`web/admin` + compose) | Partial (redirect from dashboard) |
|
||||
| G-1 discover detail + Neshan embed | Done |
|
||||
| G-6 Review owner reply (dashboard + public) | Done |
|
||||
| G-4 Loyalty earn on pay | Done (1 pt / 10k ت) |
|
||||
| O-1 Public queue ticket + plan gate + SMS | Done |
|
||||
| O-3 Shift close UI | Done (`/shifts`) |
|
||||
| I-2 Snappfood outbound on status/pay | Done |
|
||||
|
||||
---
|
||||
|
||||
## How to use this doc
|
||||
|
||||
- Work in **phases** (below); each phase is 2–4 weeks of focused PRs.
|
||||
- Split work into **small PRs**: `api` | `dashboard` | `mobile` | `infra` | `docs`.
|
||||
- Mark items **Built (thin)** vs **Greenfield** — don’t rebuild what already exists.
|
||||
- **Plan gates** are called out per feature; wire via `IPlatformCatalogService` + `PlanLimitMiddleware` / `IPlanLimitChecker`.
|
||||
|
||||
---
|
||||
|
||||
## Current baseline (relevant to this roadmap)
|
||||
|
||||
| Area | Already in repo |
|
||||
|------|------------------|
|
||||
| Discover profile | `CafeDiscoverProfile`, merchant/admin editor, `GET /api/public/discover`, taxonomy |
|
||||
| Reviews | `CafeReview`, public create; **`OwnerReply` on entity** — UI/public display thin |
|
||||
| Queue | `QueueController`, `queue-screen.tsx`, feature flag `queue` in seeder |
|
||||
| Loyalty | `Customer.LoyaltyPoints` field — **no earn/redeem rules or UI** |
|
||||
| Delivery | Inbound webhooks (Snappfood/Tap30/Digikala), `DeliveryStatusSyncService` — **outbound Snappfood partial** |
|
||||
| Terminals | `PlanLimits.MaxTerminals`, JWT/header patterns — **enforcement incomplete** |
|
||||
| Security | Turnstile + Redis abuse limits — **`docs/SECURITY.md` missing** |
|
||||
| Admin | `Meezi.Admin.API` exists; **admin UI still in `web/dashboard`** |
|
||||
|
||||
---
|
||||
|
||||
## Phase overview
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
P0[Phase 0\nQuality + Ops]
|
||||
P1[Phase 1\nDiscover public]
|
||||
P2[Phase 2\nGrowth CRM]
|
||||
P3[Phase 3\nOperations]
|
||||
P4[Phase 4\nIntegrations]
|
||||
P5[Phase 5\nPlatform Enterprise]
|
||||
P0 --> P1
|
||||
P1 --> P2
|
||||
P2 --> P3
|
||||
P3 --> P4
|
||||
P1 --> P4
|
||||
P4 --> P5
|
||||
```
|
||||
|
||||
| Phase | Theme | Outcome | Duration |
|
||||
|-------|--------|---------|----------|
|
||||
| **0** | Quality & ops docs | Safe public surface, CI confidence | 1–2 weeks |
|
||||
| **1** | Public discover | Consumer-facing کافهیاب for Tehran/Karaj | 2–3 weeks |
|
||||
| **2** | Growth & community | Loyalty, reviews 2.0, badges | 3–4 weeks |
|
||||
| **3** | Operations | Queue polish, printers, shifts, terminals | 3–4 weeks |
|
||||
| **4** | Integrations | Maps, delivery parity, hardware onboarding | 3–4 weeks |
|
||||
| **5** | Platform & Enterprise | Admin split, API keys, audit, export | 4–6 weeks |
|
||||
|
||||
---
|
||||
|
||||
## Phase 0 — Quality & operations (do first)
|
||||
|
||||
### Q-1 — `docs/SECURITY.md` (ops)
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| **Effort** | S (1 day) |
|
||||
| **Deliverable** | Turnstile setup, Redis limits, rate-limit table, Arvan WAF/CDN rules (OTP, `/api/public/*`, `/api/q/*`), `X-Forwarded-For`, incident checklist |
|
||||
| **Acceptance** | On-call can enable CAPTCHA and edge rules without reading source |
|
||||
|
||||
### Q-2 — Playwright E2E (dashboard)
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| **Effort** | M (3–5 days) |
|
||||
| **Scope** | `web/dashboard/e2e/`: auth OTP mock or test phone, POS happy path (table → item → pay), QR public order smoke (optional second project) |
|
||||
| **CI** | Job on PR; secrets for test DB/API |
|
||||
| **Acceptance** | 2–3 stable tests green in GitHub Actions |
|
||||
|
||||
### Q-3 — Load tests (public QR + OTP)
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| **Effort** | M (2–3 days) |
|
||||
| **Tool** | k6 or NBomber script in `tests/load/` |
|
||||
| **Scenarios** | `GET /api/q/{code}`, `GET /api/public/.../menu`, `POST` guest order, `POST /api/auth/send-otp` |
|
||||
| **Acceptance** | Document p95 targets; verify `429` / `RATE_LIMITED` under abuse |
|
||||
|
||||
### Q-4 — Package / Docker hardening
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| **Effort** | S | NU1903 bumps, `nuget.config` + Dockerfile (done), CI `docker compose build api` optional job |
|
||||
|
||||
**Phase 0 exit:** SECURITY doc published, 2+ E2E tests, load script runnable locally.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Growth: public discover homepage
|
||||
|
||||
### G-1 — Public discover web app (Tehran / Karaj)
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| **Effort** | L (1.5–2 weeks) |
|
||||
| **Plan** | Free to browse; Pro+ cafés appear when `discover_profile` filled & `IsVerified` |
|
||||
| **Route** | `web/dashboard/src/app/[locale]/(public)/discover/` **or** separate `web/discover` (lighter SEO) — recommend **public routes inside dashboard** first to reuse API client + i18n |
|
||||
| **API** | Extend `GET /api/public/discover` with filters: `city` (تهران/کرج), `themes[]`, `vibes[]`, `occasions[]`, `spaceFeatures[]`, `noise`, `priceTier`, `minRating`, `sort` (rating, distance later) |
|
||||
| **Backend** | Filter in SQL/EF on deserialized `DiscoverProfileJson` (JSONB query on PostgreSQL) or materialized columns if perf needed |
|
||||
| **UI** | Filter chips (taxonomy from `GET /api/public/discover-profile/taxonomy`), café cards (cover, rating, badges, price tier), detail page → menu link / map link |
|
||||
| **i18n** | `discoverPublic.*` in fa/ar/en |
|
||||
| **Acceptance** | User can filter “date + outdoor + کرج” and open café detail; RTL correct |
|
||||
|
||||
### G-2 — Neshan maps (discover + detail) — *can start in Phase 1 or 4*
|
||||
|
||||
See **I-1** below; for discover MVP, static map embed on detail is enough.
|
||||
|
||||
**Phase 1 exit:** Public discover listing + detail live at `/fa/discover` (or dedicated host).
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Growth & community (depth)
|
||||
|
||||
### G-3 — Customer accounts (lightweight)
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| **Effort** | L (2 weeks) |
|
||||
| **Plan** | Pro+ for “registered guests”; optional SMS OTP customer auth |
|
||||
| **Model** | `CustomerAccount` (phone PK per platform or per cafe), link to `Customer` on first order; JWT `role=customer` scoped to optional `cafeId` or global |
|
||||
| **API** | `POST /api/auth/customer/send-otp`, `verify-otp`, `GET /api/customers/me/orders`, `GET /api/customers/me/reservations` |
|
||||
| **Apps** | `meezi_app`: login, order history; discover web: “my orders” |
|
||||
| **Acceptance** | Returning guest sees past orders after OTP; no merge with staff JWT |
|
||||
|
||||
### G-4 — Loyalty points (earn / redeem)
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| **Effort** | M–L (1.5 weeks) |
|
||||
| **Built** | `Customer.LoyaltyPoints` |
|
||||
| **Rules** | `LoyaltyRule` per cafe: earn % of paid order, min redeem, expiry; Business+ feature flag `loyalty` |
|
||||
| **API** | Earn on order `Paid`; redeem as discount line on POS; `PATCH` adjust (manager) |
|
||||
| **UI** | CRM customer row, POS pay panel preview, SMS on milestone (optional) |
|
||||
| **Acceptance** | Closed order increases points; redeem reduces total with audit row |
|
||||
|
||||
### G-5 — Café badges (Enterprise)
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| **Effort** | M (1 week) |
|
||||
| **Plan** | Enterprise only; admin assigns badges |
|
||||
| **Model** | `CafeBadge` (key, labelFa, icon, assignedAt) or JSON on `Cafe` |
|
||||
| **API** | Admin CRUD; public discover DTO includes `badges[]` |
|
||||
| **UI** | Admin café detail; discover cards show badge chips |
|
||||
| **Acceptance** | Only Enterprise cafés display admin-assigned badges |
|
||||
|
||||
### G-6 — Review photos + owner responses (polish)
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| **Effort** | M (1 week) |
|
||||
| **Built** | `OwnerReply`, `OwnerRepliedAt` on `CafeReview` |
|
||||
| **Photos** | `CafeReviewPhoto` (url, sort); upload via public or authenticated; max 3 photos, 5MB, MIME validate |
|
||||
| **API** | `POST /api/public/cafes/{slug}/reviews` multipart; `PATCH /api/cafes/{cafeId}/reviews/{id}/reply` (owner) |
|
||||
| **UI** | Dashboard reviews screen: reply editor; public discover detail: reviews + photos |
|
||||
| **Moderation** | `IsHidden` flag; admin can hide (abuse) |
|
||||
| **Acceptance** | Owner reply visible on public page; photos optional |
|
||||
|
||||
**Phase 2 exit:** Loyalty + review 2.0 + badges; customer OTP optional for pilot.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Operations
|
||||
|
||||
### O-1 — Queue / waitlist (polish, not greenfield)
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| **Effort** | S–M (3–5 days) |
|
||||
| **Built** | `IQueueService`, `queue-screen.tsx` |
|
||||
| **Gaps** | Plan gate `queue` (Business+); public “take number” QR/tablet; SMS when called (Kavenegar); TV display mode (fullscreen Next page); branch-scoped boards |
|
||||
| **API** | `POST /api/public/{cafeId}/queue/tickets` (anonymous, rate limited) |
|
||||
| **Acceptance** | Walk-in gets number; staff calls next; plan limit blocks Free tier |
|
||||
|
||||
### O-2 — Kitchen printer routing per station
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| **Effort** | L (2 weeks) |
|
||||
| **Model** | `KitchenStation` (name, printerAddress/bluetoothId), `MenuCategory.StationId`, order route split on submit |
|
||||
| **API** | CRUD stations; KDS ticket includes `stationId` |
|
||||
| **Dashboard** | Settings → stations; map categories |
|
||||
| **Mobile POS** | `meezi_pos` Phase 2: `bluetooth_print` per station ticket |
|
||||
| **Acceptance** | Drink items print to bar printer; food to kitchen |
|
||||
|
||||
### O-3 — Cash drawer / shift close reports
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| **Effort** | M–L (1.5 weeks) |
|
||||
| **Model** | `CashShift` (openedAt, closedAt, openingFloat, countedCash, expectedCash, variance, userId) |
|
||||
| **API** | Open/close shift; Z-report snapshot (orders, payments, voids, discounts) |
|
||||
| **UI** | POS: “بستن شیفت”; PDF/printable summary; manager-only |
|
||||
| **HR tie-in** | Optional link to `Employee` clock-out |
|
||||
| **Acceptance** | Cannot close shift with open tables; report matches day orders |
|
||||
|
||||
### O-4 — Multi-terminal enforcement
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| **Effort** | M (1 week) |
|
||||
| **Built** | `PlanLimits.MaxTerminals`, `X-Meezi-Terminal-Id` mentioned for POS |
|
||||
| **Implementation** | Redis set `terminals:{cafeId}` with TTL; register on staff login/refresh; reject 4th terminal on Free with `PLAN_LIMIT_REACHED` |
|
||||
| **Dashboard** | Settings → active terminals list + revoke |
|
||||
| **Acceptance** | Free cafe blocked on 2nd concurrent terminal session |
|
||||
|
||||
**Phase 3 exit:** Queue production-ready; shift close; terminals enforced; printer routing MVP.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — Integrations
|
||||
|
||||
### I-1 — Neshan maps (discover + delivery radius)
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| **Effort** | M (1 week) |
|
||||
| **Config** | `Neshan:ApiKey` in platform settings / cafe settings |
|
||||
| **Discover** | Geocode café address; embed map on detail; optional “near me” sort (browser geolocation + distance) |
|
||||
| **Delivery radius** | `Cafe.DeliveryRadiusKm` + circle check for guest delivery orders (future) |
|
||||
| **Acceptance** | Map loads on café page; cities filtered Tehran/Karaj |
|
||||
|
||||
### I-2 — Snappfood outbound status updates
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| **Effort** | M (1 week) |
|
||||
| **Built** | `DeliveryStatusSyncService`, `ISnappfoodClient` |
|
||||
| **Work** | On order status → Ready/OutForDelivery/Delivered call Snappfood API; idempotent; Hangfire retry; log failures to `WebhookLog` |
|
||||
| **Dashboard** | Delivery settings: vendor id, test webhook |
|
||||
| **Acceptance** | Status change in KDS triggers outbound call when `SnappfoodOrderId` set |
|
||||
|
||||
### I-3 — Digikala / Tap30 delivery parity
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| **Effort** | L (2 weeks) |
|
||||
| **Built** | Normalizers + webhook ingress pattern |
|
||||
| **Work** | Symmetric outbound sync; commission rules; admin integration toggles; menu mapping table `DeliveryMenuMapping` |
|
||||
| **Acceptance** | Same lifecycle as Snappfood for each enabled platform |
|
||||
|
||||
### I-4 — Hardware bundle onboarding (tablet + printer)
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| **Effort** | M (1 week product + 1 week ops) |
|
||||
| **Not code-only** | SKU in admin; café `HardwareBundlePurchasedAt` |
|
||||
| **App flow** | Wizard: download POS APK, pair printer (BLE), register terminal id, test print |
|
||||
| **Docs** | PDF checklist Farsi; support ticket auto-tag `hardware` |
|
||||
| **Acceptance** | New Pro signup can complete wizard end-to-end |
|
||||
|
||||
**Phase 4 exit:** Maps on discover; delivery platforms symmetric; hardware wizard documented.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — Platform & Enterprise
|
||||
|
||||
### P-1 — Separate admin web + Compose services
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| **Effort** | L (2–3 weeks) |
|
||||
| **Work** | New `web/admin` Next app; move `src/app/[locale]/admin/**`; env `NEXT_PUBLIC_ADMIN_API_URL`; `docker-compose.admin.yml` (admin-api + admin-web); CORS split |
|
||||
| **API** | Merchant `Meezi.API` strips `/api/admin/*` when migration complete |
|
||||
| **Acceptance** | Admin users never hit merchant dashboard origin; two compose profiles documented |
|
||||
|
||||
### P-2 — API keys (Enterprise)
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| **Effort** | M (1 week) |
|
||||
| **Model** | `CafeApiKey` (hash, prefix, scopes, expiresAt, lastUsedAt) |
|
||||
| **Auth** | `Authorization: Bearer mk_...` middleware path; scopes: `orders:read`, `menu:write`, etc. |
|
||||
| **UI** | Dashboard settings (Enterprise): create/revoke keys |
|
||||
| **Acceptance** | External script can `GET` orders with key; keys tenant-scoped |
|
||||
|
||||
### P-3 — Audit log for owners
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| **Effort** | M (1 week) |
|
||||
| **Model** | `AuditEvent` (cafeId, userId, action, entityType, entityId, diffJson, ip, at) |
|
||||
| **Instrument** | Order void, refund, settings change, plan change, employee role change |
|
||||
| **UI** | Settings → audit feed; filter by date/user |
|
||||
| **Acceptance** | Owner sees who voided a line item |
|
||||
|
||||
### P-4 — Data export & GDPR-style tooling
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| **Effort** | M–L (1.5 weeks) |
|
||||
| **Export** | Hangfire job: ZIP JSON (customers, orders, reviews) per café; signed download link 24h |
|
||||
| **Delete** | Soft-delete existing; `POST /api/cafes/{id}/privacy/erase-customer` anonymize PII (phone hash, name redacted) |
|
||||
| **Retention** | Document policy in privacy page |
|
||||
| **Acceptance** | Owner can export month of CRM; erase one customer on request |
|
||||
|
||||
**Phase 5 exit:** Admin split deployed; Enterprise API keys + audit; export/erase available.
|
||||
|
||||
---
|
||||
|
||||
## Cross-cutting requirements (every phase)
|
||||
|
||||
| Rule | Action |
|
||||
|------|--------|
|
||||
| Multi-tenant | All EF queries filter `CafeId` |
|
||||
| Plans | Feature flags in `PlatformPlanDefinitions` + `IsFeatureEnabledForCafeAsync` |
|
||||
| Public abuse | Rate limit + optional Turnstile on new `POST` routes |
|
||||
| i18n | No hardcoded UI strings |
|
||||
| Tests | At least one integration test per new controller; E2E for critical UX |
|
||||
|
||||
---
|
||||
|
||||
## Suggested PR order (first 10 PRs)
|
||||
|
||||
1. `docs/SECURITY.md` (Q-1)
|
||||
2. Playwright smoke POS (Q-2)
|
||||
3. Public discover filters API + page (G-1)
|
||||
4. Discover map embed Neshan (I-1 minimal)
|
||||
5. Review reply UI + public display (G-6 partial)
|
||||
6. Loyalty earn on pay (G-4)
|
||||
7. Terminal enforcement (O-4)
|
||||
8. Queue public ticket + plan gate (O-1)
|
||||
9. Snappfood outbound hardening (I-2)
|
||||
10. Shift close MVP (O-3)
|
||||
|
||||
---
|
||||
|
||||
## Effort summary
|
||||
|
||||
| Bucket | Items | Rough total |
|
||||
|--------|-------|-------------|
|
||||
| Quality | Q-1–Q-4 | ~2 weeks |
|
||||
| Growth & community | G-1–G-6 | ~6–8 weeks |
|
||||
| Operations | O-1–O-4 | ~5–6 weeks |
|
||||
| Integrations | I-1–I-4 | ~5–6 weeks |
|
||||
| Platform | P-1–P-4 | ~6–8 weeks |
|
||||
|
||||
**Calendar (1 dev):** ~20–24 weeks sequential. **Parallel (2 dev):** Phase 1 + 0 in parallel; Phase 3 + 4 overlap after Phase 2 API stable.
|
||||
|
||||
---
|
||||
|
||||
## Out of scope (unless product changes)
|
||||
|
||||
- Full Sepidz parity
|
||||
- Native iOS/Android store apps separate from Flutter
|
||||
- Real-time ML ranking for discover (start with filter + sort)
|
||||
- Full Taraz production (see `CURRENT_STATE_FOR_PLANNING.md`)
|
||||
|
||||
---
|
||||
|
||||
## Planning prompts for Cursor
|
||||
|
||||
Copy into a session with this file attached:
|
||||
|
||||
1. *“Implement G-1 PR-1 only: extend `GET /api/public/discover` filters + tests.”*
|
||||
2. *“Implement O-4 terminal registration with Redis and PlanLimits.”*
|
||||
3. *“Scaffold `web/admin` and move admin routes from dashboard.”*
|
||||
|
||||
---
|
||||
|
||||
*End of roadmap — update phase exit criteria as items ship.*
|
||||
Reference in New Issue
Block a user