From 45cd028d1cd5de797168e1e5ba17ea85f54a499d Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Wed, 27 May 2026 21:33:10 +0330 Subject: [PATCH] chore: initial project structure and root configuration Adds root-level config files: solution (.slnx), NuGet, global.json, Docker Compose files for all services (API, dashboard, website, finder, admin), environment example, and developer documentation. Co-Authored-By: Claude Sonnet 4.5 --- .env.example | 52 +++ .gitignore | 40 ++ DEPLOY.md | 83 +++++ DOCKER.md | 182 ++++++++++ Directory.Build.props | 10 + Directory.Packages.props | 33 ++ MEEZI_CURSOR_GUIDE.md | 762 +++++++++++++++++++++++++++++++++++++++ Meezi.slnx | 8 + README.md | 77 ++++ docker-compose.admin.yml | 59 +++ docker-compose.full.yml | 188 ++++++++++ docker-compose.yml | 154 ++++++++ global.json | 7 + nuget.config | 11 + package-lock.json | 6 + 15 files changed, 1672 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 DEPLOY.md create mode 100644 DOCKER.md create mode 100644 Directory.Build.props create mode 100644 Directory.Packages.props create mode 100644 MEEZI_CURSOR_GUIDE.md create mode 100644 Meezi.slnx create mode 100644 README.md create mode 100644 docker-compose.admin.yml create mode 100644 docker-compose.full.yml create mode 100644 docker-compose.yml create mode 100644 global.json create mode 100644 nuget.config create mode 100644 package-lock.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d14a0f8 --- /dev/null +++ b/.env.example @@ -0,0 +1,52 @@ +# Copy to .env and adjust if ports conflict on your machine: +# copy .env.example .env + +# Host ports (what you open in the browser) +WEB_PORT=3101 # Dashboard http://localhost:3101/fa/login +WEBSITE_PORT=3010 # Website http://localhost:3010/fa +ADMIN_WEB_PORT=3102 # Admin panel http://localhost:3102/fa/admin/login +API_PORT=5080 # Main API http://localhost:5080/swagger +ADMIN_API_PORT=5081 # Admin API http://localhost:5081/swagger + +# Optional: expose DB/Redis on host (for local tools). Change if already in use. +POSTGRES_PORT=5434 +REDIS_PORT=6381 + +# Browser must reach the API on the host (not Docker service names) +NEXT_PUBLIC_API_URL=http://localhost:5080 +NEXT_PUBLIC_ADMIN_API_URL=http://localhost:5081 + +# Marketing website — public URL (used for sitemap, JSON-LD, canonical) +NEXT_PUBLIC_SITE_URL=http://localhost:3010 + +# API Docker base images (if build fails — see docs/DOCKER.md) +# DOTNET_SDK_IMAGE=mcr.microsoft.com/dotnet/sdk:10.0 +# DOTNET_ASPNET_IMAGE=mcr.microsoft.com/dotnet/aspnet:10.0 + +# --- API (docker-compose / Arvan) --- +# ConnectionStrings__DefaultConnection=Host=postgres;Port=5432;Database=meezi;Username=meezi;Password=... +# ConnectionStrings__Redis=redis:6379 +# Jwt__Key=<32+ char secret> +# App__PublicBaseUrl=http://localhost:5080 +# App__QrPublicBaseUrl=http://localhost:3101 +# Billing__DashboardBaseUrl=http://localhost:3101 +# RUN_MIGRATIONS=true + +# ZarinPal (empty = mock payment in dev) +# Get your merchant ID from: https://panel.zarinpal.com → API → MerchantID +ZARINPAL_MERCHANT_ID= +ZARINPAL_SANDBOX=true + +# Snappfood webhook HMAC secret (dev default in appsettings) +# Snappfood__WebhookSecret=meezi-dev-snappfood-secret + +# Taraz / سامانه مودیان (optional; stub without cert) +# Taraz__Username= +# Taraz__Password= +# Taraz__CertificatePath= + +# Kavenegar SMS (empty = OTP logged to API console in dev) +# Kavenegar__ApiKey= + +# CORS (comma-separated origins for production) +# Cors__Origins__0=https://app.meezi.ir diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..87bf336 --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +# Dependencies +node_modules/ +web/dashboard/node_modules/ +web/dashboard/.next/ +.pnpm-store/ + +# Build outputs +.next/ +out/ +build/ +dist/ +bin/ +obj/ + +# IDE +.vs/ +*.user +*.suo +.idea/ +.cursor/ + +# Claude Code (local settings and skill data — not project code) +.claude/settings.local.json +.claude/skills/ +.config/ + +# Environment +.env* +!.env.example + +# Flutter +mobile/meezi_app/.dart_tool/ +mobile/meezi_app/build/ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log diff --git a/DEPLOY.md b/DEPLOY.md new file mode 100644 index 0000000..bf61690 --- /dev/null +++ b/DEPLOY.md @@ -0,0 +1,83 @@ +# Meezi — Production deployment (Arvan Cloud) + +## Prerequisites + +- Arvan Cloud account (Iran region) +- Domain `meezi.ir` DNS pointed to Arvan load balancer +- ZarinPal merchant ID (production, sandbox off) +- Kavenegar API key +- PostgreSQL 16 + Redis managed or VMs + +## Services + +| Service | Suggested host | +|---------|----------------| +| API | `api.meezi.ir` → ASP.NET container port 8080 | +| Dashboard | `app.meezi.ir` or `meezi.ir` → Next.js standalone | +| QR landing | `meezi.ir/q/*` → same Next.js app (`/q/[code]` route) | +| Postgres | Private network only | +| Redis | Private network only | + +## Environment variables (API) + +``` +ConnectionStrings__DefaultConnection=Host=...;Database=meezi;... +ConnectionStrings__Redis=... +Jwt__Key=<32+ char secret> +App__PublicBaseUrl=https://api.meezi.ir +App__QrPublicBaseUrl=https://meezi.ir +Billing__DashboardBaseUrl=https://app.meezi.ir +ZarinPal__MerchantId= +ZarinPal__Sandbox=false +Kavenegar__ApiKey= +Snappfood__WebhookSecret= +Taraz__Username= +Taraz__Password= +Taraz__CertificatePath=/secrets/taraz.pfx +RUN_MIGRATIONS=true +Cors__Origins__0=https://app.meezi.ir +Cors__Origins__1=https://meezi.ir +``` + +### ZarinPal / Taraz validation + +1. **Sandbox first:** set `ZarinPal__Sandbox=true` and a sandbox merchant; complete subscribe flow; confirm redirect `?billing=success` and JWT refresh on settings. +2. **Production:** set `ZarinPal__Sandbox=false` and production `ZarinPal__MerchantId`; verify callback URL is reachable from ZarinPal. +3. **Taraz:** with real credentials, submit from Settings → تاراز; confirm tracking in API logs (stub logs until full SDK wired). + +## Environment variables (Web) + +``` +NEXT_PUBLIC_API_URL=https://api.meezi.ir +``` + +## Routing + +- Customer QR codes encode `https://meezi.ir/q/{qrCode}` (see `App:QrPublicBaseUrl`). +- Next.js route [`web/dashboard/src/app/q/[code]/page.tsx`](web/dashboard/src/app/q/[code]/page.tsx) resolves via public `GET /api/q/{code}`. +- Flutter app parses scanned URL and calls the same API. + +## Arvan checklist + +- [ ] Postgres + Redis on private network (no public ports) +- [ ] `api_uploads` persistent volume mounted at `/app/uploads` +- [ ] `RUN_MIGRATIONS=true` on API deploy only +- [ ] Hangfire `/hangfire` behind VPN or basic auth +- [ ] CORS origins: dashboard + marketing domain +- [ ] `App__QrPublicBaseUrl=https://meezi.ir` +- [ ] `Billing__DashboardBaseUrl=https://app.meezi.ir` (locale path added by API) +- [ ] TLS on load balancer for `api.*` and `app.*` +- [ ] Kavenegar + ZarinPal production keys in Arvan secrets (not in git) + +## Deploy steps + +1. Build and push Docker images (`docker compose` Dockerfiles in `docker/`). +2. Run EF migrations on API startup (`RUN_MIGRATIONS=true`) once per release. +3. Configure Hangfire dashboard behind auth in production. +4. Smoke test: OTP login, POS terminal register, create order, menu images visible, ZarinPal subscribe (sandbox first). + +## CI suggestion + +- `dotnet build` + `dotnet test` on PR +- `npm run build` in `web/dashboard` +- Deploy on tag to Arvan registry diff --git a/DOCKER.md b/DOCKER.md new file mode 100644 index 0000000..f7cfe97 --- /dev/null +++ b/DOCKER.md @@ -0,0 +1,182 @@ +# Meezi — Docker (full platform) + +Run all services with one command. + +## Services + +| Service | Description | Default Port | +|---------|-------------|-------------| +| **postgres** | PostgreSQL 16 database | 5434 (host) | +| **redis** | Redis 7 (session, rate-limit, refresh tokens) | 6381 (host) | +| **api** | Main ASP.NET Core 10 API | 5080 | +| **admin-api** | Admin ASP.NET Core 10 API | 5081 | +| **web** | Customer-facing Next.js dashboard | 3101 | +| **website** | Marketing / landing website | 3010 | +| **admin-web** | Admin panel Next.js | 3102 | + +--- + +## Quick start — full stack (all 7 services) + +```powershell +cd F:\Projects\Meezi +copy .env.example .env +docker compose -f docker-compose.full.yml up -d --build +``` + +## Quick start — core stack only (dashboard + API) + +```powershell +docker compose up -d --build +``` + +## Quick start — with admin panel + +```powershell +docker compose -f docker-compose.yml -f docker-compose.admin.yml up -d --build +``` + +--- + +## Default URLs + +| Service | URL | +|---------|-----| +| Dashboard (customer) | http://localhost:**3101**/fa/login | +| Marketing website | http://localhost:**3010**/fa | +| Admin panel | http://localhost:**3102**/fa/admin/login | +| Main API Swagger | http://localhost:**5080**/swagger | +| Admin API Swagger | http://localhost:**5081**/swagger | +| Health (main API) | http://localhost:**5080**/health | +| Health (admin API) | http://localhost:**5081**/health | +| Hangfire (main API) | http://localhost:**5080**/hangfire | + +Demo login: `09121234567` — OTP appears in API logs (`docker compose logs -f api`). + +--- + +## Ports (change in `.env`) + +| Variable | Default | Purpose | +|----------|---------|---------| +| `WEB_PORT` | 3101 | Next.js dashboard | +| `WEBSITE_PORT` | 3010 | Marketing website | +| `ADMIN_WEB_PORT` | 3102 | Admin panel | +| `API_PORT` | 5080 | Main ASP.NET API | +| `ADMIN_API_PORT` | 5081 | Admin ASP.NET API | +| `POSTGRES_PORT` | 5434 | Postgres on host | +| `REDIS_PORT` | 6381 | Redis on host | + +If a port is taken, edit `.env`: +```powershell +copy .env.example .env +# Edit .env — change WEB_PORT, WEBSITE_PORT, etc. +docker compose -f docker-compose.full.yml up -d --build +``` + +--- + +## Build args and API URLs + +`NEXT_PUBLIC_API_URL` — what the **browser** uses to call the Main API. Must be a host URL (not `api:8080`). + +`MEEZI_API_URL` — what the **website server** uses for internal SSR calls. Uses the Docker service name `http://api:8080`. + +```powershell +# Rebuild only the website after changing NEXT_PUBLIC_SITE_URL: +docker compose -f docker-compose.full.yml up -d --build website +``` + +--- + +## Useful commands + +```powershell +# Status +docker compose -f docker-compose.full.yml ps + +# Logs +docker compose -f docker-compose.full.yml logs -f api +docker compose -f docker-compose.full.yml logs -f website +docker compose -f docker-compose.full.yml logs -f web + +# Stop everything +docker compose -f docker-compose.full.yml down + +# Stop and remove volumes (wipes DB!) +docker compose -f docker-compose.full.yml down -v + +# Rebuild a single service +docker compose -f docker-compose.full.yml up -d --build website +docker compose -f docker-compose.full.yml up -d --build api +``` + +--- + +## Printer setup (inside Docker) + +The Meezi API sends print jobs directly from the **browser dashboard** — the API itself does not talk to printers. This means: + +- Your thermal printer only needs to be on the **same WiFi/LAN** as the browser running the dashboard +- **No Docker-specific configuration needed** for printers +- See the full guide at: http://localhost:3010/fa/printer-guide + +--- + +## Dev without Docker + +Still works: +```powershell +docker compose up -d postgres redis +# then in separate terminals: +dotnet run --project src/Meezi.API +dotnet run --project src/Meezi.Admin.API +cd web/website && npm run dev # port 3010 +cd web/dashboard && npm run dev # port 3000 → maps to 3101 +cd web/admin && npm run dev # port 3102 +``` + +--- + +## Sprint 10 — billing & integrations (dev) + +**ZarinPal (mock):** leave `ZarinPal:MerchantId` empty. In dashboard **تنظیمات** → upgrade Pro/Business → redirected through mock pay URL back to `/fa/settings?billing=success`. + +**Snappfood webhook** (demo café vendor `demo_vendor`): + +```powershell +$body = '{"orderId":"sf-001","vendorId":"demo_vendor","customerName":"Test","customerPhone":"09121111111","total":150000,"items":[{"name":"لاته","quantity":1,"unitPrice":150000}]}' +$hmac = [System.BitConverter]::ToString((New-Object System.Security.Cryptography.HMACSHA256([Text.Encoding]::UTF8.GetBytes("meezi-dev-snappfood-secret"))).ComputeHash([Text.Encoding]::UTF8.GetBytes($body))).Replace("-","").ToLower() +Invoke-RestMethod -Method Post -Uri "http://localhost:5080/api/webhooks/snappfood" -Body $body -ContentType "application/json" -Headers @{ "X-Snappfood-Signature" = $hmac } +``` + +**Taraz:** Settings → «ارسال به تاراز» (logs only until `Taraz:Username` is set). + +**Hangfire:** renewal reminder job runs daily — dashboard at `http://localhost:5080/hangfire` (dev). + +--- + +## Tables & QR + +- Dashboard: `/fa/tables` — floor plan, add table, print QR (PNG) +- Dev QR URL in codes: `http://localhost:3101/q/{qrCode}` (see `App__QrPublicBaseUrl`) +- Scan `demo_table_01` in Flutter or open manual entry on QR screen + +--- + +## Production env (Arvan) + +See [DEPLOY.md](DEPLOY.md). Key additional website vars: + +```env +NEXT_PUBLIC_SITE_URL=https://meezi.ir +MEEZI_API_URL=http://api:8080 +``` + +--- + +## Menu images (Food-101) + +- Manifest: `data/menu-image-manifest.json` +- API upserts images on dev seed via `EnsureMenuImagesAsync` +- Optional import: `dotnet run --project tools/MenuImageImporter -- --food101 ` diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..94a4758 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,10 @@ + + + net10.0 + latest + enable + enable + false + true + + diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000..03890ac --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,33 @@ + + + 10.0.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MEEZI_CURSOR_GUIDE.md b/MEEZI_CURSOR_GUIDE.md new file mode 100644 index 0000000..7c0c367 --- /dev/null +++ b/MEEZI_CURSOR_GUIDE.md @@ -0,0 +1,762 @@ +# راهنمای کامل توسعه Meezi با Cursor +> از صفر تا اولین سفارش واقعی — گام به گام + +--- + +## پیش‌نیازها — قبل از هر چیز نصب کن + +```bash +# 1. Node.js 20+ +node --version # باید v20 یا بالاتر باشد +# دانلود: https://nodejs.org + +# 2. .NET 10 SDK +dotnet --version # باید 8.x باشد +# دانلود: https://dotnet.microsoft.com/download/dotnet/8.0 + +# 3. Flutter 3.x +flutter --version +# دانلود: https://docs.flutter.dev/get-started/install + +# 4. Docker Desktop (برای PostgreSQL و Redis محلی) +docker --version +# دانلود: https://www.docker.com/products/docker-desktop + +# 5. pnpm +npm install -g pnpm + +# 6. Cursor +# دانلود: https://cursor.com +``` + +--- + +## مرحله ۱ — ساختار پروژه را بساز + +```bash +# یک پوشه اصلی بساز +mkdir meezi && cd meezi + +# Git راه‌اندازی کن +git init +echo "node_modules/\n.next/\nbuild/\n*.user\n.env*\n!.env.example" > .gitignore +``` + +--- + +## مرحله ۲ — فایل‌های کلیدی را بریز + +### `.cursorrules` — در ریشه پروژه + +``` +You are building Meezi (میزی) — a Persian-first SaaS POS and community +platform for Iranian cafés in Tehran and Karaj. + +CONTEXT: Read MEEZI_PRD.md for full product details before any task. + +STACK: +- Backend: ASP.NET Core 10 (C#) in src/Meezi.API +- Web: Next.js 14 TypeScript in web/dashboard +- Mobile: Flutter 3 Dart in mobile/meezi_app +- DB: PostgreSQL + Redis +- ORM: Entity Framework Core 10 + +PRODUCT: +- Brand: Meezi (میزی) | میزت منتظرته +- Markets: Tehran + Karaj (V1) +- Languages: Farsi (default) + Arabic + English +- Competitor: Sepidz (legacy license, no SaaS) +- Pricing: Free / Pro 1.49M / Business 3.49M / Enterprise + +C# RULES: +- Async/await everywhere — never .Result or .Wait() +- EVERY EF query filters by CafeId (multi-tenant) +- Return ApiResponse always: { bool Success, T? Data, ApiError? Error } +- Use record types for DTOs +- FluentValidation for all inputs +- Hangfire for background jobs +- SignalR for real-time KDS +- Never Console.WriteLine — use ILogger + +NEXT.JS RULES: +- next-intl for i18n — ALL strings in messages/fa.json, ar.json, en.json +- RTL for fa/ar — LTR for en +- ms-* me-* ps-* pe-* for spacing (never ml mr pl pr) +- TanStack Query v5 for server state +- Zustand for cart and UI state +- date-fns-jalali for ALL dates — never show Gregorian +- Numbers: .toLocaleString('fa-IR') +- Currency: n.toLocaleString('fa-IR') + ' ت' +- shadcn/ui components always + +FLUTTER RULES: +- Riverpod 2 for all state +- GoRouter for navigation +- Drift SQLite for offline +- shamsi_date for all dates +- 3 locales: fa (RTL), ar (RTL), en (LTR) +- Feature-first: lib/features/{feature}/ + +SECURITY: +- Validate CafeId ownership on every protected endpoint +- Rate limit OTP: 5/hour per phone via Redis +- Never log phone, national ID, or payment tokens +- Soft delete only — never hard delete + +API FORMAT: +Success: { "success": true, "data": {...} } +Error: { "success": false, "error": { "code": "...", "message": "..." } } +List: { "success": true, "data": [...], "meta": { "total": 0, "page": 1 } } +``` + +### `docker-compose.yml` — در ریشه پروژه + +```yaml +version: '3.8' +services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_DB: meezi + POSTGRES_USER: meezi + POSTGRES_PASSWORD: meezi_local_pass + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + + redis: + image: redis:7-alpine + ports: + - "6379:6379" + +volumes: + postgres_data: +``` + +--- + +## مرحله ۳ — Backend راه‌اندازی کن + +```bash +# پوشه backend +mkdir src && cd src + +# پروژه‌های .NET بساز +dotnet new webapi -n Meezi.API --use-controllers +dotnet new classlib -n Meezi.Core +dotnet new classlib -n Meezi.Infrastructure +dotnet new classlib -n Meezi.Shared + +# Solution بساز +cd .. +dotnet new sln -n Meezi +dotnet sln add src/Meezi.API +dotnet sln add src/Meezi.Core +dotnet sln add src/Meezi.Infrastructure +dotnet sln add src/Meezi.Shared + +# Reference ها +cd src/Meezi.API +dotnet add reference ../Meezi.Core +dotnet add reference ../Meezi.Infrastructure +dotnet add reference ../Meezi.Shared + +cd ../Meezi.Infrastructure +dotnet add reference ../Meezi.Core +dotnet add reference ../Meezi.Shared +``` + +### حالا Cursor را باز کن و این prompt را بده: + +``` +I'm building Meezi.API — ASP.NET Core 10 Web API for a SaaS POS system. +Read .cursorrules for full context. + +Add these NuGet packages to Meezi.API: +- Npgsql.EntityFrameworkCore.PostgreSQL +- Microsoft.EntityFrameworkCore.Design +- FluentValidation + FluentValidation.DependencyInjectionExtensions +- AutoMapper.Extensions.Microsoft.DependencyInjection +- Hangfire.AspNetCore + Hangfire.PostgreSql +- Serilog.AspNetCore + Serilog.Sinks.Console +- StackExchange.Redis +- Microsoft.AspNetCore.Authentication.JwtBearer +- Microsoft.AspNetCore.SignalR +- QuestPDF (for PDF generation) +- EPPlus (for Excel export) +- CsvHelper + +Add these to Meezi.Infrastructure: +- Npgsql.EntityFrameworkCore.PostgreSQL +- Microsoft.EntityFrameworkCore.Tools + +Then create this folder structure inside Meezi.API: +Controllers/ Services/ Jobs/ Middleware/ Hubs/ Extensions/ + +And this in Meezi.Core: +Entities/ Enums/ Interfaces/ Constants/ + +And this in Meezi.Infrastructure: +Data/ Data/Migrations/ Repositories/ ExternalServices/ + +Create the base ApiResponse record in Meezi.Shared/ApiResponse.cs +Create Program.cs with full middleware pipeline setup. +``` + +--- + +## مرحله ۴ — Database Schema + +در Cursor بده: + +``` +Create all EF Core entity classes in Meezi.Core/Entities/ based on this schema: + +Cafe: Id(string/cuid), Name, NameAr, NameEn, Slug(unique), Phone, + Address, City, LogoUrl, PlanTier(enum), PlanExpiresAt, IsVerified, + PreferredLanguage, CreatedAt + +Branch: Id, CafeId(FK), Name, Address, City + +Table: Id, CafeId, BranchId(nullable), Number, Capacity(default 4), + Floor, QrCode(unique), IsActive + +Employee: Id, CafeId, Name, Phone, NationalId, Role(enum EmployeeRole), + BaseSalary, PinCode, CreatedAt + +MenuCategory: Id, CafeId, Name, NameAr, NameEn, SortOrder, TaxId, + DiscountPercent, IsActive + +MenuItem: Id, CafeId, CategoryId(FK), Name, NameAr, NameEn, + Description, Price, ImageUrl, IsAvailable + +Order: Id, CafeId, BranchId, TableId, CustomerId, EmployeeId, + OrderType(enum), Status(enum), CouponId, DiscountAmount, + Subtotal, TaxTotal, Total, SnappfoodOrderId, CreatedAt + +OrderItem: Id, OrderId(FK), MenuItemId(FK), Quantity, UnitPrice, Notes + +Payment: Id, OrderId(FK), Method(enum PaymentMethod), Amount, + Status(enum), Reference + +Customer: Id, CafeId, Name, Phone, NationalId, BirthDateJalali, + Group(enum CustomerGroup), LoyaltyPoints, ReferredBy, CreatedAt + +Coupon: Id, CafeId, Code, Type(enum CouponType), Value, MinOrderAmount, + MaxDiscount, UsageLimit, UsedCount, TargetGroup, + StartsAt, ExpiresAt, IsActive + +Tax: Id, CafeId, Name, Rate, IsDefault, IsRequired, IsCompound + +EmployeeSalary: Id, EmployeeId(FK), MonthYear(string YYYY-MM), + BaseSalary, OvertimePay, Deductions, NetSalary, IsPaid + +Attendance: Id, EmployeeId(FK), Date, ClockIn, ClockOut, Notes + +Shift: Id, EmployeeId(FK), DayOfWeek(int), ShiftType(enum: Morning/Evening/Off) + +LeaveRequest: Id, EmployeeId(FK), StartDate, EndDate, Reason, + Status(enum: Pending/Approved/Rejected), ReviewedBy + +Enums to create in Meezi.Core/Enums/: +PlanTier: Free, Pro, Business, Enterprise +OrderType: DineIn, Takeaway, Delivery +OrderStatus: Pending, Confirmed, Preparing, Ready, Delivered, Cancelled +PaymentMethod: Cash, Card, Credit +EmployeeRole: Owner, Manager, Cashier, Waiter, Chef, Delivery +CustomerGroup: Regular, VIP, New, Employee +CouponType: Percentage, FixedAmount, FreeItem +ShiftType: Morning, Evening, DayOff + +Then create AppDbContext in Meezi.Infrastructure/Data/AppDbContext.cs +with all DbSets and proper relationships. +Create the initial EF migration called "InitialCreate". +``` + +--- + +## مرحله ۵ — Auth System + +``` +Build the complete auth system for Meezi: + +1. In Meezi.Infrastructure/ExternalServices/KavenegarSmsService.cs: + - HTTP client calling Kavenegar REST API + - Method: SendOtpAsync(string phone, string otp) + - Read API key from IConfiguration + +2. In Meezi.API/Services/AuthService.cs: + - SendOtpAsync(string phone): + * Generate 6-digit OTP + * Store in Redis with key "otp:{phone}" TTL 5 minutes + * Store attempt counter "otp:attempts:{phone}" TTL 1 hour + * Block if attempts > 5 (return error RATE_LIMITED) + * Call KavenegarSmsService + - VerifyOtpAsync(string phone, string code, string cafeId): + * Check Redis for code + * If valid: generate JWT with claims { userId, cafeId, role, planTier, lang } + * Delete OTP from Redis + * Return token + refresh token + +3. In Meezi.API/Controllers/AuthController.cs: + POST /api/auth/send-otp → body: { phone } + POST /api/auth/verify-otp → body: { phone, code } + POST /api/auth/refresh → body: { refreshToken } + +4. In Meezi.API/Middleware/TenantMiddleware.cs: + - Extract cafeId from JWT claims + - Inject ITenantContext into request scope + - Return 401 if cafeId missing on protected routes + +5. In Meezi.API/Middleware/PlanLimitMiddleware.cs: + - Read planTier from JWT + - Check against PlanLimits constants + - Return PLAN_LIMIT_REACHED error if exceeded + +JWT claims: { sub: userId, cafeId, role, planTier, lang, iat, exp } +Token expiry: 7 days access, 30 days refresh +``` + +--- + +## مرحله ۶ — Core POS APIs + +``` +Build these API endpoints in Meezi.API/Controllers/: + +MenuController.cs: +GET /api/cafes/{cafeId}/menu/categories +POST /api/cafes/{cafeId}/menu/categories +PATCH /api/cafes/{cafeId}/menu/categories/{id} +DELETE /api/cafes/{cafeId}/menu/categories/{id} +GET /api/cafes/{cafeId}/menu/items +POST /api/cafes/{cafeId}/menu/items +PATCH /api/cafes/{cafeId}/menu/items/{id} +PATCH /api/cafes/{cafeId}/menu/items/{id}/availability + +TablesController.cs: +GET /api/cafes/{cafeId}/tables +POST /api/cafes/{cafeId}/tables → auto-generate QR code URL +GET /api/q/{qrCode} → public, returns cafeSlug + tableId + +OrdersController.cs: +GET /api/cafes/{cafeId}/orders → with status filter +POST /api/cafes/{cafeId}/orders → create + check plan limits +PATCH /api/cafes/{cafeId}/orders/{id}/status +POST /api/cafes/{cafeId}/orders/{id}/payments → record payment(s) +GET /api/cafes/{cafeId}/orders/live → SSE or SignalR for KDS + +Every controller must: +- Use [Authorize] attribute +- Validate CafeId matches JWT claim +- Use FluentValidation for request body +- Return ApiResponse format +- Use async/await throughout +``` + +--- + +## مرحله ۷ — Next.js Dashboard + +```bash +# از ریشه پروژه +mkdir web && cd web +npx create-next-app@latest dashboard --typescript --tailwind --app --src-dir +cd dashboard + +# پکیج‌های اضافی +pnpm add @tanstack/react-query zustand next-intl date-fns-jalali +pnpm add @hookform/resolvers react-hook-form zod +pnpm add recharts lucide-react +pnpm add -D @types/node +``` + +### در Cursor: + +``` +Set up the Next.js 14 dashboard for Meezi with these steps: + +1. Configure next-intl for 3 locales: fa (default, RTL), ar (RTL), en (LTR) + - Create messages/fa.json, messages/ar.json, messages/en.json + - Wrap app in [locale] dynamic route + - Set dir="rtl" for fa/ar, dir="ltr" for en + - Load Vazirmatn font for fa/ar, Inter for en + +2. Install and configure shadcn/ui: + npx shadcn-ui@latest init + Add components: button, input, card, dialog, table, select, + badge, skeleton, toast, sheet, dropdown-menu + +3. Create the app layout at app/[locale]/(dashboard)/layout.tsx: + - Sidebar with icons for: POS, CRM, Coupons, Inventory, HR, + Reports, Reservations, SMS, Taxes, Settings + - Top bar with: cafe name, plan badge, language switcher, user menu + - RTL-aware: sidebar on RIGHT for fa/ar, LEFT for en + +4. Create Zustand store at lib/stores/cart.store.ts: + State: { items: CartItem[], couponCode, appliedCoupon, tableId } + Actions: addItem, removeItem, updateQty, applyCoupon, clearCart + +5. Create API client at lib/api/client.ts: + - Axios instance with baseURL from env + - JWT interceptor (attach token from localStorage) + - 401 interceptor (redirect to login) + - Response unwrapper (extract .data from ApiResponse) + +6. Create the main POS page at app/[locale]/(dashboard)/pos/page.tsx: + - Left panel (60%): category tabs + item grid (3 cols) + - Right panel (40%): order items + coupon + tax + split payment + - All text from fa.json translation keys + - Farsi numbers everywhere + - Toman currency format +``` + +--- + +## مرحله ۸ — Flutter App + +```bash +# از ریشه پروژه +mkdir mobile && cd mobile +flutter create meezi_app --org ir.meezi --platforms android,ios,windows +cd meezi_app +``` + +### `pubspec.yaml` — جایگزین کن: + +```yaml +name: meezi_app +description: Meezi - میزی - سیستم کافه و رستوران + +environment: + sdk: '>=3.3.0 <4.0.0' + flutter: ">=3.19.0" + +dependencies: + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + + # State + flutter_riverpod: ^2.5.1 + riverpod_annotation: ^2.3.5 + + # Navigation + go_router: ^14.2.0 + + # HTTP + dio: ^5.4.3 + retrofit: ^4.3.0 + + # Local DB (offline) + drift: ^2.18.0 + sqlite3_flutter_libs: ^0.5.24 + path_provider: ^2.1.3 + path: ^1.9.0 + + # i18n + intl: ^0.19.0 + + # Shamsi date + shamsi_date: ^1.1.1 + + # QR + qr_flutter: ^4.1.0 + mobile_scanner: ^5.2.3 + + # Printer + esc_pos_utils_plus: ^2.0.1 + bluetooth_print: ^4.4.0 + + # Notifications + firebase_messaging: ^15.1.0 + flutter_local_notifications: ^17.2.3 + + # Storage + flutter_secure_storage: ^9.2.2 + shared_preferences: ^2.3.2 + + # UI + cached_network_image: ^3.3.1 + image_picker: ^1.1.2 + shimmer: ^3.0.0 + flutter_svg: ^2.0.10+1 + + # Utils + freezed_annotation: ^2.4.1 + json_annotation: ^4.9.0 + equatable: ^2.0.5 + uuid: ^4.4.2 + +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: ^2.4.11 + freezed: ^2.5.2 + json_serializable: ^6.8.0 + retrofit_generator: ^8.1.0 + riverpod_generator: ^2.4.0 + drift_dev: ^2.18.0 +``` + +### در Cursor: + +``` +Set up the Flutter app structure for Meezi following feature-first architecture. + +Create this folder structure in lib/: + app/ + router.dart → GoRouter with all routes + providers.dart → Riverpod ProviderScope setup + features/ + auth/ → OTP login screen + pos/ → Owner POS tablet view + menu/ → Customer menu browse + cart/ → Cart + checkout + track/ → Order tracking + reserve/ → Table reservation + discover/ → کافه‌یاب discovery + hr/ → Employee clock-in + leave request + profile/ → Customer profile + history + core/ + api/ + api_client.dart → Dio + Retrofit setup + interceptors.dart → JWT + error interceptors + db/ + app_database.dart → Drift database + daos/ → Data access objects + sync/ + sync_engine.dart → Offline queue + sync logic + i18n/ + app_localizations.dart + utils/ + jalali_utils.dart → shamsi_date helpers + currency_utils.dart → Toman formatting + shared/ + widgets/ + theme/ + app_theme.dart → RTL-aware theme + +Routes to create in router.dart: +/ → redirect to /discover or /pos based on user role +/login → OTP auth screen +/discover → Café discovery (public) +/cafe/:slug → Café public profile +/cafe/:slug/menu → Menu (reads tableId from QR) +/cafe/:slug/cart → Checkout +/order/:id/track → Delivery tracking +/reserve/:slug → Table reservation +/pos → Owner POS (requires OWNER/MANAGER/CASHIER role) +/profile → Customer profile + +Configure localization for fa (RTL), ar (RTL), en (LTR). +Create MaterialApp.router with locale switching support. +``` + +--- + +## مرحله ۹ — Docker و اجرای محلی + +```bash +# از ریشه پروژه +docker-compose up -d +# PostgreSQL روی port 5432 بالا می‌آید +# Redis روی port 6379 + +# Migration اجرا کن +cd src/Meezi.API +dotnet ef database update + +# Backend اجرا کن +dotnet run +# روی https://localhost:7001 بالا می‌آید + +# Dashboard اجرا کن (terminal جدید) +cd web/dashboard +pnpm dev +# روی http://localhost:3000 بالا می‌آید + +# Flutter اجرا کن (terminal جدید) +cd mobile/meezi_app +flutter run -d chrome # برای تست وب +flutter run # برای اندروید/شبیه‌ساز +``` + +--- + +## مرحله ۱۰ — Prompt های آماده برای هر ماژول + +### CRM + جستجو با کد ملی: +``` +Build the CRM module: +- GET /api/cafes/{cafeId}/customers?q={search} + Search by name, phone, or nationalId (10-digit) +- POST /api/cafes/{cafeId}/customers + Fields: name, phone, nationalId, birthDateJalali (YYYY/MM/DD), group +- Check FREE plan limit: max 50 customers → return PLAN_LIMIT_REACHED +- Next.js page: /crm with search input, customer cards, add modal +- All in Farsi with RTL layout +``` + +### HR + حضور و غیاب Flutter: +``` +Build the HR attendance feature in Flutter: +- Screen: features/hr/attendance_screen.dart +- Show employee's today shift (Morning 8-16 / Evening 16-00 / DayOff) +- Clock In button: POST /api/employees/{id}/attendance/clock-in +- Clock Out button: POST /api/employees/{id}/attendance/clock-out +- Leave request form: reason + date range in Shamsi calendar +- Works OFFLINE: queue attendance locally, sync when online +- Shamsi date display throughout +- Farsi UI with RTL layout +``` + +### KDS آشپزخانه realtime: +``` +Build the Kitchen Display System: +- SignalR Hub in Meezi.API/Hubs/KdsHub.cs + - Group orders by cafeId + - Broadcast new orders to kitchen screen + - Broadcast status changes +- Next.js page: /kds + - Connect to SignalR hub + - Show live order cards (Pending → Preparing → Ready) + - Color coded: yellow=Pending, blue=Preparing, green=Ready + - Each card: table number, items list, time elapsed + - Click to advance status + - Auto-refresh every 30s as fallback + - All Farsi, RTL layout +``` + +### گزارش مالی + Excel خروجی: +``` +Build the reports API: +GET /api/cafes/{cafeId}/reports/daily?date=1403-10-16 + Returns: totalOrders, newCustomers, returningCustomers, + revenue, taxTotal, discountTotal, topItems(5) + +GET /api/cafes/{cafeId}/reports/monthly?month=1403-10 + Returns: daily breakdown, totalRevenue, totalCosts breakdown + (salary from EmployeeSalary, plus manual cost entries) + netProfit = revenue - costs + +GET /api/cafes/{cafeId}/reports/export?month=1403-10&format=excel + Returns: Excel file using EPPlus + Sheets: Sales, Items, Customers, Employees + +Next.js page /reports: + - Stats cards: today customers (new vs returning) + - Bar chart (Recharts): 7-day revenue vs cost + - Top items list + - Monthly P&L table + - Export button + All Farsi, RTL, Toman currency, Shamsi dates +``` + +### پرداخت ZarinPal: +``` +Build ZarinPal subscription payment in C#: + +1. PaymentService.cs: + - InitiateSubscriptionAsync(cafeId, planTier, months): + * Call ZarinPal /pg/v4/payment/request.json + * Store pending payment record + * Return payment URL + - VerifyPaymentAsync(authority, status): + * Call ZarinPal /pg/v4/payment/verify.json + * On success: update cafe PlanTier + PlanExpiresAt + * Send confirmation SMS via Kavenegar + +2. BillingController.cs: + POST /api/billing/subscribe → body: { planTier, months } + → returns { paymentUrl } + GET /api/billing/verify?Authority=...&Status=OK + → ZarinPal callback, redirect to dashboard + GET /api/billing/status + → current plan, expiry, usage stats + +3. Hangfire job: SubscriptionRenewalReminderJob + - Run daily + - Find cafes expiring in 3 days + - Send SMS reminder via Kavenegar +``` + +### Snappfood Integration: +``` +Build Snappfood webhook receiver: + +POST /api/webhooks/snappfood (no auth, verify with HMAC secret) + - Parse Snappfood order payload + - Map to Meezi Order: + * OrderType = Delivery + * Status = Confirmed (already paid via Snappfood) + * SnappfoodOrderId = external ID + * Items mapped from Snappfood items to MenuItems by name match + - Save order to DB + - Broadcast to KDS via SignalR + - Return 200 OK within 5 seconds + +Also: +PATCH /api/cafes/{cafeId}/orders/{id}/status + - When status → Delivered: notify Snappfood API if SnappfoodOrderId exists +``` + +--- + +## مرحله ۱۱ — اشتباهات رایج که باید از آن‌ها بپرهیزی + +``` +❌ نکن: +- Console.WriteLine در C# → از ILogger استفاده کن +- ml-4 یا mr-4 در Next.js → از ms-4 و me-4 استفاده کن +- new Date() برای نمایش تاریخ → از date-fns-jalali استفاده کن +- متن فارسی hardcode در کامپوننت → همه در fa.json باشد +- query بدون .Where(x => x.CafeId == cafeId) → داده tenant دیگر برمی‌گرداند +- setState در Flutter برای business logic → از Riverpod استفاده کن +- .Result یا .Wait() در C# → deadlock می‌دهد + +✅ بکن: +- هر mutation در Flutter اول به Drift ذخیره کن، بعد sync کن +- هر endpoint با [Authorize] + validate CafeId شروع کن +- هر string قابل نمایش را در fa.json/ar.json/en.json بریز +- تمام اعداد نمایشی را با .toLocaleString('fa-IR') فرمت کن +- هر job background را در Hangfire بریز، نه await inline +``` + +--- + +## مرحله ۱۲ — ترتیب توسعه پیشنهادی (Solo با Cursor) + +``` +هفته ۱-۲: Backend foundation (auth + schema + tenant middleware) +هفته ۳-۴: POS core (menu + orders + tables + QR + payments) +هفته ۵-۶: Dashboard Next.js (POS screen + KDS + basic reports) +هفته ۷-۸: CRM + Coupons + SMS marketing (Kavenegar) +هفته ۹-۱۰: HR module (shifts + attendance + payroll) +هفته ۱۱-۱۲: Flutter customer app (QR + menu + cart + reservation) +هفته ۱۳-۱۴: Discovery platform (کافه‌یاب) + Reviews +هفته ۱۵-۱۶: ZarinPal billing + Taraz + Snappfood + production deploy + +اولین beta: 5 کافه در شمال تهران — هفته ۸ +اولین پول: هفته ۱۶ +``` + +--- + +## چک‌لیست شروع روز اول + +``` +[ ] Node 20+ نصب است +[ ] .NET 10 SDK نصب است +[ ] Flutter 3.x نصب است +[ ] Docker Desktop در حال اجرا است +[ ] Cursor نصب است و باز است +[ ] پوشه meezi/ ساخته شده +[ ] .cursorrules در ریشه قرار دارد +[ ] MEEZI_PRD.md در ریشه قرار دارد +[ ] docker-compose up -d اجرا شده +[ ] در Cursor: Cmd+L → "Read .cursorrules and MEEZI_PRD.md, + then start Sprint 1: create the .NET solution structure" +``` diff --git a/Meezi.slnx b/Meezi.slnx new file mode 100644 index 0000000..ac5302c --- /dev/null +++ b/Meezi.slnx @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..60db12e --- /dev/null +++ b/README.md @@ -0,0 +1,77 @@ +# Meezi — Day 1 Quick Start + +## Step 1 — Copy these 4 files to your project root +.cursorrules ← AI rules for Cursor +MEEZI_CURSOR_GUIDE.md ← Full development guide +docker-compose.yml ← Local DB + Redis +README.md ← This file + +## Step 2 — Start local services +docker-compose up -d + +## Step 3 — Create project structure +mkdir meezi && cd meezi +git init + +mkdir src web mobile +cd src +dotnet new sln -n Meezi +dotnet new webapi -n Meezi.API --use-controllers +dotnet new classlib -n Meezi.Core +dotnet new classlib -n Meezi.Infrastructure +dotnet new classlib -n Meezi.Shared +dotnet sln add Meezi.API Meezi.Core Meezi.Infrastructure Meezi.Shared + +cd ../web +npx create-next-app@latest dashboard --typescript --tailwind --app + +cd ../mobile +flutter create meezi_app --org ir.meezi + +## Step 4 — Open in Cursor +cursor . (from the meezi/ root folder) + +## Step 5 — Paste this FIRST prompt in Cursor chat (Cmd+L) + +Read .cursorrules and MEEZI_CURSOR_GUIDE.md completely. +Then do Sprint 1 Week 1: +1. Set up the .NET solution with proper references between projects +2. Add all NuGet packages listed in the guide to each project +3. Create the complete EF Core entity schema from the guide +4. Set up AppDbContext with all DbSets +5. Create TenantMiddleware and ITenantContext +6. Set up Program.cs with full middleware pipeline + +--- + +## Cursor Chat Tips + +### Ask one sprint at a time +"Do Sprint 2: build the POS order APIs (menu, tables, orders)" + +### Reference specific sections +"Build the CRM endpoint from Step 10 of MEEZI_CURSOR_GUIDE.md" + +### Debug with context +"The order API returns 403. Check if TenantMiddleware is injecting CafeId correctly" + +### Generate Flutter screens +"Build the Flutter POS screen from Sprint 2 in the guide. RTL, Farsi, Riverpod state" + +--- + +## Useful Cursor Keyboard Shortcuts +Cmd+L → Open AI chat +Cmd+K → Inline AI edit (select code first) +Cmd+I → Composer (multi-file edits) +Cmd+. → Quick fix / suggestion +Tab → Accept autocomplete + +--- + +## Local URLs when running +Backend API: https://localhost:7001 +API Swagger: https://localhost:7001/swagger +Dashboard: http://localhost:3000 +Hangfire UI: https://localhost:7001/hangfire +Flutter web: http://localhost:8080 diff --git a/docker-compose.admin.yml b/docker-compose.admin.yml new file mode 100644 index 0000000..8fe5db4 --- /dev/null +++ b/docker-compose.admin.yml @@ -0,0 +1,59 @@ +# Meezi platform admin — use WITH main stack (shared Postgres + Redis) +# +# docker compose up -d postgres redis +# docker compose -f docker-compose.yml -f docker-compose.admin.yml up -d --build +# +# URLs: +# Admin web http://localhost:3102/fa/admin/login +# Admin API http://localhost:5081/swagger +# Health http://localhost:5081/health + +services: + admin-api: + build: + context: . + dockerfile: docker/admin-api/Dockerfile + args: + DOTNET_SDK_IMAGE: ${DOTNET_SDK_IMAGE:-mcr.microsoft.com/dotnet/sdk:10.0} + DOTNET_ASPNET_IMAGE: ${DOTNET_ASPNET_IMAGE:-mcr.microsoft.com/dotnet/aspnet:10.0} + container_name: meezi-admin-api + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + environment: + ASPNETCORE_ENVIRONMENT: Development + ASPNETCORE_URLS: http://+:8080 + RUN_MIGRATIONS: "false" + ConnectionStrings__DefaultConnection: Host=postgres;Port=5432;Database=meezi;Username=meezi;Password=meezi_local_pass + ConnectionStrings__Redis: redis:6379 + Cors__Origins__0: http://localhost:${ADMIN_WEB_PORT:-3102} + Cors__Origins__1: http://localhost:3101 + Kavenegar__ApiKey: "" + ports: + - "${ADMIN_API_PORT:-5081}:8080" + healthcheck: + test: ["CMD-SHELL", "bash -c 'cat /dev/tcp/127.0.0.1/8080' || exit 1"] + interval: 10s + timeout: 5s + retries: 12 + start_period: 40s + + admin-web: + build: + context: . + dockerfile: docker/admin-web/Dockerfile + args: + NEXT_PUBLIC_ADMIN_API_URL: ${NEXT_PUBLIC_ADMIN_API_URL:-http://localhost:5081} + container_name: meezi-admin-web + restart: unless-stopped + depends_on: + admin-api: + condition: service_healthy + environment: + PORT: "3000" + HOSTNAME: 0.0.0.0 + ports: + - "${ADMIN_WEB_PORT:-3102}:3000" diff --git a/docker-compose.full.yml b/docker-compose.full.yml new file mode 100644 index 0000000..d03799f --- /dev/null +++ b/docker-compose.full.yml @@ -0,0 +1,188 @@ +# Meezi — FULL platform (all 7 services in one file) +# +# Includes: Postgres, Redis, Main API, Admin API, +# Dashboard (web), Admin Panel (admin-web), Marketing Website +# +# Setup: +# copy .env.example .env +# docker compose -f docker-compose.full.yml up -d --build +# +# URLs (defaults): +# Dashboard http://localhost:3101/fa/login +# Website http://localhost:3010/fa +# Admin panel http://localhost:3102/fa/admin/login +# Main API http://localhost:5080/swagger +# Admin API http://localhost:5081/swagger + +services: + # ── Infrastructure ────────────────────────────────────────────────────────── + + postgres: + image: postgres:16-alpine + container_name: meezi-db + restart: unless-stopped + environment: + POSTGRES_DB: meezi + POSTGRES_USER: meezi + POSTGRES_PASSWORD: meezi_local_pass + ports: + - "${POSTGRES_PORT:-5434}:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U meezi -d meezi"] + interval: 5s + timeout: 5s + retries: 10 + + redis: + image: redis:7-alpine + container_name: meezi-redis + restart: unless-stopped + ports: + - "${REDIS_PORT:-6381}:6379" + command: redis-server --appendonly yes + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 10 + + # ── Backend APIs ──────────────────────────────────────────────────────────── + + api: + build: + context: . + dockerfile: docker/api/Dockerfile + args: + DOTNET_SDK_IMAGE: ${DOTNET_SDK_IMAGE:-mcr.microsoft.com/dotnet/sdk:10.0} + DOTNET_ASPNET_IMAGE: ${DOTNET_ASPNET_IMAGE:-mcr.microsoft.com/dotnet/aspnet:10.0} + container_name: meezi-api + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + environment: + ASPNETCORE_ENVIRONMENT: Development + ASPNETCORE_URLS: http://+:8080 + RUN_MIGRATIONS: "true" + ConnectionStrings__DefaultConnection: Host=postgres;Port=5432;Database=meezi;Username=meezi;Password=meezi_local_pass + ConnectionStrings__Redis: redis:6379 + App__PublicBaseUrl: ${NEXT_PUBLIC_API_URL:-http://localhost:5080} + App__QrPublicBaseUrl: http://localhost:${WEB_PORT:-3101} + Cors__Origins__0: http://localhost:${WEB_PORT:-3101} + Cors__Origins__1: http://localhost:${WEBSITE_PORT:-3010} + Cors__Origins__2: http://localhost:${ADMIN_WEB_PORT:-3102} + Auth__MaxOtpAttemptsPerHour: "100" + Kavenegar__ApiKey: "" + Billing__DashboardBaseUrl: http://localhost:${WEB_PORT:-3101} + Snappfood__WebhookSecret: meezi-dev-snappfood-secret + ZarinPal__MerchantId: "104c093d-2f5b-470d-978b-e4edefbf6cc8" + ZarinPal__Sandbox: "true" + ports: + - "${API_PORT:-5080}:8080" + volumes: + - api_uploads:/app/uploads + healthcheck: + test: ["CMD-SHELL", "bash -c 'cat /dev/tcp/127.0.0.1/8080' || exit 1"] + interval: 10s + timeout: 5s + retries: 12 + start_period: 40s + + admin-api: + build: + context: . + dockerfile: docker/admin-api/Dockerfile + args: + DOTNET_SDK_IMAGE: ${DOTNET_SDK_IMAGE:-mcr.microsoft.com/dotnet/sdk:10.0} + DOTNET_ASPNET_IMAGE: ${DOTNET_ASPNET_IMAGE:-mcr.microsoft.com/dotnet/aspnet:10.0} + container_name: meezi-admin-api + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + environment: + ASPNETCORE_ENVIRONMENT: Development + ASPNETCORE_URLS: http://+:8080 + RUN_MIGRATIONS: "false" + ConnectionStrings__DefaultConnection: Host=postgres;Port=5432;Database=meezi;Username=meezi;Password=meezi_local_pass + ConnectionStrings__Redis: redis:6379 + Cors__Origins__0: http://localhost:${ADMIN_WEB_PORT:-3102} + Cors__Origins__1: http://localhost:${WEB_PORT:-3101} + Kavenegar__ApiKey: "" + ports: + - "${ADMIN_API_PORT:-5081}:8080" + healthcheck: + test: ["CMD-SHELL", "bash -c 'cat /dev/tcp/127.0.0.1/8080' || exit 1"] + interval: 10s + timeout: 5s + retries: 12 + start_period: 40s + + # ── Frontend Services ─────────────────────────────────────────────────────── + + web: + build: + context: . + dockerfile: docker/web/Dockerfile + args: + NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:5080} + container_name: meezi-web + restart: unless-stopped + depends_on: + api: + condition: service_healthy + environment: + PORT: "3000" + HOSTNAME: 0.0.0.0 + ports: + - "${WEB_PORT:-3101}:3000" + + admin-web: + build: + context: . + dockerfile: docker/admin-web/Dockerfile + args: + NEXT_PUBLIC_ADMIN_API_URL: ${NEXT_PUBLIC_ADMIN_API_URL:-http://localhost:5081} + container_name: meezi-admin-web + restart: unless-stopped + depends_on: + admin-api: + condition: service_healthy + environment: + PORT: "3000" + HOSTNAME: 0.0.0.0 + ports: + - "${ADMIN_WEB_PORT:-3102}:3000" + + website: + build: + context: . + dockerfile: docker/website/Dockerfile + args: + MEEZI_API_URL: http://api:8080 + NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL:-http://localhost:3010} + container_name: meezi-website + restart: unless-stopped + depends_on: + api: + condition: service_healthy + environment: + PORT: "3000" + HOSTNAME: 0.0.0.0 + MEEZI_API_URL: http://api:8080 + NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL:-http://localhost:3010} + ports: + - "${WEBSITE_PORT:-3010}:3000" + +volumes: + postgres_data: + redis_data: + api_uploads: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b5c742a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,154 @@ +# Meezi — full stack (Postgres, Redis, API, Dashboard, Marketing Website) +# +# Setup: +# copy .env.example .env +# powershell -File scripts/docker-up-full.ps1 +# — or — docker compose up -d --build +# +# If image pulls fail (Iran / MCR timeout): VPN on, or see docs/DOCKER.md +# +# URLs (defaults): +# Dashboard http://localhost:3101/fa/login +# Website http://localhost:3010/fa +# Finder http://localhost:3103/fa +# API http://localhost:5080/swagger +# Health http://localhost:5080/health + +services: + postgres: + image: postgres:16-alpine + container_name: meezi-db + restart: unless-stopped + environment: + POSTGRES_DB: meezi + POSTGRES_USER: meezi + POSTGRES_PASSWORD: meezi_local_pass + ports: + - "${POSTGRES_PORT:-5434}:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U meezi -d meezi"] + interval: 5s + timeout: 5s + retries: 10 + + redis: + image: redis:7-alpine + container_name: meezi-redis + restart: unless-stopped + ports: + - "${REDIS_PORT:-6381}:6379" + command: redis-server --appendonly yes + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 10 + + api: + build: + context: . + dockerfile: docker/api/Dockerfile + args: + DOTNET_SDK_IMAGE: ${DOTNET_SDK_IMAGE:-mcr.microsoft.com/dotnet/sdk:10.0} + DOTNET_ASPNET_IMAGE: ${DOTNET_ASPNET_IMAGE:-mcr.microsoft.com/dotnet/aspnet:10.0} + container_name: meezi-api + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + environment: + ASPNETCORE_ENVIRONMENT: Development + ASPNETCORE_URLS: http://+:8080 + RUN_MIGRATIONS: "true" + ConnectionStrings__DefaultConnection: Host=postgres;Port=5432;Database=meezi;Username=meezi;Password=meezi_local_pass + ConnectionStrings__Redis: redis:6379 + App__PublicBaseUrl: ${NEXT_PUBLIC_API_URL:-http://localhost:5080} + App__QrPublicBaseUrl: http://localhost:${WEB_PORT:-3101} + Cors__Origins__0: http://localhost:${WEB_PORT:-3101} + Cors__Origins__1: http://localhost:${WEBSITE_PORT:-3010} + Cors__Origins__2: http://localhost:${FINDER_PORT:-3103} + Auth__MaxOtpAttemptsPerHour: "100" + Kavenegar__ApiKey: "" + Billing__DashboardBaseUrl: http://localhost:${WEB_PORT:-3101} + Snappfood__WebhookSecret: meezi-dev-snappfood-secret + ZarinPal__MerchantId: "${ZARINPAL_MERCHANT_ID:-}" + ZarinPal__Sandbox: "${ZARINPAL_SANDBOX:-true}" + ports: + - "${API_PORT:-5080}:8080" + volumes: + - api_uploads:/app/uploads + healthcheck: + # TCP probe only — no apt-get/curl in image (build works offline / without Ubuntu mirrors) + test: ["CMD-SHELL", "bash -c 'cat /dev/tcp/127.0.0.1/8080' || exit 1"] + interval: 10s + timeout: 5s + retries: 12 + start_period: 40s + + web: + build: + context: . + dockerfile: docker/web/Dockerfile + args: + NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:5080} + container_name: meezi-web + restart: unless-stopped + depends_on: + api: + condition: service_healthy + environment: + PORT: "3000" + HOSTNAME: 0.0.0.0 + ports: + - "${WEB_PORT:-3101}:3000" + + website: + build: + context: . + dockerfile: docker/website/Dockerfile + args: + MEEZI_API_URL: http://api:8080 + NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL:-http://localhost:3010} + container_name: meezi-website + restart: unless-stopped + depends_on: + api: + condition: service_healthy + environment: + PORT: "3000" + HOSTNAME: 0.0.0.0 + MEEZI_API_URL: http://api:8080 + NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL:-http://localhost:3010} + ports: + - "${WEBSITE_PORT:-3010}:3000" + + finder: + build: + context: . + dockerfile: docker/finder/Dockerfile + args: + NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:5080} + NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_FINDER_URL:-http://localhost:3103} + container_name: meezi-finder + restart: unless-stopped + depends_on: + api: + condition: service_healthy + environment: + PORT: "3000" + HOSTNAME: 0.0.0.0 + NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:5080} + NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_FINDER_URL:-http://localhost:3103} + ports: + - "${FINDER_PORT:-3103}:3000" + +volumes: + postgres_data: + redis_data: + api_uploads: diff --git a/global.json b/global.json new file mode 100644 index 0000000..d46d21e --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "10.0.100", + "rollForward": "latestFeature", + "allowPrerelease": false + } +} diff --git a/nuget.config b/nuget.config new file mode 100644 index 0000000..b26a4e6 --- /dev/null +++ b/nuget.config @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..9cc934d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "Meezi", + "lockfileVersion": 3, + "requires": true, + "packages": {} +}