The branch-menu-overrides availability switch (dashboard) and the BlogToggle
(admin website editor) still moved their knob with translate-x while inheriting
RTL, so the knob escaped the track on the right. Pin both to dir="ltr" like the
other switches. All four role="switch" toggles in the codebase now share the fix.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add DELETE /api/cafes/{cafeId}/menu/items/{id} (DeleteItemAsync soft-delete,
mirroring the existing category delete) — item delete had no backend route.
- Dashboard menu admin: destructive "delete" action in the item and category
edit modals, behind a shared confirm dialog (AlertDialog). Deleting the
selected category falls back to "all items".
- Fix the availability ToggleSwitch in RTL: force dir="ltr" so the knob's
translate-x stays inside the track instead of escaping on the right
(same fix as the admin-panel toggles).
- i18n: deleteItem/deleteCategory confirm + success strings (fa/en/ar).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
BuildDemoIngredients/BuildDemoTables built ids as
"{cafeId}_ing_{guid}"[..36]. For a real cafe (32-char hex id) the
first 36 chars are just "{cafeId}_ing" — the unique guid is cut off,
so all 15 ingredients (and all 10 tables) get the SAME id, causing a
primary-key collision on SaveChanges -> 500. cafe_demo_001 has a short
id so the guid survived, which is why the bug only hit real cafes.
The Id columns are text (no length limit), so the truncation served no
purpose. Removed [..36] from both so the full unique id is kept.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ROOT CAUSE of demo-seed/billing/etc. returning 403 for real owners: .NET's JWT
handler remaps the short "role" claim to ClaimTypes.Role on inbound, so
TenantMiddleware's FindFirst("role") returned null and tenant.Role (EmployeeRole?)
stayed null. EnsureManager/EnsureOwner then rejected even a valid Owner token with
MANAGER_REQUIRED / OWNER_REQUIRED, while reads (no role gate) worked and
[Authorize(Roles=...)] worked (it reads the remapped claim). Now reads the role
under both MeeziClaimTypes.Role ("role") and ClaimTypes.Role. Same fix applied to
the AuthController whoami role. Fixes demo seed, subscription billing, and every
other tenant.Role-gated action.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lets the POS agent and the QR/app customer attach a free-text note to each
order line (e.g. "no tomato", "extra hot") that reaches the kitchen/bar.
- Backend already supported it (OrderItem.Notes persists; CreateOrderItemRequest
and OrderItemDto carry Notes; LiveOrderDto items include it) — this wires the UI.
- cart.store: add setNotes(menuItemId, notes); notes already travel in
getPendingLines and round-trip via hydrateFromOrder.
- POS pos-screen: a note input under each cart line.
- QR guest menu: a note input under each cart line (QrCartLine.note).
- KDS: render the note prominently under each item so kitchen/bar sees it.
- i18n: pos.itemNotePlaceholder + qrMenu.itemNote (fa/ar/en).
Note: notes are captured on items being added; editing a note on an
already-submitted line is out of scope (no pending delta to re-send).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The redirect source "/:slug([a-z0-9][a-z0-9-]*[a-z0-9])" matched single-segment
paths including the locale itself, so /fa redirected to /fa/cafe/fa (slug "fa")
and /en to /fa/cafe/en — non-existent cafés that returned Internal Server Error.
Visiting koja.meezi.ir (-> /fa) hit this. Removed the redirect so the home page
renders; short café URLs can be re-added via middleware with reserved-word guards.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The integrations form rendered from (gateways state, falling back to fetched data) but SAVED from the state and edited via updateGateway on . If gateways hadn't hydrated, edits (e.g. Zarinpal merchantId) were written to an empty array and the save sent nothing. Now updateGateway seeds from fetched data on first edit, and the save maps over — render, edit, and save share one source. NOTE: prod admin had also been stale because recent deploys aborted on the main-API crash before the admin containers restarted.
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>
Location card gets a 'موقعیت فعلی من' button that fills lat/lng from the browser's geolocation. Support ticket list now shows the resolved (localized) error instead of a generic message, so a failure is diagnosable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Koja auto-detected locale from the browser Accept-Language (en for many Persian users); set localeDetection:false so locale-less URLs default to fa. Also guarded cafe.discoverProfile across the cafe page, cafe card, and JSON-LD — a café without a discover profile crashed the page (500). The cafe page now resolves the café first and notFound()s an unknown slug before fetching menu/reviews.
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>
Error toasts surfaced the raw English backend message. Added an errors namespace (fa/ar/en) keyed by error code + a useApiError() resolver that maps ApiClientError.code to the localized message (fallback to a localized generic). Wired into menu, tables, demo banner, and subscription checkout; hardened getErrorMessage so it never returns the raw backend message.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The dashboard demo-data banner is shown to Owner and Manager, but the /demo/seed endpoint required strictly Owner, so a Manager clicking it got a silent 403 (the banner had no error handler) — appearing as 'nothing happens, no tables or items'. The endpoint now allows Owner or Manager, and the banner shows the error on failure.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Marketing-site login/register/dashboard links were locale-less (app.meezi.ir/login), so the dashboard auto-detected locale from the browser Accept-Language (en-US) and redirected Persian users to /en. Links now carry the current locale, and the dashboard sets localeDetection:false so any locale-less entry defaults to fa (Iran-first) instead of guessing from the browser.
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>
Replaced the rough 40-point hand-drawn polygon with the real national border (74 vertices, Natural Earth via world.geo.json) and fitted the projection bounding box to Iran's true extent, so the silhouette is recognisable and café markers stay aligned. Reworked the marker animation from a radar-style expanding ring into a slow 3.6s ease-in-out lamp fade (opacity 1->0.2->1) with a halo that glows on and off in sync. Verified via the SVG timeline: opacity 1.0 at 0s, 0.2 at 1.8s, 1.0 at 3.6s.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The test project no longer compiled: recent feature commits changed
interfaces and DTOs without updating the test doubles/call sites, so the
whole suite (and therefore CI) was failing to build.
- NoOpInventoryService: add IInventoryService.GetPurchasesSummaryAsync and
the new string? userId param on AdjustAsync.
- NoOpLoyaltyService: add ILoyaltyService.RedeemOnOrderAsync.
- NoOpOrderNotificationService: add NotifyCallWaiterAsync.
- New NoOpAbuseProtectionService and NoOpMediaStorageService test doubles.
- QrMenuTests: ReviewService/PublicService gained IAbuseProtectionService +
IHttpContextAccessor (and ReviewService an IMediaStorageService); wire the
new no-op doubles + a real HttpContextAccessor.
- PrintingTests: OrderDto gained a DisplayNumber int between CreatedAt and
Items; pass it.
- DiscoverFilterTests: add missing `using Xunit;` and the new openNow arg on
DiscoverFilterParams.FromQuery.
Result: dotnet test -> Passed: 81, Failed: 0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Dashboard & API bug fixes for owner-reported breakage:
- MenuController validators (PosValidators): NameEn was required but the
dashboard sends null when blank, so every manual menu-item create failed
and category create failed 100% (the form never sends nameEn). Now optional.
- DemoDataBanner: only showed when a cafe was exactly empty, so
showcase-seeded cafes (2-3 cats / 3-5 items) could never trigger the
one-click seed. Widened gate to sparse menus (<5 cats && <10 items) and
added a clear "nothing to add" message when already populated.
- client.ts: added one-time JWT refresh-and-retry on 401 (shared in-flight
promise) before bouncing to /login. Expired access tokens silently broke
ticket list, add-table, and other reads.
- Surface API errors as toasts on menu + table mutations (were swallowed
silently, so failures looked like "nothing happens").
- Admin blog editor: saving an edit dropped IsPublished (defaulted false,
silently unpublishing the post on every save); now persisted with a
toggle. Also hoisted the inner Field component to module scope - it was
remounting every input on each keystroke and dropping focus.
- Admin integrations: replaced raw radio gateway selector with a styled
RadioDot matching the iOS toggles.
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>
Removes <none>-tagged images left over from previous builds.
Only affects untagged images (dangling=true) — never touches other
projects' named images (soroushasadi-site, drsousan, etc.).
Also logs disk usage after prune.
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>
- Start api alone first; web/admin-api each wait for their own step
so a health-check wait never blocks unrelated services
- Detect crash-loops via RestartCount > 1 (restart:unless-stopped hides
the exited state behind rapid restarts — count is reliable)
- Dump up to 120 lines of api logs immediately on crash/timeout
- Log infra network state (json) in attach step + failure dump so we
can see exactly which aliases are registered on meezi_default
- admin-api and admin-web are now started in separate steps, same pattern
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without --alias, meezi-db joins meezi_default but is only reachable
as "meezi-db". The API uses Host=postgres — DNS lookup fails after
~5s, migration throws, container crashes.
Fix: disconnect first, then reconnect with service-name aliases
so "postgres" and "redis" resolve correctly on meezi_default.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
postgres/redis were created before the compose project name was locked
to "meezi", so they're on a different Docker network. New app containers
join meezi_default — the API crashes immediately because it can't reach
Host=postgres.
Fix: create meezi_default if needed, then docker network connect
meezi-db and meezi-redis to it before starting the app containers.
Also dump API and admin-api logs on failure to make future failures
easier to diagnose.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: after successful creation the form stayed on /blog/new.
User couldn't tell it worked, clicked Save again, the second attempt
hit the unique slug constraint and showed an error — making it look
like creation was broken.
Fix: adminPost is now typed, onSuccess redirects to /blog/{id} on new
posts so the user lands on the edit page immediately.
Also fixes commentCount being undefined in the list (MapPost now
includes comment count via eager-loaded Comments).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Existing containers lack compose project labels so Compose cannot claim
them — it tries to CREATE alongside them and hits a name conflict.
Fix: stop + rm only the 6 meezi app containers by name before compose up.
Postgres, redis, and all other projects are never touched.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The meezi-redis container was created before the name:meezi label existed
in docker-compose.yml, so Compose doesn't recognise it as its own project
container and tries to CREATE a new one, causing the name conflict.
Real fix: postgres and redis are persistent infrastructure — CI should
never restart them. Remove them from all deploy steps entirely.
Only api, web, website, koja, admin-api, admin-web are cycled on deploy.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
docker compose up without --no-recreate tries to recreate postgres/redis
when it detects a config change or finds a stopped container, which causes
"container name already in use" when the container is still running.
Fix: infrastructure (postgres, redis) uses --no-recreate so a healthy
container is never touched. App services (api, web, website, koja,
admin-api, admin-web) use --force-recreate so freshly-built images are
always applied.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds POST /api/cafes/{cafeId}/demo/seed (owner-only) that seeds:
- 9% default VAT tax
- 7 menu categories + 59+ items via DemoMenuSeeder
- 15 inventory ingredients (coffee shop staples)
- 10 tables across 3 floors on the first active branch
Frontend DemoDataBanner appears on menu, tables, and inventory
pages when the café is completely empty, so owners can populate
demo data in one click instead of entering everything manually.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- dashboard layout: wait for Zustand _hasHydrated before redirecting to /login
(was redirecting on first render before localStorage was read)
- admin shell: same fix using new _hasHydrated on admin auth store
- admin-auth.store: add _hasHydrated + onRehydrateStorage to mirror merchant store
- AdminPlansScreen: replace direct cache mutation with per-plan PlanCard component
that owns its own useState — fixes other plans disappearing after save
- AdminSettingsScreen: detect boolean values and render iOS-style Toggle switches
- AdminIntegrationsScreen: replace all <input type=checkbox> with Toggle switches;
replace OpenAI model text input with <select> dropdown (gpt-4o-mini/4o/4-turbo/4/3.5)
- blog editor: fix form never syncing existing post data into state (editing was broken);
all fields now use local form state, save uses form directly
- blog links: fix broken relative hrefs (website/blog/new → /admin/website/blog/new)
and back button using proper Link components
- ci-cd: remove image prune step entirely — never removes containers or images
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prevents runner workspace collisions with other projects (DrSousan etc.)
causing containers to be treated as orphans and stopped on deploy.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously the subscribe mutation had no onError handler, so any
payment initiation failure (wrong merchant ID, ZarinPal API error,
disabled payment method) would silently re-enable the button with
no user feedback. Now errors are shown below the Pay button.
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>
VerifyOtpRequestValidator was passing the raw phone string to
IsValidIranMobile which requires a pre-normalized 11-digit "09…" string.
Any other format (country code prefix, Persian digits, etc.) failed
validation instantly — causing verify-otp to return HTTP 400 in ~2ms
before the service logic could ever run.
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>
- docker-compose.admin.yml: RUN_MIGRATIONS was hardcoded false → now
uses ${RUN_MIGRATIONS:-true} so migrations run automatically on deploy
- Both compose files: expose Seed__SystemAdminPhone/Username/Password
env vars so the seeder sets admin credentials without manual SQL
- .env.example: document SEED_ADMIN_* variables
On next deploy: migrations run, Username='admin' is patched on the
existing admin, and password is hashed from SEED_ADMIN_PASSWORD.
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>
npm ci failed in Docker because package-lock.json was stale (missing three
and the workbox/PWA deps) and @google/model-viewer@4.2.0 requires three@^0.182.0
while package.json pinned ^0.163.0. Bumped three and regenerated the lockfile.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>