chore: Flutter mobile app, CI, and dev tooling
- 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>
This commit is contained in:
+159
@@ -0,0 +1,159 @@
|
||||
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 "۱۵٪ تخفیف"
|
||||
Reference in New Issue
Block a user