You are building Meezi (میزی) — a Persian-first SaaS POS and community
platform for Iranian cafés in Tehran and Karaj.

Always read MEEZI_PRD.md at the start of any new session for full context.

## Product
Brand: Meezi (میزی) | Tagline: میزت منتظرته
Competitor: Sepidz (سپیدز) — legacy license, no SaaS, no customer app
Markets V1: Tehran (تهران) + Karaj (کرج)
Languages: Farsi fa (default) + Arabic ar + English en
Pricing: Free / Pro 1.49M ت / Business 3.49M ت / Enterprise custom
Hardware: Android tablet + thermal printer bundle

## Stack
Backend:    ASP.NET Core 10 C# — src/Meezi.API
Web:        Next.js 14 TypeScript — web/dashboard
Mobile:     Flutter 3 Dart — mobile/meezi_app
DB:         PostgreSQL 16 + Redis
ORM:        EF Core 10 (Npgsql)
Queue:      Hangfire
Realtime:   SignalR (KDS live orders)
SMS:        Kavenegar API
Payment:    ZarinPal
Maps:       Neshan API
Tax:        Taraz API (سامانه مودیان)
Delivery:   Snappfood webhook
Hosting:    Arvan Cloud Iran

## C# / ASP.NET Core Rules
- Async/await everywhere — NEVER .Result or .Wait()
- EF Core 10 only — no raw SQL unless aggregation requires it
- EVERY query: .Where(x => x.CafeId == _tenant.CafeId) — multi-tenant
- Return ApiResponse<T> always:
  record ApiResponse<T>(bool Success, T? Data, ApiError? Error = null)
  record ApiError(string Code, string Message, string? Field = null)
- Use record types for all DTOs
- FluentValidation for ALL request models
- ILogger<T> for logging — never Console.WriteLine
- Hangfire for all background jobs (SMS, coupons, renewal reminders)
- SignalR hub /hubs/kds for real-time kitchen display
- Program.cs minimal hosting style

## Next.js / TypeScript Rules
- next-intl for ALL i18n — zero hardcoded strings in components
- ALL user text in messages/fa.json + messages/ar.json + messages/en.json
- Dynamic direction: fa/ar → dir="rtl" | en → dir="ltr"
- Spacing: ms-* me-* ps-* pe-* ALWAYS — never ml-* mr-* pl-* pr-*
- TanStack Query v5 for ALL server state
- Zustand for cart + UI-only state
- Dates: date-fns-jalali ALWAYS — never display Gregorian to user
- Numbers fa: n.toLocaleString('fa-IR')
- Currency: n.toLocaleString('fa-IR') + ' ت'
- shadcn/ui components — don't rebuild what shadcn provides
- TypeScript strict — no `any`, no `as unknown`

## Flutter / Dart Rules
- Riverpod 2.x for ALL state — no setState in business logic
- GoRouter for all navigation
- Drift SQLite for offline storage (lib/core/db/)
- Sync pattern: write to Drift first → queue → upload on reconnect
- shamsi_date package for ALL date display — never show Gregorian
- 3 locales: fa (RTL), ar (RTL), en (LTR)
- Feature-first folders: lib/features/{feature}/
- Thermal printer: bluetooth_print or esc_pos_utils_plus
- QR scanner: mobile_scanner
- Dio + Retrofit for API calls
- freezed for immutable models

## Multi-Tenancy (CRITICAL)
- JWT claims: { userId, cafeId, role, planTier, lang }
- TenantMiddleware injects ITenantContext into every request
- Every EF query filters by CafeId — no exceptions
- PlanLimitMiddleware checks limits before: orders, customers, SMS
- On limit hit return: { code: "PLAN_LIMIT_REACHED", message: "..." }

## Plan Limits to enforce
Free:     50 orders/day, 1 terminal, 50 CRM, 0 SMS, 1 branch
Pro:      unlimited orders, 3 terminals, unlimited CRM, 50 SMS, 1 branch
Business: unlimited everything, 200 SMS, 5 branches + HR + delivery
Enterprise: unlimited + badges + white_label + API

## API Format
GET list:    { success: true, data: [...], meta: { total, page, pageSize } }
GET single:  { success: true, data: { ... } }
POST/PATCH:  { success: true, data: { id, ... } }
Error:       { success: false, error: { code: "...", message: "..." } }

## Endpoint Pattern
/api/cafes/{cafeId}/orders        → protected, validate cafeId == JWT cafeId
/api/public/discover              → no auth
/api/q/{qrCode}                   → no auth, returns cafeSlug + tableId
/api/webhooks/snappfood           → no JWT, verify HMAC secret
/api/auth/send-otp                → no auth, rate limit 5/hour/phone
/api/billing/verify               → ZarinPal callback

## Security
- Validate cafeId ownership: if (order.CafeId != _tenant.CafeId) return 403
- OTP rate limit: Redis INCR "otp:attempts:{phone}" with 1h TTL, block at 5
- Never log phone, nationalId, or payment tokens
- Soft delete: DeletedAt DateTime? — never hard DELETE customer data
- File upload: validate MIME + max 5MB

## i18n String Keys Convention
fa.json:
{
  "common": { "save":"ذخیره", "cancel":"انصراف", "confirm":"تأیید",
              "delete":"حذف", "search":"جستجو", "loading":"در حال بارگذاری..." },
  "pos":    { "order":"سفارش", "table":"میز", "total":"مبلغ نهایی",
              "confirmOrder":"ثبت و پرداخت", "applyСoupon":"اعمال کوپن" },
  "crm":    { "customer":"مشتری", "nationalId":"کد ملی", "phone":"موبایل" },
  "hr":     { "employee":"کارمند", "shift":"شیفت", "salary":"حقوق",
              "clockIn":"ورود", "clockOut":"خروج", "leave":"مرخصی" },
  "errors": { "planLimit":"به سقف پلن رسیده‌اید. برای ادامه ارتقا دهید",
              "notFound":"یافت نشد", "unauthorized":"دسترسی ندارید" }
}

UI QUALITY RULES — apply to every screen:

Visual hierarchy: 3 levels always
  Level 1: page title + primary action button (largest, highest contrast)
  Level 2: section headers + card titles (medium, color-coded)
  Level 3: metadata, secondary info (small, muted)

Cards: always border-radius-lg (12px), 0.5px border, white background
  Never flat boxes without border — everything lives in a card

Color system:
  Primary action: #0F6E56 (Meezi green)
  Positive/money: #0F6E56 green
  Warning/promo: #BA7517 amber
  Destructive: #A32D2D red
  Info: #0C447C blue
  Backgrounds: tertiary (page) → secondary (section) → primary (card)

Typography:
  Page titles: 18px weight 500
  Section labels: 11px UPPERCASE letter-spacing .06em muted
  Body text: 13px regular
  Prices/amounts: 13-14px weight 500 green
  Metadata: 11px muted

Status indicators:
  All orders/statuses have colored dot + badge — never plain text
  Badges: colored background matching meaning (green=active, amber=pending)

Every list row: icon or emoji + name + metadata + right-side value + action
Never a plain text list — always structured rows with visual anchors

Interactive states:
  Hover: border-color changes to primary (#0F6E56)
  Active: scale(0.98) transform
  Selected: green background tint #E1F5EE
  
Section headers above every group of items:
  "پیشنهاد ویژه امروز" / "همه آیتم‌ها" / "پرفروش‌ترین"
  Small uppercase label + optional "مشاهده همه" link

Promo tags on items with active discount:
  Small amber badge top-right of item card showing "۱۵٪ تخفیف"