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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||||
+40
@@ -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
|
||||||
@@ -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=<merchant>
|
||||||
|
ZarinPal__Sandbox=false
|
||||||
|
Kavenegar__ApiKey=<key>
|
||||||
|
Snappfood__WebhookSecret=<secret>
|
||||||
|
Taraz__Username=<moadian-user>
|
||||||
|
Taraz__Password=<moadian-pass>
|
||||||
|
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
|
||||||
@@ -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 <path-to-food-101/images>`
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||||
|
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<MicrosoftPackageVersion>10.0.0</MicrosoftPackageVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageVersion Include="CsvHelper" Version="33.0.1" />
|
||||||
|
<PackageVersion Include="EPPlus" Version="7.5.2" />
|
||||||
|
<PackageVersion Include="FluentValidation" Version="11.11.0" />
|
||||||
|
<PackageVersion Include="FluentValidation.DependencyInjectionExtensions" Version="11.11.0" />
|
||||||
|
<PackageVersion Include="Hangfire.AspNetCore" Version="1.8.17" />
|
||||||
|
<PackageVersion Include="Hangfire.MemoryStorage" Version="1.8.1.1" />
|
||||||
|
<PackageVersion Include="Hangfire.PostgreSql" Version="1.20.10" />
|
||||||
|
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="$(MicrosoftPackageVersion)" />
|
||||||
|
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="$(MicrosoftPackageVersion)" />
|
||||||
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="$(MicrosoftPackageVersion)" />
|
||||||
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="$(MicrosoftPackageVersion)" />
|
||||||
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="$(MicrosoftPackageVersion)" />
|
||||||
|
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="$(MicrosoftPackageVersion)" />
|
||||||
|
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="$(MicrosoftPackageVersion)" />
|
||||||
|
<PackageVersion Include="Microsoft.Extensions.Http" Version="$(MicrosoftPackageVersion)" />
|
||||||
|
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||||
|
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
||||||
|
<PackageVersion Include="QRCoder" Version="1.6.0" />
|
||||||
|
<PackageVersion Include="QuestPDF" Version="2024.12.3" />
|
||||||
|
<PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" />
|
||||||
|
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||||
|
<PackageVersion Include="StackExchange.Redis" Version="2.8.16" />
|
||||||
|
<PackageVersion Include="System.Security.Cryptography.Xml" Version="10.0.6" />
|
||||||
|
<PackageVersion Include="Swashbuckle.AspNetCore" Version="7.2.0" />
|
||||||
|
<PackageVersion Include="xunit" Version="2.9.2" />
|
||||||
|
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -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<T> 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<T>
|
||||||
|
|
||||||
|
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<T> 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<T> 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<T>)
|
||||||
|
|
||||||
|
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<T> استفاده کن
|
||||||
|
- 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"
|
||||||
|
```
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<Solution>
|
||||||
|
<Folder Name="/src/">
|
||||||
|
<Project Path="src/Meezi.API/Meezi.API.csproj" />
|
||||||
|
<Project Path="src/Meezi.Core/Meezi.Core.csproj" />
|
||||||
|
<Project Path="src/Meezi.Infrastructure/Meezi.Infrastructure.csproj" />
|
||||||
|
<Project Path="src/Meezi.Shared/Meezi.Shared.csproj" />
|
||||||
|
</Folder>
|
||||||
|
</Solution>
|
||||||
@@ -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
|
||||||
@@ -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/null >/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"
|
||||||
@@ -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/null >/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/null >/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:
|
||||||
@@ -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/null >/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:
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"sdk": {
|
||||||
|
"version": "10.0.100",
|
||||||
|
"rollForward": "latestFeature",
|
||||||
|
"allowPrerelease": false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<packageSources>
|
||||||
|
<clear />
|
||||||
|
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
|
||||||
|
</packageSources>
|
||||||
|
<config>
|
||||||
|
<!-- Helps flaky TLS / filtered networks during docker build restore -->
|
||||||
|
<add key="http_retry_count" value="8" />
|
||||||
|
</config>
|
||||||
|
</configuration>
|
||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "Meezi",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user