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:
soroush.asadi
2026-05-27 21:35:27 +03:30
parent 42d4cb896a
commit a85890f30a
52 changed files with 3919 additions and 0 deletions
+159
View File
@@ -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 "۱۵٪ تخفیف"