a85890f30a
- mobile/: Flutter/Dart merchant mobile app skeleton - .github/: GitHub Actions CI workflows - .dockerignore: exclude host node_modules from build context - .cursorrules: Cursor IDE project rules - .claude/: Claude Code project settings and launch config Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
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 "۱۵٪ تخفیف" |