159 lines
6.8 KiB
Plaintext
159 lines
6.8 KiB
Plaintext
|
|
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 "۱۵٪ تخفیف"
|