Production-readiness audit fixes — every mock fallback is now gated on
IsDevelopment; in production these paths fail loudly instead:
- ZarinPal/Tara/SnappPay init: missing credentials returned a MOCK
payment URL whose callback verified as paid — a café could activate a
paid plan without paying. Now: "Payment gateway is not configured."
- Tara/SnappPay verify: a forged MOCK-* trace/token on the callback was
accepted as a verified payment in any environment. Now rejected
outside Development.
- Taraz (سامانه مودیان): returned a fake MOCK-TARAZ tracking code as if
invoices reached the tax authority. Now returns an honest error (the
real integration is not built yet).
- Admin integrations: NextPay/Vandar removed — they were listed but have
no gateway implementation (selecting them silently used ZarinPal).
- docker-compose: ASPNETCORE_ENVIRONMENT default flipped Development →
Production so a missing env var can never run prod in dev mode.
86 tests pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The platform no longer sells SMS. Each café saves its OWN Kavenegar API
key + sender line (new Cafes columns + migration) and campaigns are sent
and billed through that account.
Backend:
- GET/PUT /sms/settings (Manager/Owner; key echoed masked, verified
against the provider before saving)
- campaign + balance use the café's credentials; SMS_NOT_CONFIGURED
error when missing; plan-tier SMS gating removed everywhere
(PlanLimitChecker, SmsMarketingService, billing status)
- platform Kavenegar config stays ONLY for login OTPs (env/DB)
- design-time DbContext factory so `dotnet ef migrations add` works
without booting the host
Dashboard:
- SMS screen: provider-settings card, not-configured callout, campaign
form disabled until configured; quota bar removed (usage stays as info)
- subscription screen + plan comparison no longer show SMS limits
Admin panel:
- Kavenegar/SMS section removed from integrations (request field now
optional; stored OTP config untouched)
- SMS limit field removed from the plan editor
- nav label "درگاه و پیامک" → "درگاه پرداخت و AI"
fa/en/ar translations. 86 tests pass; all tsc clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- CanonicalPlans(): single source for Free·Starter·Pro·Business·Enterprise with the
locked feature sets (Free is broad: KDS/queue/Koja/offline/reviews/reservations/
coupons/employees; Starter +watermark-removal/custom-styling/review-reply; Pro +CRM/
reports/taxes/HR/delivery/expenses/branches; Business +3D/AI-3D; Enterprise *).
- Feature catalog: + offline, employees, watermark_removed, custom_menu_styling,
review_reply, api, white_label.
- New Starter plan (690k Toman default, billable, sort 1).
- One-time, version-guarded matrix upgrade (catalog.planMatrixVersion=2): brings the
existing (never-yet-admin-edited) prod plans to the canonical limits/features/order/
price and inserts Starter. Runs once; won't clobber later admin edits.
- Replaced the additive feature-merge (which would wrongly re-add menu_3d to Pro).
Defaults only — admins will be able to change everything in S4. 86 tests pass.
Uploads previously wrote every file to disk with a fresh GUID name, so the
same image uploaded twice produced two identical files. Now:
- New MediaAsset table records each stored upload (SHA-256 hash, size, type,
url, kind, scope) + migration. Indexed on (CafeId, ContentHash).
- MediaStorageService computes the content hash on upload; if an identical file
already exists for that café it returns the existing URL instead of writing a
duplicate (covers images, videos, 3D models). Dedup lookup/record run via a
scoped DbContext (the service is a singleton) and never block an upload on
failure.
- GET /api/cafes/{cafeId}/media lists the café's library (newest first, optional
?kind=) so the UI can let users pick an existing file instead of re-uploading.
86 API tests pass.
Backend half of offline Phase 1. Lets the offline outbox replay a write after a
lost response without executing it twice (e.g. an order whose POST reached the
server but whose reply never came back).
- IdempotencyRecord entity + table (unique index on (Scope, Key)); migration
AddIdempotencyRecords. Standalone POCO — no tenant/soft-delete filters.
- IdempotencyMiddleware (after TenantMiddleware, before plan-limit/controllers):
opt-in via `Idempotency-Key` header on POST/PUT/PATCH/DELETE.
* Completed key → replays stored status+body with `Idempotent-Replay: true`.
* In-progress key → 409 IDEMPOTENCY_IN_PROGRESS; the unique index serializes
racing first requests; stale (>60s) reservations are recovered after a crash.
* Only <500 responses are cached; 5xx is released so the client can retry.
Bookkeeping runs in isolated DI scopes so it never contaminates the controller's
unit of work. Keys are scoped per café — no cross-tenant collisions.
- 5 middleware tests (replay/execute-once, distinct key, pass-through, tenant
isolation, 5xx-not-cached). Full suite 86 passing.
Next in Phase 1: generalize the POS order queue into a generic client outbox that
sends these keys and remaps client→server ids.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Before, buying a plan immediately switched the tier and stacked the duration.
Now a purchase made while the café still has paid coverage is QUEUED to start
when the current coverage ends, and the owner can cancel a queued one.
Model:
- SubscriptionPayment gains EffectiveFrom/EffectiveTo; status gains Scheduled
(paid, queued) and Cancelled. EF migration AddSubscriptionScheduling (nullable).
BillingService:
- On payment completion, compute coverage end (latest of active expiry + furthest
queued period). If it is in the future → Scheduled (queued, café tier/expiry
untouched); else activate immediately as before. Periods chain correctly.
- GetStatusAsync lazily promotes any due queued period to active, and returns the
queue (QueuedPlans).
- CancelQueuedAsync cancels a Scheduled period (owner-only) and re-packs the queue
so later periods slide earlier. Active prepaid plan is never cut short; no
automatic refund (manual, per product decision).
- Confirmation SMS distinguishes "activated until X" vs "queued, starts X".
API: BillingStatusDto.QueuedPlans + DELETE /api/billing/queued/{paymentId}.
Dashboard:
- Subscription screen shows a "Queued subscriptions" card (tier, window, cancel
with confirm).
- Checkout shows "you already have an active subscription — this will start on
{date}" when the café is still covered.
- i18n fa/en/ar.
81 API tests pass; dashboard typechecks.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Delete (every manageable entity that only had "add" now has delete):
- Ingredients (warehouse): new DELETE /inventory/ingredients/{id} (soft-delete via
the global DeletedAt filter — no FK trouble with recipes/movements) + NoOp stub +
trash button in the materials cards.
- Reservations: new DELETE /reservations/{id} (soft-delete) + per-card delete button.
- Coupons & Customers: backend DELETE already existed; wired delete buttons in the UI.
- Shared ConfirmDialog component used by all delete flows (RTL-aware).
- Audit result: tables/branches/taxes/kitchen-stations/expenses/menu/terminals already
had delete; HR has no "add" so no delete needed; shifts intentionally excluded
(financial open/close records, not add-style entities).
Koja visibility:
- New Cafe.ShowOnKoja flag, default TRUE (DB default true so existing cafés stay
listed). Discover query now filters IsVerified && !Deleted && ShowOnKoja.
- public-profile GET/PUT expose showOnKoja; dashboard public-profile panel has an
on-by-default toggle that persists immediately. Platform IsVerified gate unchanged.
- EF migration AddCafeShowOnKoja (defaultValue: true).
Also: added the missing errors.generic i18n key (fa/en/ar) so useApiError's fallback
resolves instead of rendering the literal "errors.generic". 81 API tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root cause of the crash-loop: a soft-deleted Free plan still occupies its Tier in the unique index, but the existing-row check queried THROUGH the soft-delete global filter and missed it, so the seeder re-inserted Free and violated IX_PlatformPlanDefinitions_Tier on boot. Fixes: (1) IgnoreQueryFilters() on the plan/feature existing-checks so soft-deleted tiers/keys are counted; (2) wrap plan/feature/location seeding in try/catch so any seeding failure logs and startup continues — non-essential seeding must never crash-loop the API.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The previous change deduped on Id, but the unique constraints are on PlatformPlanDefinitions.Tier and PlatformFeatures.Key. Prod's existing Free plan has a different Id, so seeding re-inserted a Free-tier row and crashed on IX_PlatformPlanDefinitions_Tier (23505), crash-looping the API. Now skips any tier/key that already exists.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Plan + feature seeding was dev-gated and all-or-nothing, so production only had the Free plan (admin Plans page showed one). Now runs in every environment and upserts missing rows (adds Pro/Business/Enterprise on top of the existing Free). Also force LTR on the admin toggle switch so the knob doesn't render off-track under the RTL page.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Real cafés without a map pin now get approximate coordinates at their city centre (with a deterministic per-café offset) on every boot, in all environments, so the public Iran map lights up with merchant dots. Only fills rows where Latitude/Longitude is null and the city is recognised (20 major Iranian cities); never overwrites an owner-set pin. Owners can drop an exact pin from Settings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Showcase cafés (dev/staging only) now get Latitude/Longitude scattered around their real city (Tehran/Karaj) with a deterministic per-id offset, so the homepage Iran map renders a realistic cluster of blinking merchant lights. Backfills existing rows where coords are null. Production cafés get coordinates when owners set their location in dashboard Settings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Manual migration was missing the [Migration("...")] and [DbContext] attributes
that EF Core requires to discover and apply migrations via MigrateAsync().
Without them the Latitude/Longitude columns were never added to Cafes, causing
every query involving the Cafe entity to throw 42703 column-not-found errors.
Columns must be applied manually on the server before the next deploy:
ALTER TABLE "Cafes" ADD COLUMN IF NOT EXISTS "Latitude" double precision, ...
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DemoMenuSeeder used hardcoded IDs like cat_demo_coffee for every café.
If the dev seeder (runs when ASPNETCORE_ENVIRONMENT=Development) already
inserted those IDs for cafe_demo_001, a production café clicking
"Add demo data" hit a primary-key constraint violation.
Fix: EnsureMenuAsync now accepts useScopedIds=true which prefixes every
category and item ID with cafeId (e.g. cafe_abc_cat_demo_coffee).
CategoryId FKs on items are remapped through the same function.
DemoSeedService (the API endpoint handler) always passes useScopedIds=true.
DevelopmentDataSeeder keeps useScopedIds=false (default) so the existing
cafe_demo_001 rows in dev databases are not touched.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sidebar:
- All groups start collapsed on first load (v4 storage key resets old state)
- Opening one group closes all others (accordion)
- Navigating to a section opens only that section's group
Koja slug:
- SlugHelper: Persian->Latin transliteration, slug validation
- Registration accepts optional custom slug; auto-derives from cafe name
- Slug can be updated from dashboard Settings -> Profile
- Settings PATCH validates uniqueness (SLUG_TAKEN) and format (INVALID_SLUG)
- koja.meezi.ir/{slug} now redirects to /fa/cafe/{slug} (short URL support)
Bug fix:
- SupportTicketService: cafeId/status filters applied before Select() projection
to fix EF "could not be translated" crash on the support tickets page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- VerifyRegisterAsync: create a Branch named after the café alongside
the Café and Owner, so new owners can use the dashboard immediately
without hitting the "select a branch" gate
- PlatformDataSeeder: EnsureDefaultBranchesAsync runs on every boot and
creates a default branch for any existing café that has none (covers
cafés registered before this fix)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EnsureOwnerAdminAsync now sets Username='admin' (configurable via
Seed:SystemAdminUsername) on any existing admin that has no username,
and hashes Seed:SystemAdminPassword if provided and no hash is stored.
Covers fresh deploys and existing prod admins created before credentials
were added.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the raw HttpClient implementation with the Kavenegar NuGet SDK
(v1.2.4) for OTP, single, and bulk sends plus account info, wrapping the
synchronous SDK calls and translating its exceptions. Register the
service as scoped instead of via AddHttpClient.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move the dev-mode OTP logging into KavenegarSmsService so consumer and
admin auth flows no longer duplicate the fallback log.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Full backend implementation:
- Multi-tenant cafe/restaurant management (menus, orders, tables, staff)
- POS order flow with ZarinPal and Snappfood payment integration
- OTP authentication via Kavenegar SMS
- QR digital menu with public discover/finder endpoints
- Customer loyalty, coupons, CRM
- PostgreSQL via EF Core, Redis for caching/sessions
- Background jobs, webhook handlers
- Full migration history
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>