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 always: record ApiResponse(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 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 "۱۵٪ تخفیف"