Drop the duplicate customer app from the monorepo and keep user-app-v2 as the only app; include order pricing, admin cart, OTP/SMS, and dashboard cache updates.
Co-authored-by: Cursor <cursoragent@cursor.com>
Add paged orders API and infinite scroll, short order numbers, and align the guest invite card with the logged-in hero shell.
Co-authored-by: Cursor <cursoragent@cursor.com>
اپ: نام برند در همهی صفحهها و پیامها یکسان شده و از یک تنظیم مرکزی خوانده میشود.
اپ: کارت حسابداری در صفحهی مشتری بازطراحی شد؛ ماندهها و وضعیت لینک واضحتر نمایش داده میشوند.
پنل: کارت حسابداری در پروندهی مشتری بازطراحی شد؛ ماندهها، آخرین ارسال و فرم لینک دستی واضحتر شدهاند.
پنل: ستون «کد» در جدولها بهطور پیشفرض پنهان است و از انتخاب ستونها قابل نمایش است.
پنل: تغییر نمایش ستونها در جدولها بلافاصله اعمال میشود.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Cities (owner: as legacy)
- City.parentId (self relation) replaces the flat `province` label; the
migration turns each label into a parent row («استان …») first, so nothing
is lost.
- Admin pickers are legacy's tree (CityTreeSelect): the top level, the
country, is a heading. The city form and grid get «والد».
- Restored legacy _getP: a city without its own percent takes its
ancestor's. Restored the omde-customer form's city → percent autofill.
- Import copies legacy parents; delete refuses a city with children; a
parent loop is refused.
Permissions (owner approved the deletion)
- dashboard / dashboard.omde / dashboard.tak rows become dashboard.view,
then are deleted.
- Import maps them the same way: legacy types 102, 104, 105, 108 and 135
had lost the dashboard.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Legacy stored customers.sts but no code read it, so the switch never
stopped an order, and 313 active customers carry it, mostly as the
sign-up default. Owner option 2: enforce it from now on, for orders
placed from the app only (an operator can still order for the
customer), and import every customer as allowed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Legacy fixed `orders.reagent_id` once, when the order was placed, and its
«میزان فروش» summed `customer_id = X OR reagent_id = X`. The app credited
orders to the customer's CURRENT referrer and left out an expert's own
orders, so 9 of 49 imported experts showed a different figure.
- Order.salesAgentId (+ referrerCustomerId for 17 legacy orders whose
referrer was not an expert); additive migration, recent app orders
backfilled from the customer's referrer
- createOrder records the expert at placement time
- one shared filter (agent-orders.ts) for the experts list, expert
detail, dashboard and the expert app; child orders follow salesAgentId
- customer page gets its «میزان فروش» back
- import carries orders.reagent_id as is; ex-experts kept as admins keep
their profile so their own orders stay linked
- reconcile-sales: 1,557 of 1,558 match legacy's own formula; balances
unchanged
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
apps/api/scripts/legacy-import/ reads the legacy MySQL (read only) and
merges into the target Postgres. Every rule is from
docs/DATA-MIGRATION-MAP.md, and each step cites its section. It covers
cities, roles and permissions, branches, admin users, sales experts,
workshops, customers, brands, categories, products, variants, product
images, orders, the ledger, favourites, custom orders and a
reconciliation report.
Key rules:
- Debt is rebuilt from legacy's own formula (updateRemain): delivered
orders minus confirmed type 1-4 documents. Type 9 «سفارش» documents
mirror orders and are skipped, or every order's debt would count twice.
A balance the records cannot explain is carried as one ADJUSTMENT entry.
In the 2026-10-04 dump that is only customer #125 (owner ruling).
- Merge, not import: rows already in the target are matched by mobile or
product code, and only their empty fields are filled. Never written:
accounting* columns, ProductVariant.imageId, Accounting* tables. No
ProductImage is ever deleted.
- Roles map by title, not number. Legacy 86 is «حسابدار» but target 86
is «کارشناس فروش». An expert sharing a mobile with an admin never
demotes the admin.
- Soft-deleted variants and products that live order lines still point
at (473 of 610 lines) come over inactive, so order history stays whole.
- Owner rulings 2026-10-07: birthdays filtered (541 of 915), typed city
kept plus exact-match cityId, the 7 legacy rejections get the sentinel
reason, OTP-only accounts get OTP_ONLY_PASSWORD_HASH.
- Tehran wall-clock timestamps are converted with ICU rules, including
Iran's pre-2022 DST. Jalali receipt dates are converted with the
jalaali-js arithmetic (truncating division; verified on 8 known dates).
Safety: --target is required and never read from .env. Nothing is
written without --yes, and a target not named *import*/*staging* also
needs --confirm-db. The id map lives in its own schema, legacy_import,
so a re-run updates instead of duplicating. Writes bypass the API
services, so no SMS is sent and nothing is queued to accounting.
Tested against goldbank_import_test, a clone of the dev DB, in three
runs:
- 1,503/1,503 created customers match legacy gold and rial balances
(±0.0001 g / ±1 rial)
- accounting data is byte-identical before and after
- 0 imported variants have an imageId
- runs 2 and 3 created zero duplicate rows
- 6,196 image files copied, 3 missing in the legacy storage
New devDependency: mysql2 ^3.24.5.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>