Commit Graph

228 Commits

Author SHA1 Message Date
soroush.asadi 456a446850 feat(meta): per-page titles + favicon/app icons + PWA across the panel
CI/CD / CI · API (dotnet build + test) (push) Successful in 40s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 31s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m9s
CI/CD / CI · Admin Web (tsc) (push) Successful in 39s
CI/CD / CI · Website (tsc) (push) Successful in 46s
CI/CD / CI · Koja (tsc) (push) Successful in 50s
CI/CD / Deploy · all services (push) Successful in 2m54s
The app had no metadata anywhere — pages showed no <title> and no favicon
or app icon. Added:
- Root metadata in [locale]/layout: title default + "%s — میزی" template,
  description, icons (favicon + apple-touch-icon → /icons), manifest link,
  appleWebApp, themeColor viewport, noindex (private panel).
- Per-page title on all 22 dashboard route pages (داشبورد, منو, گزارش‌ها, …).
- Guest menu (/q) layout: own title + icon + manifest.

PWA + favicon now use the Meezi icon everywhere. Verified via SSR: titles
render (e.g. "منو — میزی") and icon/manifest/apple-touch-icon links present.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 15:47:00 +03:30
soroush.asadi 4523c8861f feat(ui): grouped thousands separators for price/amount inputs
Price fields showed raw digits (1490000) while typing — hard to read for
Toman amounts. New shared MoneyInput groups as you type (1,490,000),
accepts Persian/Arabic digits, and reports a raw digit string so callers
keep parsing unchanged. Applied to menu item price, branch price override,
expense amount, and payment-correction replacement amount. Displays already
group via formatCurrency (incl. the QR guest-menu preview).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 15:46:25 +03:30
soroush.asadi a855cf1d80 feat(auth): admin-issued café recovery key login
CI/CD / CI · API (dotnet build + test) (push) Successful in 5m6s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 1m30s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m10s
CI/CD / CI · Admin Web (tsc) (push) Successful in 38s
CI/CD / CI · Website (tsc) (push) Successful in 46s
CI/CD / CI · Koja (tsc) (push) Successful in 1m0s
CI/CD / Deploy · all services (push) Successful in 5m31s
Platform admins can generate a permanent recovery key per café (admin
panel → Cafés). The café Owner uses it to sign in when OTP access is lost;
once authenticated, all server-side data syncs as normal (data is per-café
on the server, the device only caches it).

Backend:
- Cafe.RecoveryKeyHash (SHA-256, unique index) + RecoveryKeyCreatedAt; migration
- RecoveryKeyGenerator util: MZ-XXXXX-XXXXX-XXXXX-XXXXX, ~190-bit entropy,
  stored as SHA-256 (API-token pattern — raw key shown once, never retrievable)
- Admin: POST/DELETE /api/admin/cafes/{id}/recovery-key (key returned once);
  café list now reports HasRecoveryKey + RecoveryKeyCreatedAt
- Login: POST /api/auth/login-key → exact-hash lookup → resolves café Owner →
  issues normal JWT; rate-limited (auth-otp), suspended/no-owner guarded, logged

Admin UI: per-café generate / regenerate / revoke with one-time reveal + copy.
Dashboard login: discreet "ورود با کلید بازیابی" link → key field. fa/en/ar.

86 tests pass; all tsc clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 15:10:11 +03:30
soroush.asadi 76d4434581 fix(qr): guest menu 500 (SSR) + remove café discovery from owner panel
1. The /q/{code} guest menu returned HTTP 500 on every load. Root cause:
   menu-item-model-viewer.tsx did a top-level `import "@google/model-viewer"`,
   a browser-only lib that touches `self` at module evaluation. Next pulled
   it into the server module graph (page → qr-guest-menu → qr-menu-3d-sheet →
   model-viewer) and SSR crashed with "self is not defined". Now the library
   is imported lazily inside useEffect (client-only); a poster placeholder
   shows until the custom element registers. Verified /q/* now returns 200.

2. Removed the "discover" (browse other cafés) item from the café owner
   sidebar — café discovery belongs in Koja, not the owner panel. The owner
   still manages their OWN Koja listing from Settings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 15:08:48 +03:30
soroush.asadi 9765491f6f fix(prod): payment/tax gateways never fake success outside Development
CI/CD / CI · API (dotnet build + test) (push) Successful in 41s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 29s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m7s
CI/CD / CI · Admin Web (tsc) (push) Successful in 38s
CI/CD / CI · Website (tsc) (push) Successful in 45s
CI/CD / CI · Koja (tsc) (push) Successful in 51s
CI/CD / Deploy · all services (push) Successful in 1m31s
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>
2026-06-12 10:16:01 +03:30
soroush.asadi 00649d0248 feat(sms): bring-your-own-provider — cafés use their own SMS account
CI/CD / CI · API (dotnet build + test) (push) Successful in 40s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 30s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m8s
CI/CD / CI · Admin Web (tsc) (push) Successful in 37s
CI/CD / CI · Website (tsc) (push) Successful in 45s
CI/CD / CI · Koja (tsc) (push) Successful in 50s
CI/CD / Deploy · all services (push) Successful in 5m16s
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>
2026-06-12 09:23:50 +03:30
soroush.asadi 615d5348de fix(subscription): plan comparison + checkout read the live plan catalog
CI/CD / CI · API (dotnet build + test) (push) Successful in 42s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 30s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m10s
CI/CD / CI · Admin Web (tsc) (push) Successful in 38s
CI/CD / CI · Website (tsc) (push) Successful in 47s
CI/CD / CI · Koja (tsc) (push) Successful in 50s
CI/CD / Deploy · all services (push) Successful in 3m27s
The merchant plan page hard-coded 4 tiers, prices and a feature matrix
that drifted from the admin-editable platform catalog (Starter tier
missing, stale prices/features). PlanComparison and CheckoutScreen now
consume /platform/plans + new /platform/features-catalog:

- columns = active plans by SortOrder (incl. Starter), names from
  DisplayNameFa/En, prices from MonthlyPriceToman
- limit rows from PlanLimitsData (int.MaxValue → "نامحدود")
- feature rows from the feature catalog, ticked via FeatureKeys
- checkout validates the ?plan= param against isBillableOnline and
  prices from the catalog — no more client-side price constants

fa/en/ar limit-row labels added.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 08:16:29 +03:30
soroush.asadi 74f46a4781 fix(dashboard): Set spread → Array.from for CI tsconfig target
CI/CD / CI · API (dotnet build + test) (push) Successful in 41s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 30s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m10s
CI/CD / CI · Admin Web (tsc) (push) Successful in 39s
CI/CD / CI · Website (tsc) (push) Successful in 45s
CI/CD / CI · Koja (tsc) (push) Successful in 51s
CI/CD / Deploy · all services (push) Successful in 3m41s
Local tsconfig.json has uncommitted target changes, so `[...voidIds]`
passed locally but failed CI's tsc (TS2802, target < es2015).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 01:30:21 +03:30
soroush.asadi c47922414a feat: اصلاح سند payment corrections + audit-log & daily P&L views
CI/CD / CI · API (dotnet build + test) (push) Successful in 51s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 31s
CI/CD / CI · Dashboard (tsc) (push) Failing after 1m12s
CI/CD / CI · Admin Web (tsc) (push) Successful in 40s
CI/CD / CI · Website (tsc) (push) Successful in 46s
CI/CD / CI · Koja (tsc) (push) Successful in 51s
CI/CD / Deploy · all services (push) Has been skipped
Backend:
- POST /orders/{id}/payments/corrections (Manager/Owner): void wrong
  payments (marked Refunded, never deleted) and/or record replacements
  atomically; mandatory reason; requires an open register shift; full
  before/after written to the immutable audit trail.
- GET /orders/closed?date= — closed orders of one Iran-calendar day,
  paged, the browsing surface for corrections.
- CalculateExpectedCash now subtracts cash refunds so corrections keep
  the drawer expectation honest.

Dashboard (reports screen now has three tabs):
- عملکرد و سود: existing KPIs/charts + new day-by-day breakdown table
  (orders, revenue, expenses, net profit per Jalali day).
- اصلاح سند: closed-orders browser with payment chips + correction
  dialog (void checkboxes, replacement rows, live balance, reason).
- گزارش عملیات: filterable audit-log viewer (category, Jalali range,
  branch) with expandable structured details.

fa/en/ar translations included. 86 backend tests pass; dashboard tsc clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 01:24:19 +03:30
soroush.asadi 2a4cf1d20b feat(dashboard): Jalali date pickers + mobile/tablet responsive shell
CI/CD / CI · API (dotnet build + test) (push) Successful in 51s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 38s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m5s
CI/CD / CI · Admin Web (tsc) (push) Successful in 37s
CI/CD / CI · Website (tsc) (push) Successful in 44s
CI/CD / CI · Koja (tsc) (push) Successful in 50s
CI/CD / Deploy · all services (push) Successful in 2m41s
Full Persian calendar:
- New JalaliDateField — Shamsi popover picker (Saturday-first weeks,
  Persian digits, امروز shortcut); wire format stays ISO Gregorian
  YYYY-MM-DD. Falls back to the native input for the en locale.
- Replaces all 5 native type="date" inputs (Gregorian-only pickers):
  reservations, expenses from/to, reports from/to.
- Reservations list date now renders Jalali instead of the raw ISO
  string; branches purge timestamp now formats with fa-IR.

Responsive shell (mobile + tablet):
- New MobileNav: hamburger in the topbar (< md) opening an RTL-aware
  slide-over drawer with all nav destinations, permission-filtered,
  Escape/backdrop close and body scroll lock.
- Desktop sidebar hidden below md; header center cluster (clock/plan)
  hidden below md; language switcher hidden below sm.
- Main content padding scales p-3 → p-4 → p-6.
- Verified at 375px and 768px: no horizontal overflow, drawer and
  Jalali picker fully functional.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 23:10:38 +03:30
soroush.asadi d811b7d6d5 feat(dashboard): simplify navigation — frequency-based IA
CI/CD / CI · API (dotnet build + test) (push) Successful in 47s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 28s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m6s
CI/CD / CI · Admin Web (tsc) (push) Successful in 40s
CI/CD / CI · Website (tsc) (push) Successful in 44s
CI/CD / CI · Koja (tsc) (push) Successful in 50s
CI/CD / Deploy · all services (push) Successful in 2m43s
The sidebar had 22 items in 5 accordion groups, all defaulting closed:
first visit showed five vague headers and zero destinations, there was
no Dashboard/Home link at all, and rare pages (taxes, subscription) had
equal weight with POS. Restructured around usage frequency:

- Flat primary (always visible, no header): Dashboard, POS, Tables,
  Kitchen, Queue, Reservations, Menu, Reports
- Two collapsible groups: Customers & marketing (crm, coupons, sms,
  reviews, discover) and Café management (inventory, expenses, shifts,
  taxes, hr, branches)
- Footer utility icons: settings, subscription, support
- Removed "notifications" from the nav (duplicate of the topbar bell)

Other fixes folded in:
- Deleted [locale]/page.tsx which redirected "/" to /pos — it made the
  POS exit button a no-op loop and left OverviewScreen unreachable.
  "/" now renders the overview home; login still lands on /pos.
- Branch gating moved from group-level to an item whitelist
  (BRANCH_ALLOWED_NAV_KEYS) — also closes the hole where branch
  accounts could deep-link to /reports etc. past the RouteGuard.
- RouteGuard now checks footer items too (subscription stays gated).
- revalidate=300 on the locale layout: Next emitted s-maxage=31536000
  and the WCDN edge kept serving year-old HTML shells after deploys.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 22:06:59 +03:30
soroush.asadi e0c786fcd1 ci: drop AIA cert fetch — mirror chain is fixed at the source
CI/CD / CI · API (dotnet build + test) (push) Successful in 55s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 31s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m10s
CI/CD / CI · Admin Web (tsc) (push) Successful in 38s
CI/CD / CI · Website (tsc) (push) Successful in 47s
CI/CD / CI · Koja (tsc) (push) Successful in 57s
CI/CD / Deploy · all services (push) Successful in 3m1s
Run 77 diagnostics proved http://yr.i.lencr.org/ connects but never
responds from the runner (national filtering), so fetching ISRG Root YR
at build time can never work. Meanwhile the mirror's fullchain.pem now
serves the complete chain: leaf → YR2 → ISRG Root YR cross-signed by
ISRG Root X1, which IS in every stock trust store — verified with
strict curl (ssl_verify_result=0) and openssl verify.

Replace both Trust steps with a cheap s_client sanity check that fails
early with a pointer to the server-side fix if the cert regresses on
its ~90-day renewal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 21:29:31 +03:30
soroush.asadi bafbfbcadf ci: fix Trust step crash in sh — replace pipefail with POSIX set -eu
CI/CD / CI · API (dotnet build + test) (push) Failing after 18s
CI/CD / CI · Admin API (dotnet build) (push) Failing after 17s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m5s
CI/CD / CI · Admin Web (tsc) (push) Successful in 36s
CI/CD / CI · Website (tsc) (push) Successful in 44s
CI/CD / CI · Koja (tsc) (push) Successful in 49s
CI/CD / Deploy · all services (push) Has been skipped
Gitea act runner v0.6.1 ignores `shell: bash` step overrides and always
executes with `sh -e {0}`. The `set -euo pipefail` on line 2 caused sh to
exit immediately with "Illegal option -o pipefail" before any curl/openssl
ran. Replace with POSIX-compatible `set -eu` in both api-build and
admin-api-build trust steps so the diagnostic curl output is finally visible.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 08:28:52 +03:30
soroush.asadi 206cd7d3c3 ci: fix Trust step — add shell: bash (act runner defaults to sh, no pipefail)
CI/CD / CI · API (dotnet build + test) (push) Successful in 43s
CI/CD / CI · Admin API (dotnet build) (push) Failing after 2s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m6s
CI/CD / CI · Admin Web (tsc) (push) Successful in 38s
CI/CD / CI · Website (tsc) (push) Successful in 45s
CI/CD / CI · Koja (tsc) (push) Successful in 49s
CI/CD / Deploy · all services (push) Has been skipped
set -euo pipefail is bash-only; Gitea act runner used sh by default so
the step crashed on line 1 before curl even ran. Adding shell: bash
lets the step actually execute and surface the real AIA/cert output.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 08:17:54 +03:30
soroush.asadi 7b77bb4722 ci: verbose diagnostic Trust step to pinpoint PartialChain cause
CI/CD / CI · API (dotnet build + test) (push) Failing after 3s
CI/CD / CI · Admin API (dotnet build) (push) Failing after 2s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m6s
CI/CD / CI · Admin Web (tsc) (push) Successful in 37s
CI/CD / CI · Website (tsc) (push) Successful in 44s
CI/CD / CI · Koja (tsc) (push) Successful in 49s
CI/CD / Deploy · all services (push) Has been skipped
Previous attempt used curl -sf which silently swallows failures, so
we never knew if ISRG Root YR was actually fetched. This run:
  • set -euo pipefail  → step fails fast and loudly on any error
  • curl -v            → shows connection result / error in log
  • openssl verify     → confirms cert bundle is good before restore
  • openssl s_client   → shows full chain verify against live mirror

If the AIA URL (http://yr.i.lencr.org/) is unreachable from the
runner, the step will fail HERE rather than silently at dotnet restore.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 08:09:18 +03:30
soroush.asadi 1db8a8f08c fix(ci): fetch ISRG Root YR root cert at build time + belt-and-suspenders
CI/CD / CI · API (dotnet build + test) (push) Failing after 3m35s
CI/CD / CI · Admin API (dotnet build) (push) Failing after 6m23s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m6s
CI/CD / CI · Admin Web (tsc) (push) Successful in 37s
CI/CD / CI · Website (tsc) (push) Successful in 44s
CI/CD / CI · Koja (tsc) (push) Successful in 49s
CI/CD / Deploy · all services (push) Has been skipped
The prior Trust step added only the YR2 intermediate to the OS trust
store. dotnet's X.509 chain builder requires a self-signed ROOT as the
trust anchor (it does not enable OpenSSL's X509_V_FLAG_PARTIAL_CHAIN),
so intermediate-only still caused PartialChain.

New approach (two jobs: api-build, admin-api-build):
  1. curl http://yr.i.lencr.org/ (plain HTTP AIA) → ISRG Root YR DER
     → convert to PEM → add to /usr/local/share/ca-certificates/
  2. cp YR2 intermediate (docker/nexus-mirror-ca.crt) → same dir
  3. update-ca-certificates  (OS method)
  4. cat both certs >> /etc/ssl/certs/ca-certificates.crt
     (belt-and-suspenders: directly appends to the OpenSSL bundle
      dotnet reads on Linux, works even if step 3 is a no-op)

If the AIA fetch fails (network block) step 4 still appends the
intermediate, which may work if dotnet ever enables partial chains.
Fetch failure is non-fatal (echo warning + continue).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 07:34:52 +03:30
soroush.asadi 82145b0d21 feat(pos2): add dashboard exit button to POS board header
CI/CD / CI · API (dotnet build + test) (push) Failing after 3m19s
CI/CD / CI · Admin API (dotnet build) (push) Failing after 3m19s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m5s
CI/CD / CI · Admin Web (tsc) (push) Successful in 37s
CI/CD / CI · Website (tsc) (push) Successful in 44s
CI/CD / CI · Koja (tsc) (push) Successful in 48s
CI/CD / Deploy · all services (push) Has been cancelled
POS runs in the (fullscreen) layout which strips the sidebar.
Adds a Home → داشبورد button at the top-left of the table board so
users can navigate back to the dashboard without being stuck.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 07:23:14 +03:30
soroush.asadi 59486cdf24 fix(pos2): wait for branch before fetching menu + add left category sidebar
CI/CD / CI · API (dotnet build + test) (push) Failing after 3m20s
CI/CD / CI · Admin API (dotnet build) (push) Failing after 3m19s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m5s
CI/CD / CI · Admin Web (tsc) (push) Successful in 41s
CI/CD / CI · Website (tsc) (push) Successful in 44s
CI/CD / CI · Koja (tsc) (push) Successful in 52s
CI/CD / Deploy · all services (push) Has been cancelled
Race fix: orderBranchId now returns `undefined` (not null) while the /branches
query is in flight. usePos2Menu treats undefined as "not yet determined" and
skips the fetch, preventing getBranchMenu(cafeId, null) → empty array.
Once branchesFetched=true, orderBranchId resolves to the correct branchId
(or null for café-wide fallback).

Layout: desktop order screen now shows a left vertical category sidebar
(116 px, md+) instead of horizontal chips, giving the classic POS sidebar
feel. Horizontal chips kept for mobile (<md). Menu grid columns adjusted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 07:10:03 +03:30
soroush.asadi f02f78a97c fix(pos): POS v2 menu empty — resolve a valid branch like classic POS
CI/CD / CI · API (dotnet build + test) (push) Successful in 42s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 31s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m6s
CI/CD / CI · Admin Web (tsc) (push) Successful in 38s
CI/CD / CI · Website (tsc) (push) Successful in 45s
CI/CD / CI · Koja (tsc) (push) Successful in 49s
CI/CD / Deploy · all services (push) Successful in 2m37s
The menu/tables are branch-scoped. v2 used the raw stored branchId, which is
null or stale for users who never opened the classic POS (it has no branch
picker), so getBranchMenu returned an empty menu. Now v2 fetches /branches,
auto-selects the first valid branch (self-healing the stored id), and loads the
branch menu + tables + order submission against that resolved branch — matching
the classic POS exactly. Also adds a visible "menu failed to load / retry"
state instead of a silent empty grid.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 06:17:37 +03:30
soroush.asadi cc0933c514 fix(auth): don't log out fullscreen routes (POS/queue) on refresh
CI/CD / CI · API (dotnet build + test) (push) Successful in 42s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 31s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m4s
CI/CD / CI · Admin Web (tsc) (push) Successful in 36s
CI/CD / CI · Website (tsc) (push) Successful in 45s
CI/CD / CI · Koja (tsc) (push) Successful in 48s
CI/CD / Deploy · all services (push) Successful in 2m35s
The (fullscreen) layout redirected to /login whenever user.accessToken was
falsy — but on a page refresh that fires before Zustand finishes rehydrating
the persisted auth from localStorage, so an authenticated user was bounced to
login on every refresh. Gate the redirect on _hasHydrated (and show a loader
while rehydrating), matching RouteGuard. Tokens themselves are already long
(30d access / 365d refresh), so sessions now survive refreshes as expected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 05:56:11 +03:30
soroush.asadi 7c35984096 feat(pos): default the pay sheet to Card
CI/CD / CI · API (dotnet build + test) (push) Successful in 40s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 31s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m6s
CI/CD / CI · Admin Web (tsc) (push) Successful in 37s
CI/CD / CI · Website (tsc) (push) Successful in 44s
CI/CD / CI · Koja (tsc) (push) Successful in 48s
CI/CD / Deploy · all services (push) Successful in 2m42s
Card is now the pre-selected payment method (and split rows default to Card),
matching Iran's card-dominant payments. Card already sits first in the selector.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 05:38:13 +03:30
soroush.asadi bf0ca68fa6 feat(pos): show Card first in pay sheet, keep Cash as default
CI/CD / CI · API (dotnet build + test) (push) Successful in 43s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 31s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m6s
CI/CD / CI · Admin Web (tsc) (push) Has been cancelled
CI/CD / CI · Website (tsc) (push) Has been cancelled
CI/CD / CI · Koja (tsc) (push) Has been cancelled
CI/CD / Deploy · all services (push) Has been cancelled
Reorder the payment method tabs to کارت / نقدی / تقسیم (Card first, most common
in Iran) while keeping Cash as the pre-selected default method.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 05:32:26 +03:30
soroush.asadi 6778c32028 feat(pos): POS v2 feature parity + promote to default /pos
CI/CD / CI · API (dotnet build + test) (push) Successful in 45s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 32s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m7s
CI/CD / CI · Admin Web (tsc) (push) Successful in 38s
CI/CD / CI · Website (tsc) (push) Successful in 45s
CI/CD / CI · Koja (tsc) (push) Successful in 49s
CI/CD / Deploy · all services (push) Successful in 2m46s
Completes the four POS v2 roadmap items:

1. Real split payments — split tab records N separate payment rows (equal split,
   last row takes the remainder), each row toggles Cash/Card; posts payments[].
2. Card-terminal push — confirmPay sums Card amounts and calls requestPosPayment
   (POS device) before recording; surfaces POS_DEVICE_* errors.
3. Customer + coupons + loyalty — reuses PosCustomerPicker (attach/search/create)
   and validates coupons via /coupons/validate (discount in totals). Pay sheet
   offers loyalty redemption (1 point = 100 toman) when a customer is attached.
4. Promote to default — /pos now renders POS v2 (full-screen, café-themed); the
   classic terminal moves to /pos-classic with its sidebar+topbar chrome. The
   "نسخه کلاسیک" link points there.

Order submission already carried customerId/guestName/guestPhone/couponId via the
shared cart store, so customer + coupon flow straight through send + pay.
tsc --noEmit clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 05:16:52 +03:30
soroush.asadi 75a0a1c834 feat(pos): wire POS v2 to live data (board, orders, payments)
CI/CD / CI · API (dotnet build + test) (push) Successful in 43s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 32s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m5s
CI/CD / CI · Admin Web (tsc) (push) Successful in 37s
CI/CD / CI · Website (tsc) (push) Successful in 45s
CI/CD / CI · Koja (tsc) (push) Successful in 49s
CI/CD / Deploy · all services (push) Successful in 2m42s
POS v2 is now a real, working point of sale at /[locale]/pos2 (was a static
mock). It reuses the existing data layer so it shares the React Query cache and
offline pipeline with the classic POS:

- Table board ← fetchCafeTableBoard (Free/Busy/Reserved/Cleaning, live totals,
  guest-QR badge); polls every 15s. Open a free table to start an order; open a
  busy table to hydrate its existing order (GET order → cart hydrateFromOrder).
- Order screen ← real branch/café menu + categories, bound to useCartStore
  (add/qty/remove). Send via submitOrderToApi (online + offline outbox) then
  re-hydrate; "ارسال (n)" shows the pending (unsynced) line count.
- Pay sheet ← POST /orders/{id}/payments. Cash (numpad + change), Card, and a
  Split helper (records the full amount; split is cashier guidance for now).
- Online/offline badge, loading/empty states, toasts, busy overlay, and a
  "نسخه کلاسیک" link back to /pos.

The static design mock stays at /[locale]/pos2-preview (dev-only, 404 in prod).
tsc --noEmit clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 00:16:37 +03:30
soroush.asadi 8a8eaf37e0 chore: never line-ending-convert cert files (.crt/.pem/.cer)
CI/CD / CI · API (dotnet build + test) (push) Successful in 2m27s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 34s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m8s
CI/CD / CI · Admin Web (tsc) (push) Successful in 41s
CI/CD / CI · Website (tsc) (push) Successful in 46s
CI/CD / CI · Koja (tsc) (push) Successful in 51s
CI/CD / Deploy · all services (push) Successful in 3m19s
Protects docker/nexus-mirror-ca.crt from CRLF corruption on Windows commits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 23:23:42 +03:30
soroush.asadi 9a27858125 ci: trust Nexus mirror CA in backend dotnet restore (fixes skipped deploys)
The mirror's Let's Encrypt cert renewed under the new ISRG Root YR root,
which isn't in the dotnet SDK image's trust store. `dotnet restore` validates
TLS and fails (NU1301 / unable to get local issuer certificate), so both
backend CI jobs fail and the deploy is skipped. The npm jobs are unaffected
because they already pass --strict-ssl=false.

Pin the mirror's intermediate (CN=YR2, CA:TRUE, valid to Sept 2028) and add it
as a trust anchor before restore in:
- CI api-build + admin-api-build jobs (.gitea/workflows/ci-cd.yml)
- docker/api/Dockerfile + docker/admin-api/Dockerfile (deploy image builds)

Also set NUGET_CERT_REVOCATION_MODE=offline in the CI restore steps to avoid
CRL/OCSP fetches to lencr.org (filtered from Iran).

Permanent fix is server-side (re-chain to ISRG Root X1 or update trust stores);
this unblocks CI/deploys without depending on that.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 23:22:57 +03:30
soroush.asadi 5078af2dd7 feat(pos): clickable POS v2 redesign prototype at /pos2 (static, no backend)
CI/CD / CI · API (dotnet build + test) (push) Failing after 3m18s
CI/CD / CI · Admin API (dotnet build) (push) Failing after 3m17s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m9s
CI/CD / CI · Admin Web (tsc) (push) Successful in 39s
CI/CD / CI · Website (tsc) (push) Successful in 46s
CI/CD / CI · Koja (tsc) (push) Successful in 1m2s
CI/CD / Deploy · all services (push) Has been skipped
Responsive RTL big-touch reimagining of the POS order screen for judging the
redesign on real devices before decomposing the 1568-line pos-screen.tsx + wiring
real logic. Self-contained: mock menu + local cart, no API/store/SignalR.

- 3 zones: category chips + item grid · order ticket · sticky action bar.
- lg+ side-panel ticket; smaller screens get a "view order" bar + slide-over
  (covers landscape tablet, portrait tablet, phone).
- Big-touch (56px primary / 44px qty), brand green #0F6E56, Toman totals.
  Send/Pay are mock toasts. tsc clean.
2026-06-03 17:23:58 +03:30
soroush.asadi 4123654077 build(meezi_app): Android Maven mirrors for Iran (Aliyun)
CI/CD / CI · API (dotnet build + test) (push) Successful in 12m40s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 10m30s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m7s
CI/CD / CI · Admin Web (tsc) (push) Successful in 39s
CI/CD / CI · Website (tsc) (push) Successful in 46s
CI/CD / CI · Koja (tsc) (push) Successful in 50s
CI/CD / Deploy · all services (push) Successful in 23s
Google's Android maven2 artifacts (AGP, androidx, Kotlin) 404 from Iran like
pub.dev does. Route Gradle resolution through the reachable Aliyun mirrors:

- android/settings.gradle.kts (pluginManagement) + build.gradle.kts (allprojects)
  now list maven.aliyun.com/repository/{gradle-plugin,google,central} before the
  originals (kept as fallback).
- BUILD_IRAN.md documents the full setup incl. the machine-local GRADLE_USER_HOME
  init.gradle needed for Flutter's included flutter_tools/gradle build.

Verified: dependency resolution now succeeds via the mirrors (AGP + kotlin-compiler
download from Aliyun). The APK build itself is currently blocked only by low disk
space on this machine, not configuration.
2026-06-03 10:50:17 +03:30
soroush.asadi 55e0c9499d content(website): reflect latest features across all pages
CI/CD / CI · API (dotnet build + test) (push) Successful in 44s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 34s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m5s
CI/CD / CI · Admin Web (tsc) (push) Successful in 38s
CI/CD / CI · Website (tsc) (push) Successful in 45s
CI/CD / CI · Koja (tsc) (push) Successful in 49s
CI/CD / Deploy · all services (push) Successful in 1m49s
Full pass over the marketing site so every page reflects the current product:

- Features page: +"Works Offline" and +"Get Discovered on Koja" cards (NEW badges);
  rewrote "Real-time Notifications" to describe the dashboard sound+toast alert.
- Solutions: added offline / Koja / real-time-alert bullets across cafés,
  restaurants, chains, and cloud kitchens.
- Tour: POS step now mentions offline + auto-sync; kitchen step describes the
  sound+toast new-order alert on any screen.
- Docs: +Offline Mode and +Koja Discovery module guides (fa/en).
- appPromo: waiter-app "new order" alert notes sound.
- privacy/terms: "Last updated" June 2025 → June 2026.

Website build clean.
2026-06-03 08:37:42 +03:30
soroush.asadi c8ea364ca2 build(meezi_app): Flutter Koja now builds for web (mirror + platform gen + fixes)
CI/CD / CI · API (dotnet build + test) (push) Successful in 43s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 31s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m5s
CI/CD / CI · Admin Web (tsc) (push) Successful in 39s
CI/CD / CI · Website (tsc) (push) Successful in 45s
CI/CD / CI · Koja (tsc) (push) Successful in 50s
CI/CD / Deploy · all services (push) Successful in 23s
Connectivity fixed: pub.dev/googleapis are 403 under sanctions, so PUB_HOSTED_URL
+ FLUTTER_STORAGE_BASE_URL now point at the reachable Flutter mirror
(pub.flutter-io.cn / storage.flutter-io.cn) — set persistently on the build machine.

- `flutter create --platforms=android,web --org ir.meezi` generated android/ + web/
  (#33). `flutter build web` succeeds; `flutter analyze` errors all cleared.
- pubspec: intl ^0.19.0 → ^0.20.2 (SDK pins 0.20.2 via flutter_localizations).
- Fixed 3 pre-existing compile errors (app had never been built):
  * attendance: JalaliFormatter.yyyyMMdd() removed → build the date from year/month/day.
  * qr_scan: dropped a call to a non-existent CartNotifier.setTable (table context is
    already set via tableContextProvider just above).
  * widget_test: default counter test referenced MyApp → minimal MeeziApp smoke test.
- discover_screen: drop redundant foundation import; value → initialValue (non-deprecated).

Verified: flutter build web ✓. (Android packaging still needs a Gradle/Maven mirror.)
2026-06-03 08:20:37 +03:30
soroush.asadi af1794925d feat(meezi_app): café profile parity — cover, open badge, gallery, hours (code-only)
CI/CD / CI · API (dotnet build + test) (push) Successful in 41s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 33s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m5s
CI/CD / CI · Admin Web (tsc) (push) Successful in 38s
CI/CD / CI · Website (tsc) (push) Successful in 45s
CI/CD / CI · Koja (tsc) (push) Successful in 50s
CI/CD / Deploy · all services (push) Successful in 22s
Enhances the café detail screen toward web-Koja parity. Parsing verified against
the real backend DTOs (CafePublicDto / WorkingHoursPublicDto), still unbuilt (pub blocked).

- Cover image hero (coverImageUrl), open/closed badge (isOpenNow).
- Photo gallery (galleryUrls) horizontal strip.
- Working hours rendered from the day-keyed WorkingHoursPublicDto ({sat..fri} of
  {isOpen,open,close}), Sat→Fri with Persian day labels.
2026-06-03 08:00:22 +03:30
soroush.asadi 2652736d31 feat(meezi_app): discovery screen parity — rich filters + taxonomy (code-only)
Brings the Flutter discover screen toward web-Koja parity. Unverified (pub blocked).

- DiscoverFilters is now a copyWith class so the many optional filters set safely.
- Adds an "open now" chip, rating chips, sort, and a taxonomy-driven filter sheet
  (themes/vibes/occasions/space-features as multi-select chips + price tier),
  feeding the rich discover() query. Active-filter badge + pull-to-refresh.
- Café cards show open/closed status.
2026-06-03 07:52:49 +03:30
soroush.asadi 1d79dde5e1 feat(meezi_app): Meezi green theme + rich discovery API (Koja parity, code-only)
CI/CD / CI · API (dotnet build + test) (push) Successful in 42s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 37s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m5s
CI/CD / CI · Admin Web (tsc) (push) Successful in 38s
CI/CD / CI · Website (tsc) (push) Successful in 45s
CI/CD / CI · Koja (tsc) (push) Successful in 50s
CI/CD / Deploy · all services (push) Successful in 23s
Head-start on the Koja-Flutter build while pub access is unavailable (pub.dev 403
under sanctions). NOT yet built/verified — needs `flutter create` + `pub get` once
package access is restored.

- core/theme/app_theme.dart: centralized MeeziTheme (brand green #0F6E56, Material 3,
  filled/outlined buttons, inputs), wired into main.dart (was a brown seed, no theme).
- public_api.dart: discover() gains the full filter set (themes/vibes/occasions/
  spaceFeatures/noise/priceTier/size/openNow) + discoverNearby/nlpParse/discoverTaxonomy,
  matching the web Koja's backend surface. Follows the existing dio pattern.
2026-06-03 07:33:12 +03:30
soroush.asadi 45dab8b253 test: update ReportPlanGate test for maxDays signature
CI/CD / CI · API (dotnet build + test) (push) Successful in 41s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 32s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m5s
CI/CD / CI · Admin Web (tsc) (push) Successful in 38s
CI/CD / CI · Website (tsc) (push) Successful in 44s
CI/CD / CI · Koja (tsc) (push) Successful in 50s
CI/CD / Deploy · all services (push) Successful in 1m32s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 07:00:49 +03:30
soroush.asadi e46d833371 feat(plans): report-history + AI-3D limits read from the catalog (S3 finish)
CI/CD / CI · API (dotnet build + test) (push) Successful in 43s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 31s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m4s
CI/CD / CI · Admin Web (tsc) (push) Has been cancelled
CI/CD / CI · Website (tsc) (push) Has been cancelled
CI/CD / CI · Koja (tsc) (push) Has been cancelled
CI/CD / Deploy · all services (push) Has been cancelled
The last two limits that still read hardcoded PlanLimits now come from the
admin-editable catalog, so editing them in the admin panel takes effect:

- ReportPlanGate is now limit-driven (takes int maxDays, not a tier); ReportsController
  resolves MaxReportHistoryDays from catalog.GetLimitsAsync. LimitMessage is generic
  (reflects the actual days). EnsureReportDateAllowed is now async.
- MenuAi3dGenerationService.ResolveLimitAsync reads MaxMenuAi3dPerMonth from the catalog.

Every plan limit + feature gate is now DB-driven and admin-editable. 86 tests pass.
2026-06-03 06:57:59 +03:30
soroush.asadi dcdb0d5747 feat(realtime): global guest-order alert on the dashboard
CI/CD / CI · API (dotnet build + test) (push) Successful in 47s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 32s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m4s
CI/CD / CI · Admin Web (tsc) (push) Successful in 37s
CI/CD / CI · Website (tsc) (push) Successful in 44s
CI/CD / CI · Koja (tsc) (push) Successful in 49s
CI/CD / Deploy · all services (push) Successful in 3m9s
Guest orders from the QR/digital menu already notified via SignalR, but only
screens that were open (KDS/POS/tables) reacted — and silently (a data refresh,
no alert). So staff on any other screen never knew a menu order arrived.

- Add a global useOrderAlerts() mounted in the dashboard shell: connects to
  /hubs/kds, joins the café group, and on a new GUEST order plays a chime + shows
  a toast (localized fa/en/ar) + nudges order/KDS/POS lists to refresh — on every
  screen.
- Filter to guest QR-menu orders only (not staff POS orders): LiveOrderDto now
  carries Source, set in MapLiveOrder (+ the delivery/snappfood mappers).

86 API tests pass; dashboard tsc + build clean.
2026-06-03 02:42:29 +03:30
soroush.asadi 9b2f15151d feat(website): reflect new features + 5-tier pricing
CI/CD / CI · API (dotnet build + test) (push) Successful in 50s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 45s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m4s
CI/CD / CI · Admin Web (tsc) (push) Successful in 36s
CI/CD / CI · Website (tsc) (push) Successful in 44s
CI/CD / CI · Koja (tsc) (push) Successful in 49s
CI/CD / Deploy · all services (push) Successful in 3m18s
- Pricing: add the Starter tier (now Free·Starter·Pro·Business·Enterprise),
  fix currency ₺ (Turkish Lira) → Toman, and rewrite every plan's bullets to the
  agreed matrix (Free: 6 tables/30 orders/Koja/offline + watermark; Starter:
  watermark-removal/custom-styling/review-reply; Pro: CRM/reports/taxes/payroll/
  delivery/3 branches; Business: 3D + AI-3D + unlimited; Enterprise: API/white-label/
  SLA/24-7). 5-column responsive grid.
- Features: add two headliner cards that were missing — "Works offline" and
  "Get discovered on Koja". fa/en.

Website tsc + build clean.
2026-06-03 02:20:16 +03:30
soroush.asadi 7d06f149d3 feat(plans): menu watermark on Free (removed by paid feature)
Guest QR menu shows a "ساخته‌شده با میزی" watermark under the menu unless the café's
plan has the `watermark_removed` feature (Starter+).

- PublicMenuDto gains ShowWatermark; PublicService computes it from
  IsFeatureEnabledForCafeAsync("watermark_removed") for both slug and branch menus.
- Guest menu renders the watermark footer when showWatermark.
- NoOpPlatformCatalogService test double (all features on) for the PublicService
  ctor; QrMenuTests updated.

86 tests pass; dashboard tsc clean.
2026-06-03 02:10:24 +03:30
soroush.asadi 2487f9e30f feat(plans): Stage 3b — DB-driven gates for reviews/styling/limits
CI/CD / CI · API (dotnet build + test) (push) Successful in 1m0s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 42s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m5s
CI/CD / CI · Admin Web (tsc) (push) Successful in 38s
CI/CD / CI · Website (tsc) (push) Successful in 45s
CI/CD / CI · Koja (tsc) (push) Successful in 49s
CI/CD / Deploy · all services (push) Successful in 2m51s
Make more plan rules read the admin-editable catalog instead of hardcoded values:
- Review reply gated by the `review_reply` feature (Starter+) — 403 if not in plan.
- Custom menu styling gated by `custom_menu_styling` (Starter+): only blocks an
  actual theme change, so a normal settings save re-sending the current theme is fine.
- Menu categories/items limits now read catalog.GetLimitsAsync (Free categories
  editable; message no longer hardcodes a number).
- Terminals limit reads the catalog (enforcement in TerminalRegistryService +
  the displayed max in TerminalsController).

Remaining (small): menu watermark (Free shows it, `watermark_removed` removes it —
needs the public-menu render), report-history (static ReportPlanGate) and AI-3D
routing — these already enforce the correct matrix values, just not yet editable.

86 tests pass; build clean.
2026-06-03 01:40:00 +03:30
soroush.asadi 8f738f6469 feat(plans): Stage 4 — full admin plan/feature editor
The admin → Plans screen now edits EVERYTHING per plan (the backend already
accepted it; only the UI was partial):
- All limits (orders/day, tables, terminals, branches, menu categories, menu
  items, customers, report history, SMS, AI-3D) with an "unlimited (∞)" toggle.
- Display names (fa/en), monthly price, sort order, billable-online, active on/off.
- Per-plan feature checkboxes grouped by module, plus an "all features (*)" toggle
  (Enterprise). Sourced from the live feature catalog (/api/admin/features).
- Plans listed in sort order (Free·Starter·Pro·Business·Enterprise).
- i18n fa/en/ar.

Admin tsc + build clean.
2026-06-03 01:11:18 +03:30
soroush.asadi 7f52b2823f feat(plans): Stage 3a — enforce tables cap from the DB catalog
PlanLimitChecker already enforces orders/customers/branches/SMS from the
admin-editable catalog (GetLimitsAsync). Add the tables cap the same way
(POST /api/cafes/{cafeId}/tables → MaxTables), so Free's 6-table limit is both
enforced and admin-editable. Terminals/categories/report-history already enforce
the correct matrix values via PlanLimits defaults; routing them through the
catalog for editability + the watermark/styling/review-reply feature gates are
the remaining S3 items.

86 tests pass.
2026-06-03 00:58:49 +03:30
soroush.asadi c5d5a4006a feat(plans): Stage 2 — seed 5-tier matrix + feature catalog (editable defaults)
- 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.
2026-06-03 00:53:02 +03:30
soroush.asadi 4cb640814a feat(plans): Stage 1 — Starter tier + admin-editable limit model
Foundation for the configurable plan system (Free·Starter·Pro·Business·Enterprise).

- PlanTier: append Starter=4 (no renumber → no data migration; existing tier ints
  keep meaning). Ordering/display via PlanDefinition.SortOrder; gating uses explicit
  tier sets, never `tier >= X`.
- PlanLimits: locked 5-tier matrix — Free orders/day 50→30, tables (new) Free 6/
  Starter 15/Pro 40/∞, categories Free 3→10, menu items now unlimited, terminals/
  branches/report-history ladders incl. Starter; CRM/analytics = explicit Pro+; AI-3D
  = Business+. SMS quotas kept (Free/Starter 0, Pro 50, Business 200) until the
  pay-as-you-go credit system ships (don't break paid SMS).
- PlanLimitsData (LimitsJson shape): + MaxTables/MaxMenuCategories/MaxMenuItems/
  MaxMenuAi3dPerMonth; ForTier now derives from PlanLimits (single source of truth).

No migration. 86 tests pass. Next: S2 seed 5 plans + feature catalog (editable),
S3 wire enforcement to DB, S4 admin editor.
2026-06-03 00:40:37 +03:30
soroush.asadi 4c98c2cce1 feat(auth): extend token lifetimes for long offline gaps
CI/CD / CI · API (dotnet build + test) (push) Successful in 1m24s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 1m1s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m5s
CI/CD / CI · Admin Web (tsc) (push) Successful in 37s
CI/CD / CI · Website (tsc) (push) Successful in 46s
CI/CD / CI · Koja (tsc) (push) Successful in 50s
CI/CD / Deploy · all services (push) Successful in 2m16s
A user can be offline for months (offline-first dashboard) and must stay logged
in / be able to sync on reconnect. Access 7d→30d, refresh 30d→365d, so a ~3-month
offline gap still has a valid refresh token on reconnect (queued writes sync, no
forced logout). Client only logs out on a server 401, never while offline.
2026-06-02 23:47:06 +03:30
soroush.asadi db0c3a4a02 feat(hr): add employees from the dashboard
CI/CD / CI · API (dotnet build + test) (push) Successful in 1m10s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 1m40s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m5s
CI/CD / CI · Admin Web (tsc) (push) Successful in 1m32s
CI/CD / CI · Website (tsc) (push) Successful in 45s
CI/CD / CI · Koja (tsc) (push) Successful in 49s
CI/CD / Deploy · all services (push) Successful in 9m24s
Previously the only Employee records were the Owner (created at café signup) and
one Manager per branch — there was no way to add a waiter/cashier/chef. Adds it.

Backend:
- POST /api/cafes/{cafeId}/employees (HrController). Owner/Manager only; creating a
  Manager requires Owner; Owner cannot be created here. Validates name/phone/role,
  enforces one-employee-per-phone, validates branch belongs to the café, and can
  optionally set username/password login in the same step (same hashing + uniqueness
  as the credentials endpoint). Returns EmployeeSummaryDto.

Dashboard:
- New "Team" tab on the HR screen (now the default): employee roster (name, role,
  phone, base salary) + an "Add employee" button (owner/manager) opening an inline
  form — name, phone, role, optional branch, optional base salary, optional login.
- Role labels + all form strings in fa/en/ar.

86 API tests pass; dashboard tsc + build clean.
2026-06-02 23:28:36 +03:30
soroush.asadi f1756b491e feat(admin): rich text editor for blog content (TipTap)
CI/CD / CI · API (dotnet build + test) (push) Successful in 3m33s
CI/CD / CI · Admin API (dotnet build) (push) Failing after 3m18s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m6s
CI/CD / CI · Admin Web (tsc) (push) Failing after 3m43s
CI/CD / CI · Website (tsc) (push) Successful in 46s
CI/CD / CI · Koja (tsc) (push) Successful in 49s
CI/CD / Deploy · all services (push) Has been skipped
Blog post bodies were plain <textarea>s labelled "Markdown". Replace with a
TipTap rich editor (bold/italic/strike, H1–H3, lists, blockquote, code, links,
undo/redo), RTL-aware, producing HTML.

- New RichTextEditor component (TipTap v2: react + starter-kit + pm + link +
  placeholder), immediatelyRender:false for Next SSR, self-contained content
  styling, external-value sync.
- Wired into the FA/EN content fields of the blog editor; labels no longer say
  "Markdown" (fa/en/ar).
- Website blog page now renders HTML when the body is HTML and falls back to
  MDXRemote for older Markdown posts (backward-compatible). Content is authored
  only by trusted SystemAdmins, so HTML is stored/rendered directly.

Admin build + website typecheck clean.
2026-06-02 22:25:47 +03:30
soroush.asadi 97a9481627 feat(media): content-hash dedup for uploads + media-library endpoint
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.
2026-06-02 22:16:11 +03:30
soroush.asadi eb165db182 feat(offline): make every dashboard write durable offline (P2–P5)
CI/CD / CI · API (dotnet build + test) (push) Successful in 1m0s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 38s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m5s
CI/CD / CI · Admin Web (tsc) (push) Successful in 36s
CI/CD / CI · Website (tsc) (push) Successful in 44s
CI/CD / CI · Koja (tsc) (push) Successful in 49s
CI/CD / Deploy · all services (push) Successful in 3m11s
Builds on the outbox engine to take the whole dashboard offline in one place
instead of wiring 114 mutation sites individually.

Frontend (single chokepoint = the API client):
- offline-write: any write auto-queues to the outbox on offline/network failure
  and returns an optimistic value; the online path is unchanged apart from an
  Idempotency-Key header (so even online retries de-dup). entityType is derived
  from the URL; POSTs get a remappable local id.
- client.doWrite unifies POST/PUT/PATCH/DELETE through this path. WriteOptions
  gains `offline: "queue" | "reject" | "manual"`.
- Guardrails: auth / billing / payments / SMS / exports are online-only and throw
  OFFLINE_UNAVAILABLE offline rather than queueing (no queued double-charges or
  surprise SMS blasts). use-api-error resolves the friendly localized message
  (fa/en/ar).
- submit-order opts out ("manual") to keep its richer local-Order mock; shared
  helpers de-duplicated into offline-write.
- Request persistent storage on mount so unsynced writes survive eviction.

Backend:
- IdempotencyCleanupJob: daily purge of idempotency records older than 7 days
  (the table now gets a row per keyed write). Registered in Hangfire. No migration.

86 API tests pass; dashboard tsc + build clean.
2026-06-02 18:34:54 +03:30
soroush.asadi 3b468b48d9 feat(dashboard/offline): generic idempotent outbox + ID remapping
CI/CD / CI · API (dotnet build + test) (push) Successful in 48s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 53s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m5s
CI/CD / CI · Admin Web (tsc) (push) Successful in 35s
CI/CD / CI · Website (tsc) (push) Successful in 45s
CI/CD / CI · Koja (tsc) (push) Successful in 49s
CI/CD / Deploy · all services (push) Successful in 3m12s
Completes offline Phase 1 (frontend). Generalises the POS-orders-only queue into
a reusable write engine and fixes the two correctness bugs in the old path.

- offline-db: generic `outbox` store (DB v3, order_queue/kv preserved) with
  enqueue/list/update/remove + a persisted client→server id map.
- outbox.ts: drains in causal order — remaps local_* ids to server ids (blocking
  an op until its creator syncs), sends each op with its idempotency key, and
  classifies failures (offline → stop; 5xx / in-progress → retry; 4xx → poison
  after 5 attempts). remap/blocked logic validated against representative cases.
- client: apiPost/Put/Patch/Delete take an optional idempotencyKey →
  `Idempotency-Key` header; ApiClientError now carries HTTP status.
- submit-order: generates ONE idempotency key per submit, used for both the
  online attempt and the queued replay → server de-dups (no more double-create);
  offline create carries createsClientId so a later add-items remaps onto the
  real order instead of spawning a second order.
- use-offline-sync: drains the outbox, one-time migrates legacy order_queue
  items, invalidates queries after a successful sync.

tsc + production build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 18:19:29 +03:30
soroush.asadi f4583f5169 feat(api/offline): idempotency-key middleware for safe write retries
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>
2026-06-02 18:03:57 +03:30