Merge release/delivery (IAM single sign-on) into the triage work
Brings the four release/delivery commits (IAM SSO for the admin panel,
both apps and the API; IAM env in docker-compose) onto fix/issues-triage.
Conflicts:
- customer-auth.service.ts imports: both kept.
- both apps' api/client.ts: both kept (AccountingBalance/WageQuote, and
iamAppUrl).
- all build output (admin-panel, user-app, user-app-v2 dist, and the api
release bundle): rebuilt from the merged source instead of picking a
side.
Fixed while verifying:
- The IAM change calls iam.browserBootstrapUrl after every login, but
the identity-service stand-ins in three test files lacked it. 15 tests
failed on release/delivery itself.
- SMS relay: SMS_RELAY_PLATFORM was read but never sent, so the gateway
never got `platform` (documented body: phone, host, text, platform).
relayConfig() now returns it ("default" → null) and the request carries
it. sms.service.spec now passes.
All 249 API tests pass; the three front-ends type-check and build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+71865
-6435
File diff suppressed because one or more lines are too long
@@ -0,0 +1,110 @@
|
||||
-- Accounting integration (D27): Tahesab / Zarrin, chosen in the admin panel.
|
||||
-- Plan: docs/ACCOUNTING-INTEGRATION-PLAN.md.
|
||||
--
|
||||
-- Adds the link table (app row <-> accounting record), the outbox the push
|
||||
-- jobs run from, the issue list pulls report into, and the cached balance and
|
||||
-- stock columns read back from the accounting software. Nothing existing
|
||||
-- changes meaning: goldDebtGrams/fiatDebtRial stay the internal ledger caches.
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "AccountingEntityType" AS ENUM ('CUSTOMER', 'PRODUCT');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "AccountingOutboxKind" AS ENUM ('CUSTOMER_PUSH', 'PRODUCT_PUSH', 'ORDER_DOCUMENT', 'ORDER_DOCUMENT_DELETE');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "AccountingOutboxStatus" AS ENUM ('PENDING', 'SENDING', 'SENT', 'FAILED', 'NEEDS_REVIEW', 'RESOLVED');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Customer" ADD COLUMN "accountingBalanceAt" TIMESTAMP(3),
|
||||
ADD COLUMN "accountingGoldBalance" DECIMAL(14,4),
|
||||
ADD COLUMN "accountingRialBalance" DECIMAL(20,2);
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Product" ADD COLUMN "accountingStockAt" TIMESTAMP(3),
|
||||
ADD COLUMN "accountingStockCount" DECIMAL(14,4),
|
||||
ADD COLUMN "accountingStockWeight" DECIMAL(14,4);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AccountingLink" (
|
||||
"id" TEXT NOT NULL,
|
||||
"provider" TEXT NOT NULL,
|
||||
"entityType" "AccountingEntityType" NOT NULL,
|
||||
"entityId" TEXT NOT NULL,
|
||||
"externalCode" TEXT NOT NULL,
|
||||
"externalName" TEXT,
|
||||
"origin" TEXT NOT NULL,
|
||||
"snapshot" JSONB,
|
||||
"syncedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "AccountingLink_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AccountingOutbox" (
|
||||
"id" TEXT NOT NULL,
|
||||
"provider" TEXT NOT NULL,
|
||||
"kind" "AccountingOutboxKind" NOT NULL,
|
||||
"entityId" TEXT NOT NULL,
|
||||
"status" "AccountingOutboxStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"attempts" INTEGER NOT NULL DEFAULT 0,
|
||||
"nextAttemptAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"lastError" TEXT,
|
||||
"externalRef" TEXT,
|
||||
"progress" JSONB,
|
||||
"lastRequest" JSONB,
|
||||
"lastResponse" JSONB,
|
||||
"sentAt" TIMESTAMP(3),
|
||||
"resolvedById" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "AccountingOutbox_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AccountingIssue" (
|
||||
"id" TEXT NOT NULL,
|
||||
"provider" TEXT NOT NULL,
|
||||
"kind" TEXT NOT NULL,
|
||||
"externalCode" TEXT NOT NULL,
|
||||
"entityId" TEXT,
|
||||
"message" TEXT NOT NULL,
|
||||
"data" JSONB,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"resolvedAt" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "AccountingIssue_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AccountingLink_entityType_entityId_idx" ON "AccountingLink"("entityType", "entityId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "AccountingLink_provider_entityType_entityId_key" ON "AccountingLink"("provider", "entityType", "entityId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "AccountingLink_provider_entityType_externalCode_key" ON "AccountingLink"("provider", "entityType", "externalCode");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AccountingOutbox_status_nextAttemptAt_idx" ON "AccountingOutbox"("status", "nextAttemptAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AccountingOutbox_kind_entityId_idx" ON "AccountingOutbox"("kind", "entityId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AccountingIssue_resolvedAt_idx" ON "AccountingIssue"("resolvedAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "AccountingIssue_provider_kind_externalCode_key" ON "AccountingIssue"("provider", "kind", "externalCode");
|
||||
|
||||
|
||||
-- The new admin screen's permission. The seeded super-admin user type (85,
|
||||
-- prisma/seed.ts) gets every rid in ALL_RIDS on a fresh seed; an existing
|
||||
-- database has to be granted the new one here.
|
||||
INSERT INTO "permission" ("rid", "userTypeId")
|
||||
SELECT 'config.accounting', 85
|
||||
WHERE EXISTS (SELECT 1 FROM "user_type" WHERE "id" = 85)
|
||||
ON CONFLICT DO NOTHING;
|
||||
@@ -0,0 +1,210 @@
|
||||
-- ============================================================================
|
||||
-- Preparation for the legacy data migration — PURELY ADDITIVE.
|
||||
--
|
||||
-- Owner decisions of 2026-10-06 and 2026-10-07, recorded in
|
||||
-- docs/DATA-MIGRATION-MAP.md §5.2, §5.5, §10, §10.4, §10.5.5.
|
||||
--
|
||||
-- Nothing here drops, renames or retypes an existing column. Every statement
|
||||
-- either removes a constraint or adds a new column/table, so code that works
|
||||
-- today keeps working. Verified NOT touched:
|
||||
-- * Customer.accounting{Gold,Rial}Balance / accountingBalanceAt (D27)
|
||||
-- * Product.accountingStock{Weight,Count} / accountingStockAt (D27)
|
||||
-- * ProductVariant.imageId (PROTECTED.md §B)
|
||||
-- * AccountingLink / AccountingOutbox / AccountingIssue
|
||||
-- * SmsMessage and the SMS module
|
||||
-- These are target-only capabilities under PROTECTED.md §G ("a keep-out
|
||||
-- list"), and the owner confirmed on 2026-10-06 that their data is real.
|
||||
--
|
||||
-- D15: idempotent (IF NOT EXISTS / duplicate_object guards) because this runs
|
||||
-- under BOTH ledgers — the Prisma CLI in development and the app's own runner
|
||||
-- (src/prisma/migrate.ts) in the deploy container, which wraps each file in a
|
||||
-- transaction. The runner's splitSqlStatements() handles $$-quoted blocks
|
||||
-- (migrate.ts:83-91), so the DO blocks below are split correctly.
|
||||
-- ============================================================================
|
||||
|
||||
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- 1. Customer.nationalCode becomes OPTIONAL
|
||||
--
|
||||
-- Source reality (docs/migration-precheck.sql, run 2026-10-06 on the live
|
||||
-- `oapi` MySQL schema): empty_ncode = 1595 of n_customers = 1595. NOT ONE of
|
||||
-- the 1,616 legacy customers has a national code. dup_ncode_groups = 0.
|
||||
--
|
||||
-- Owner ruling 2026-10-07: "ساختن کد ملی فیک تخلف دیتایی است و مانع از
|
||||
-- احراز هویتهای قانونی بعدی میشود" — fabricating one is a data violation, so
|
||||
-- the column must be nullable. Without this the customer migration cannot
|
||||
-- insert a single row.
|
||||
--
|
||||
-- This also RETIRES three existing workarounds that only exist because the
|
||||
-- column is required. They write the MOBILE NUMBER into the national-code
|
||||
-- field, i.e. they store wrong data today:
|
||||
-- * admin-customers.service.ts:625 nationalCode: mobile
|
||||
-- comment: "schema requires it and it is unique; singleCustomer.vue
|
||||
-- collects no ncode"
|
||||
-- * admin-customers.service.ts:685 nationalCode: nationalCode || mobile
|
||||
-- * customer-auth.service.ts:258 ... || admin.mobile
|
||||
-- Removing those fallbacks is a SERVICE-LAYER change, deliberately NOT done
|
||||
-- here: this migration only makes it possible. Until they are removed the
|
||||
-- behaviour is exactly as it is today.
|
||||
--
|
||||
-- The UNIQUE index "Customer_nationalCode_key" (first/migration.sql:304) is
|
||||
-- left in place. In PostgreSQL NULLs are not equal to each other, so any
|
||||
-- number of rows may hold NULL under a unique index — which is precisely why
|
||||
-- the migration tool must write NULL and never the empty string: '' would
|
||||
-- collide on the second row (see §10.4).
|
||||
--
|
||||
-- DROP NOT NULL takes only a brief ACCESS EXCLUSIVE lock on the catalog; it
|
||||
-- does not rewrite the table and does not scan the data.
|
||||
ALTER TABLE "Customer" ALTER COLUMN "nationalCode" DROP NOT NULL;
|
||||
|
||||
COMMENT ON COLUMN "Customer"."nationalCode" IS
|
||||
'Legacy customers.ncode. OPTIONAL: no legacy customer has one (1595/1595 empty as of 2026-10-06), and fabricating a value is a data violation (owner, 2026-10-07). Still UNIQUE — write NULL, never '''', because NULLs do not collide under a unique index but empty strings do.';
|
||||
|
||||
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- 2. Customer.birthDate — new, nullable, unread until backfilled
|
||||
--
|
||||
-- Legacy `customers.birth_date timestamp NULL` (oapi dump line 484), populated
|
||||
-- on 917 of 1,595 live rows (~57%). Owner ruling 2026-10-07: migrate it —
|
||||
-- "نرخ پرشدگی بالای ۵۵٪ است و داده برای کمپینهای وفاداری و پیامکی کاربرد
|
||||
-- مستقیم دارد".
|
||||
--
|
||||
-- Two sibling columns were deliberately NOT ported, same ruling:
|
||||
-- * marriage_date (5 rows) — "۵ ردیف توجیه معماری ندارد"
|
||||
-- * token_mobile (99 rows) — "توکنها اغلب منقضی شدهاند · زباله دیتایی"
|
||||
--
|
||||
-- ⚠ THE COLUMN IS CREATED EMPTY ON PURPOSE. Do not backfill it until the
|
||||
-- calendar question is settled (§5.9): legacy stores some dates as JALALI
|
||||
-- STRINGS elsewhere (cashes.recive_date is varchar(255), 88 of 89 values
|
||||
-- starting "14…"), so birth_date may hold Jalali years mis-entered into a
|
||||
-- timestamp. Copying those unchanged would silently produce 917 WRONG
|
||||
-- birthdays, which for an SMS campaign is worse than having none.
|
||||
-- docs/migration-precheck-3.sql answers this.
|
||||
ALTER TABLE "Customer" ADD COLUMN IF NOT EXISTS "birthDate" TIMESTAMP(3);
|
||||
|
||||
COMMENT ON COLUMN "Customer"."birthDate" IS
|
||||
'Legacy customers.birth_date (917 of 1595 rows populated). Created empty: verify Jalali-vs-Gregorian with docs/migration-precheck-3.sql BEFORE backfilling — legacy stores dates as Jalali strings in other tables.';
|
||||
|
||||
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- 3. Rejection trail for document verification
|
||||
--
|
||||
-- Legacy has THREE states per verification step, not two
|
||||
-- (MajorCustomerController.php:106-135): 1 = awaiting review, 2 = approved,
|
||||
-- 3 = REJECTED. The target's VerificationStep enum has no rejected state, so
|
||||
-- 14 already-rejected customers had nowhere to go.
|
||||
--
|
||||
-- Owner ruling 2026-10-07: mapping them to "awaiting" is "کاملاً مردود" —
|
||||
-- it would put closed cases back in the review queue and create duplicate
|
||||
-- work. REJECTED is added to the enum (statement 4 below) and these two
|
||||
-- columns carry the reason.
|
||||
--
|
||||
-- Legacy records NO reason: all 49 `customers` columns were checked and none
|
||||
-- is a reason/note/description field, and on rejection the server sends only
|
||||
-- a FIXED SMS ("اطلاعات شما تایید نشده است. لطفا با پیشتیبانی تماس حاصل
|
||||
-- فرمایید") with no cause stored anywhere. So for those 14 rows the reason is
|
||||
-- unrecoverable; the owner asked the tool to write an explicit sentinel rather
|
||||
-- than NULL, so support knows WHY it is blank:
|
||||
-- رد شده در سیستم قدیم — فاقد شرح علت ثبتشده
|
||||
-- That text is written by the migration tool, not by this migration, and only
|
||||
-- WHERE verificationRejectionReason IS NULL so a re-run can never overwrite a
|
||||
-- real reason entered in the new system (§5.5).
|
||||
--
|
||||
-- verificationRejectedAt stays NULL for the 14 legacy rows: legacy did not
|
||||
-- record a rejection timestamp either.
|
||||
ALTER TABLE "Customer" ADD COLUMN IF NOT EXISTS "verificationRejectionReason" TEXT;
|
||||
ALTER TABLE "Customer" ADD COLUMN IF NOT EXISTS "verificationRejectedAt" TIMESTAMP(3);
|
||||
|
||||
COMMENT ON COLUMN "Customer"."verificationRejectionReason" IS
|
||||
'Why document verification was rejected. Legacy stored no reason (only a fixed SMS), so rows migrated from henza carry the sentinel «رد شده در سیستم قدیم — فاقد شرح علت ثبتشده». Write only WHERE this IS NULL so a re-run never overwrites a real reason.';
|
||||
|
||||
COMMENT ON COLUMN "Customer"."verificationRejectedAt" IS
|
||||
'When verification was rejected. NULL for rows migrated from henza — legacy did not record a rejection timestamp.';
|
||||
|
||||
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- 4. VerificationStep gains REJECTED
|
||||
--
|
||||
-- ADD VALUE is additive: existing rows and existing queries are unaffected.
|
||||
-- IF NOT EXISTS makes a re-run a no-op.
|
||||
--
|
||||
-- ⚠ CONSUMER AUDIT STILL OWED. Every read of VerificationStep must be checked
|
||||
-- for a Persian label / switch that does not yet know this value, or the admin
|
||||
-- panel will render a blank cell. Flagged in §10.5.5 as a separate task; it is
|
||||
-- a display gap, not a data risk, and nothing writes REJECTED until the
|
||||
-- migration tool runs.
|
||||
--
|
||||
-- Postgres note: before v12 ADD VALUE could not run inside a transaction, and
|
||||
-- the app's own runner wraps each file in one (migrate.ts:198). This project
|
||||
-- targets a modern Postgres where that restriction is lifted. If the deploy
|
||||
-- container ever errors with "ALTER TYPE ... ADD VALUE cannot be executed
|
||||
-- inside a transaction block", this statement must move to its own file.
|
||||
DO $$
|
||||
BEGIN
|
||||
ALTER TYPE "VerificationStep" ADD VALUE IF NOT EXISTS 'REJECTED';
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- 5. CustomerFavorite — the legacy `fav` table finally gets a home
|
||||
--
|
||||
-- Legacy `fav` holds 251 rows (customer_id, product_id) and the target had NO
|
||||
-- model for it, so every customer's saved products would have been lost.
|
||||
-- Owner ruling 2026-10-06: they matter. docs/PWA-PORT-PLAN.md reached the same
|
||||
-- conclusion independently.
|
||||
--
|
||||
-- Legacy `fav` has only two columns and NO primary key, so duplicate pairs are
|
||||
-- possible in principle. The composite primary key here closes that, and the
|
||||
-- precheck confirmed fav_duplicate_pairs = 0, so no de-duplication is needed —
|
||||
-- but the tool should still SELECT DISTINCT, since the source cannot enforce it.
|
||||
--
|
||||
-- 14 of the 251 rows point at a customer that no longer exists
|
||||
-- (fav.customer_id orphans). Those must be SKIPPED by the tool; the foreign
|
||||
-- keys below would reject them anyway.
|
||||
--
|
||||
-- ON DELETE CASCADE on both sides: a favourite is a pure join row with no
|
||||
-- meaning once either end is gone. This differs from ProductVariant.imageId,
|
||||
-- which uses SetNull precisely so a deleted image never cascades into order
|
||||
-- history — the distinction is deliberate.
|
||||
--
|
||||
-- Legacy also keeps a denormalised `product.count_fav` counter. It is NOT
|
||||
-- recreated as a column; derive it from this table (§5.2 group B) rather than
|
||||
-- showing a stale or zero value.
|
||||
CREATE TABLE IF NOT EXISTS "CustomerFavorite" (
|
||||
"customerId" TEXT NOT NULL,
|
||||
"productId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "CustomerFavorite_pkey" PRIMARY KEY ("customerId", "productId")
|
||||
);
|
||||
|
||||
-- The PK already indexes (customerId, productId) left-to-right, so "my
|
||||
-- favourites" needs no extra index. This one serves the reverse lookup —
|
||||
-- "how many customers favourited this product" — which is how count_fav is
|
||||
-- derived.
|
||||
CREATE INDEX IF NOT EXISTS "CustomerFavorite_productId_idx"
|
||||
ON "CustomerFavorite"("productId");
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
ALTER TABLE "CustomerFavorite"
|
||||
ADD CONSTRAINT "CustomerFavorite_customerId_fkey"
|
||||
FOREIGN KEY ("customerId") REFERENCES "Customer"("id")
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
ALTER TABLE "CustomerFavorite"
|
||||
ADD CONSTRAINT "CustomerFavorite_productId_fkey"
|
||||
FOREIGN KEY ("productId") REFERENCES "Product"("id")
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
COMMENT ON TABLE "CustomerFavorite" IS
|
||||
'Legacy `fav` (251 rows). Composite PK replaces the source table''s missing primary key. Derive product.count_fav from here rather than storing a counter.';
|
||||
@@ -0,0 +1,22 @@
|
||||
-- ============================================================================
|
||||
-- Customer.cityNameRaw — PURELY ADDITIVE.
|
||||
--
|
||||
-- Owner ruling 2026-10-07 (docs/DATA-MIGRATION-MAP.md §5.9): keep the city a
|
||||
-- customer typed at sign-up verbatim, and set cityId only on an exact match.
|
||||
--
|
||||
-- Legacy `customers.new_city` is free text (all 172 live values are names, 0
|
||||
-- are ids). The legacy city table has only 15 rows, so 43 of those names match
|
||||
-- no known city at all, and only ~72 match one of the 15 after ي/ك
|
||||
-- normalisation. Dropping the text would lose where those customers are.
|
||||
--
|
||||
-- Legacy sign-up also forced city_id = 5 (تهران) on every new customer
|
||||
-- (Major/CustomerController.php:21); 91 of the 92 customers with both values
|
||||
-- carry that forced Tehran. For them the typed name is the truth.
|
||||
--
|
||||
-- D15: idempotent, runs under both migration ledgers.
|
||||
-- ============================================================================
|
||||
|
||||
ALTER TABLE "Customer" ADD COLUMN IF NOT EXISTS "cityNameRaw" TEXT;
|
||||
|
||||
COMMENT ON COLUMN "Customer"."cityNameRaw" IS
|
||||
'Legacy customers.new_city — city name as typed at sign-up, kept verbatim. cityId is set too only on an exact (ي/ك-normalised) match; legacy forced city_id=5 (Tehran) on sign-up, so prefer this text over that cityId.';
|
||||
@@ -0,0 +1,31 @@
|
||||
-- ============================================================================
|
||||
-- Customer identity cleanup — data only, no schema change. Idempotent.
|
||||
--
|
||||
-- Owner ruling 2026-10-07: «هر دیتایی باید سر جای خودش بشینه».
|
||||
--
|
||||
-- 1. nationalCode held the MOBILE (or "<mobile>-<provider>-<code>") on every
|
||||
-- customer created without a national code, because the column used to be
|
||||
-- NOT NULL. It is optional since 20261007120000, and the code paths that
|
||||
-- wrote those stand-ins are fixed in the same change as this file. The
|
||||
-- stand-ins are cleared to NULL; a real national code is never touched.
|
||||
-- Accounting is unaffected: it only ever received validNationalCode(),
|
||||
-- which already mapped a mobile to null.
|
||||
--
|
||||
-- 2. passwordHash '' becomes the explicit "no password login" marker
|
||||
-- (auth/password-hash.ts). Both are refused before bcrypt by
|
||||
-- compareLocalHash, so nobody's ability to sign in changes.
|
||||
-- ============================================================================
|
||||
|
||||
UPDATE "Customer"
|
||||
SET "nationalCode" = NULL
|
||||
WHERE "nationalCode" = ''
|
||||
OR "nationalCode" = "mobile"
|
||||
OR "nationalCode" LIKE "mobile" || '-%';
|
||||
|
||||
UPDATE "Customer" SET "passwordHash" = '!OTP_ONLY_NO_PASSWORD_LOGIN' WHERE "passwordHash" = '';
|
||||
UPDATE "AdminUser" SET "passwordHash" = '!NO_PASSWORD_SET_YET' WHERE "passwordHash" = '';
|
||||
|
||||
-- The earlier comment named the wrong source table: `fav` belongs to the retail
|
||||
-- shop; wholesale favourites come from `major_favorites` (DATA-MIGRATION-MAP §10.7).
|
||||
COMMENT ON TABLE "CustomerFavorite" IS
|
||||
'Legacy `major_favorites` (wholesale; deduplicated on import). Composite PK replaces the source table''s missing primary key. Derive a favourite count from here rather than storing a counter.';
|
||||
@@ -0,0 +1,47 @@
|
||||
-- ============================================================================
|
||||
-- Order.salesAgentId / Order.referrerCustomerId — additive, idempotent.
|
||||
--
|
||||
-- Owner ruling 2026-10-08: keep legacy's rule exactly. An order is credited to
|
||||
-- the expert who was the customer's referrer WHEN IT WAS PLACED (legacy
|
||||
-- `orders.reagent_id`, set once at creation), so a customer moving to another
|
||||
-- expert later never changes past sales or commission. The app used to credit
|
||||
-- an order to the customer's CURRENT referrer, which disagreed with legacy for
|
||||
-- 8 of 49 experts on the imported data.
|
||||
--
|
||||
-- No row is deleted and no existing column changes.
|
||||
-- ============================================================================
|
||||
|
||||
ALTER TABLE "Order" ADD COLUMN IF NOT EXISTS "salesAgentId" TEXT;
|
||||
ALTER TABLE "Order" ADD COLUMN IF NOT EXISTS "referrerCustomerId" TEXT;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Order" ADD CONSTRAINT "Order_salesAgentId_fkey"
|
||||
FOREIGN KEY ("salesAgentId") REFERENCES "AdminUser"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Order" ADD CONSTRAINT "Order_referrerCustomerId_fkey"
|
||||
FOREIGN KEY ("referrerCustomerId") REFERENCES "Customer"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "Order_salesAgentId_idx" ON "Order"("salesAgentId");
|
||||
CREATE INDEX IF NOT EXISTS "Order_referrerCustomerId_idx" ON "Order"("referrerCustomerId");
|
||||
|
||||
-- Orders placed in the new app before this column existed: the customer's
|
||||
-- referrer today is the best record there is (these are recent). Orders
|
||||
-- brought over from legacy (orderNumber 'H-<id>') are NOT guessed here — the
|
||||
-- import writes their real `orders.reagent_id`.
|
||||
UPDATE "Order" o
|
||||
SET "salesAgentId" = c."referredByAgentId"
|
||||
FROM "Customer" c
|
||||
WHERE c."id" = o."customerId"
|
||||
AND o."salesAgentId" IS NULL
|
||||
AND c."referredByAgentId" IS NOT NULL
|
||||
AND o."orderNumber" NOT LIKE 'H-%';
|
||||
|
||||
COMMENT ON COLUMN "Order"."salesAgentId" IS
|
||||
'Sales expert credited with this order, fixed when it is placed (legacy orders.reagent_id). Never follows a later change of the customer''s referrer.';
|
||||
COMMENT ON COLUMN "Order"."referrerCustomerId" IS
|
||||
'History only: legacy orders.reagent_id when it named a customer who is not a sales expert. Never set for new orders.';
|
||||
@@ -0,0 +1,9 @@
|
||||
-- «اجرت» admin screen (owner ruling 2026-10-08): its permission for the
|
||||
-- seeded super-admin user type. A fresh seed grants every rid in ALL_RIDS;
|
||||
-- an existing database gets the new one here. Data only, idempotent.
|
||||
-- The formula itself needs no row: until an admin approves one, the server
|
||||
-- uses legacy's formula (pricing/wage-formula.ts LEGACY_WAGE_FORMULA).
|
||||
INSERT INTO "permission" ("rid", "userTypeId")
|
||||
SELECT 'config.wage', 85
|
||||
WHERE EXISTS (SELECT 1 FROM "user_type" WHERE "id" = 85)
|
||||
ON CONFLICT DO NOTHING;
|
||||
@@ -0,0 +1,14 @@
|
||||
-- PERM-01 (owner approval 2026-10-08): legacy's two dashboard permissions
|
||||
-- «آمار عمده فروشی» (dashboard.omde) and «آمار تک فروشی» (dashboard.tak), and
|
||||
-- their tree parent "dashboard", became the single "dashboard.view" when the two
|
||||
-- dashboards became one. A database seeded before that change still holds the
|
||||
-- old rows: nothing reads them and the permission screen cannot show or revoke
|
||||
-- them. Whoever held one keeps the dashboard through "dashboard.view", then the
|
||||
-- old rows go. Data only, idempotent.
|
||||
INSERT INTO "permission" ("rid", "userTypeId")
|
||||
SELECT DISTINCT 'dashboard.view', "userTypeId"
|
||||
FROM "permission"
|
||||
WHERE "rid" IN ('dashboard', 'dashboard.omde', 'dashboard.tak')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
DELETE FROM "permission" WHERE "rid" IN ('dashboard', 'dashboard.omde', 'dashboard.tak');
|
||||
@@ -0,0 +1,33 @@
|
||||
-- PERS-02 (owner approval 2026-10-08): cities become a tree again, as in legacy
|
||||
-- (country > province > city > district, henza `cities.parent`). The flat
|
||||
-- `province` label is turned into parent rows, nothing is lost, then dropped.
|
||||
|
||||
ALTER TABLE "City" ADD COLUMN "parentId" TEXT;
|
||||
|
||||
-- One province row per distinct label, named as legacy names them
|
||||
-- («استان تهران»), unless a city row already carries that name.
|
||||
INSERT INTO "City" ("id", "name")
|
||||
SELECT 'cprov' || substr(md5(p.pname), 1, 20), p.pname
|
||||
FROM (
|
||||
SELECT DISTINCT
|
||||
CASE WHEN btrim("province") LIKE 'استان%' THEN btrim("province") ELSE 'استان ' || btrim("province") END AS pname
|
||||
FROM "City"
|
||||
WHERE "province" IS NOT NULL AND btrim("province") <> ''
|
||||
) p
|
||||
WHERE NOT EXISTS (SELECT 1 FROM "City" c WHERE c."name" = p.pname);
|
||||
|
||||
UPDATE "City" c
|
||||
SET "parentId" = (
|
||||
SELECT p."id" FROM "City" p
|
||||
WHERE p."id" <> c."id"
|
||||
AND p."name" = CASE WHEN btrim(c."province") LIKE 'استان%' THEN btrim(c."province") ELSE 'استان ' || btrim(c."province") END
|
||||
ORDER BY p."id"
|
||||
LIMIT 1
|
||||
)
|
||||
WHERE c."province" IS NOT NULL AND btrim(c."province") <> '';
|
||||
|
||||
ALTER TABLE "City" DROP COLUMN "province";
|
||||
|
||||
CREATE INDEX "City_parentId_idx" ON "City"("parentId");
|
||||
|
||||
ALTER TABLE "City" ADD CONSTRAINT "City_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "City"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
+44
-22
@@ -1607,6 +1607,9 @@ var ALL_RIDS = [
|
||||
"comment.state",
|
||||
"comment.view",
|
||||
"comment.view.edit",
|
||||
// D27 — «اتصال حسابداری» (Tahesab / Zarrin). Not a legacy rid.
|
||||
"config.accounting",
|
||||
"config.wage",
|
||||
"config.report",
|
||||
"config.statics",
|
||||
"config.statics.brand",
|
||||
@@ -1742,28 +1745,7 @@ var PLACEHOLDER_PASSWORD_HASH = hashSync(DEV_PASSWORD, 10);
|
||||
var DEV_TEST_PHONE = "09128081392";
|
||||
var DEV_TEST_PHONE_PASSWORD_HASH = hashSync(DEV_TEST_PHONE, 10);
|
||||
var PLACEHOLDER_PRODUCT_IMAGE = "/img/no-items.png";
|
||||
async function main() {
|
||||
const [tehran, mashhad, isfahan, tabriz] = await Promise.all([
|
||||
prisma.city.upsert({ where: { id: "city-tehran" }, update: {}, create: { id: "city-tehran", name: "\u062A\u0647\u0631\u0627\u0646", province: "\u062A\u0647\u0631\u0627\u0646" } }),
|
||||
prisma.city.upsert({ where: { id: "city-mashhad" }, update: {}, create: { id: "city-mashhad", name: "\u0645\u0634\u0647\u062F", province: "\u062E\u0631\u0627\u0633\u0627\u0646 \u0631\u0636\u0648\u06CC" } }),
|
||||
prisma.city.upsert({ where: { id: "city-isfahan" }, update: {}, create: { id: "city-isfahan", name: "\u0627\u0635\u0641\u0647\u0627\u0646", province: "\u0627\u0635\u0641\u0647\u0627\u0646" } }),
|
||||
prisma.city.upsert({ where: { id: "city-tabriz" }, update: {}, create: { id: "city-tabriz", name: "\u062A\u0628\u0631\u06CC\u0632", province: "\u0622\u0630\u0631\u0628\u0627\u06CC\u062C\u0627\u0646 \u0634\u0631\u0642\u06CC" } })
|
||||
]);
|
||||
const branchTehran = await prisma.branch.upsert({
|
||||
where: { id: "branch-tehran-bazaar" },
|
||||
update: {},
|
||||
create: { id: "branch-tehran-bazaar", name: "\u0634\u0639\u0628\u0647 \u0628\u0627\u0632\u0627\u0631 \u0628\u0632\u0631\u06AF \u062A\u0647\u0631\u0627\u0646", cityId: tehran.id }
|
||||
});
|
||||
const branchMashhad = await prisma.branch.upsert({
|
||||
where: { id: "branch-mashhad-main" },
|
||||
update: {},
|
||||
create: { id: "branch-mashhad-main", name: "\u0634\u0639\u0628\u0647 \u0645\u0631\u06A9\u0632\u06CC \u0645\u0634\u0647\u062F", cityId: mashhad.id }
|
||||
});
|
||||
await prisma.workshop.upsert({
|
||||
where: { id: "workshop-central" },
|
||||
update: {},
|
||||
create: { id: "workshop-central", name: "\u06A9\u0627\u0631\u06AF\u0627\u0647 \u0645\u0631\u06A9\u0632\u06CC \u0633\u0627\u062E\u062A \u0637\u0644\u0627" }
|
||||
});
|
||||
async function seedAccessControl() {
|
||||
const userTypes = [
|
||||
// Full access — every rid the routers gate.
|
||||
{ id: 85, title: "\u0645\u062F\u06CC\u0631 \u0627\u0631\u0634\u062F", rids: ALL_RIDS },
|
||||
@@ -1824,6 +1806,46 @@ async function main() {
|
||||
skipDuplicates: true
|
||||
});
|
||||
}
|
||||
}
|
||||
function demoDataAllowed() {
|
||||
if (process.env.SEED_DEMO_DATA?.trim() === "true") return true;
|
||||
return process.env.NODE_ENV !== "production";
|
||||
}
|
||||
async function main() {
|
||||
await seedAccessControl();
|
||||
if (!demoDataAllowed()) {
|
||||
console.log("seed: production \u2014 roles and permissions only; demo data skipped (set SEED_DEMO_DATA=true to force)");
|
||||
return;
|
||||
}
|
||||
const city = (id, name, parentId) => prisma.city.upsert({ where: { id }, update: {}, create: { id, name, parentId } });
|
||||
await city("city-iran", "\u0627\u06CC\u0631\u0627\u0646", null);
|
||||
await Promise.all([
|
||||
city("city-prov-tehran", "\u0627\u0633\u062A\u0627\u0646 \u062A\u0647\u0631\u0627\u0646", "city-iran"),
|
||||
city("city-prov-khorasan-razavi", "\u0627\u0633\u062A\u0627\u0646 \u062E\u0631\u0627\u0633\u0627\u0646 \u0631\u0636\u0648\u06CC", "city-iran"),
|
||||
city("city-prov-isfahan", "\u0627\u0633\u062A\u0627\u0646 \u0627\u0635\u0641\u0647\u0627\u0646", "city-iran"),
|
||||
city("city-prov-east-azarbaijan", "\u0627\u0633\u062A\u0627\u0646 \u0622\u0630\u0631\u0628\u0627\u06CC\u062C\u0627\u0646 \u0634\u0631\u0642\u06CC", "city-iran")
|
||||
]);
|
||||
const [tehran, mashhad, isfahan, tabriz] = await Promise.all([
|
||||
city("city-tehran", "\u062A\u0647\u0631\u0627\u0646", "city-prov-tehran"),
|
||||
city("city-mashhad", "\u0645\u0634\u0647\u062F", "city-prov-khorasan-razavi"),
|
||||
city("city-isfahan", "\u0627\u0635\u0641\u0647\u0627\u0646", "city-prov-isfahan"),
|
||||
city("city-tabriz", "\u062A\u0628\u0631\u06CC\u0632", "city-prov-east-azarbaijan")
|
||||
]);
|
||||
const branchTehran = await prisma.branch.upsert({
|
||||
where: { id: "branch-tehran-bazaar" },
|
||||
update: {},
|
||||
create: { id: "branch-tehran-bazaar", name: "\u0634\u0639\u0628\u0647 \u0628\u0627\u0632\u0627\u0631 \u0628\u0632\u0631\u06AF \u062A\u0647\u0631\u0627\u0646", cityId: tehran.id }
|
||||
});
|
||||
const branchMashhad = await prisma.branch.upsert({
|
||||
where: { id: "branch-mashhad-main" },
|
||||
update: {},
|
||||
create: { id: "branch-mashhad-main", name: "\u0634\u0639\u0628\u0647 \u0645\u0631\u06A9\u0632\u06CC \u0645\u0634\u0647\u062F", cityId: mashhad.id }
|
||||
});
|
||||
await prisma.workshop.upsert({
|
||||
where: { id: "workshop-central" },
|
||||
update: {},
|
||||
create: { id: "workshop-central", name: "\u06A9\u0627\u0631\u06AF\u0627\u0647 \u0645\u0631\u06A9\u0632\u06CC \u0633\u0627\u062E\u062A \u0637\u0644\u0627" }
|
||||
});
|
||||
await prisma.adminUser.upsert({
|
||||
where: { mobile: DEV_TEST_PHONE },
|
||||
update: { passwordHash: DEV_TEST_PHONE_PASSWORD_HASH, userTypeId: 85, branchId: null },
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
class PrismaClient {
|
||||
constructor() {
|
||||
throw new Error(
|
||||
'@prisma/client/deno/edge did not initialize yet. Please run "prisma generate" and try to import it again.',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export { PrismaClient }
|
||||
+102
-8
File diff suppressed because one or more lines are too long
+97
-3
@@ -125,7 +125,7 @@ exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
|
||||
exports.Prisma.CityScalarFieldEnum = {
|
||||
id: 'id',
|
||||
name: 'name',
|
||||
province: 'province',
|
||||
parentId: 'parentId',
|
||||
percent: 'percent',
|
||||
percentAux: 'percentAux'
|
||||
};
|
||||
@@ -203,6 +203,7 @@ exports.Prisma.CustomerScalarFieldEnum = {
|
||||
nationalCode: 'nationalCode',
|
||||
passwordHash: 'passwordHash',
|
||||
cityId: 'cityId',
|
||||
cityNameRaw: 'cityNameRaw',
|
||||
kind: 'kind',
|
||||
phone: 'phone',
|
||||
address: 'address',
|
||||
@@ -214,11 +215,17 @@ exports.Prisma.CustomerScalarFieldEnum = {
|
||||
goldCreditLimitGrams: 'goldCreditLimitGrams',
|
||||
goldDebtGrams: 'goldDebtGrams',
|
||||
fiatDebtRial: 'fiatDebtRial',
|
||||
accountingGoldBalance: 'accountingGoldBalance',
|
||||
accountingRialBalance: 'accountingRialBalance',
|
||||
accountingBalanceAt: 'accountingBalanceAt',
|
||||
makingChargeOffsetPercent: 'makingChargeOffsetPercent',
|
||||
verificationStep: 'verificationStep',
|
||||
nationalIdImageUrl: 'nationalIdImageUrl',
|
||||
businessLicenseUrl: 'businessLicenseUrl',
|
||||
shopImageUrl: 'shopImageUrl',
|
||||
verificationRejectionReason: 'verificationRejectionReason',
|
||||
verificationRejectedAt: 'verificationRejectedAt',
|
||||
birthDate: 'birthDate',
|
||||
avatarFileId: 'avatarFileId',
|
||||
canPlaceOrders: 'canPlaceOrders',
|
||||
isActive: 'isActive',
|
||||
@@ -227,6 +234,12 @@ exports.Prisma.CustomerScalarFieldEnum = {
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.CustomerFavoriteScalarFieldEnum = {
|
||||
customerId: 'customerId',
|
||||
productId: 'productId',
|
||||
createdAt: 'createdAt'
|
||||
};
|
||||
|
||||
exports.Prisma.CustomerAddressScalarFieldEnum = {
|
||||
id: 'id',
|
||||
customerId: 'customerId',
|
||||
@@ -290,7 +303,10 @@ exports.Prisma.ProductScalarFieldEnum = {
|
||||
productCode: 'productCode',
|
||||
showType: 'showType',
|
||||
isActive: 'isActive',
|
||||
createdAt: 'createdAt'
|
||||
createdAt: 'createdAt',
|
||||
accountingStockWeight: 'accountingStockWeight',
|
||||
accountingStockCount: 'accountingStockCount',
|
||||
accountingStockAt: 'accountingStockAt'
|
||||
};
|
||||
|
||||
exports.Prisma.ProductImageScalarFieldEnum = {
|
||||
@@ -326,6 +342,8 @@ exports.Prisma.OrderScalarFieldEnum = {
|
||||
flatRialCharge: 'flatRialCharge',
|
||||
isSuspendedOverCreditLimit: 'isSuspendedOverCreditLimit',
|
||||
deliveredAt: 'deliveredAt',
|
||||
salesAgentId: 'salesAgentId',
|
||||
referrerCustomerId: 'referrerCustomerId',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
@@ -453,6 +471,51 @@ exports.Prisma.SettingScalarFieldEnum = {
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.AccountingLinkScalarFieldEnum = {
|
||||
id: 'id',
|
||||
provider: 'provider',
|
||||
entityType: 'entityType',
|
||||
entityId: 'entityId',
|
||||
externalCode: 'externalCode',
|
||||
externalName: 'externalName',
|
||||
origin: 'origin',
|
||||
snapshot: 'snapshot',
|
||||
syncedAt: 'syncedAt',
|
||||
createdAt: 'createdAt'
|
||||
};
|
||||
|
||||
exports.Prisma.AccountingOutboxScalarFieldEnum = {
|
||||
id: 'id',
|
||||
provider: 'provider',
|
||||
kind: 'kind',
|
||||
entityId: 'entityId',
|
||||
status: 'status',
|
||||
attempts: 'attempts',
|
||||
nextAttemptAt: 'nextAttemptAt',
|
||||
lastError: 'lastError',
|
||||
externalRef: 'externalRef',
|
||||
progress: 'progress',
|
||||
lastRequest: 'lastRequest',
|
||||
lastResponse: 'lastResponse',
|
||||
sentAt: 'sentAt',
|
||||
resolvedById: 'resolvedById',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.AccountingIssueScalarFieldEnum = {
|
||||
id: 'id',
|
||||
provider: 'provider',
|
||||
kind: 'kind',
|
||||
externalCode: 'externalCode',
|
||||
entityId: 'entityId',
|
||||
message: 'message',
|
||||
data: 'data',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt',
|
||||
resolvedAt: 'resolvedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.PromotionScalarFieldEnum = {
|
||||
id: 'id',
|
||||
title: 'title',
|
||||
@@ -567,6 +630,11 @@ exports.Prisma.JsonNullValueInput = {
|
||||
JsonNull: Prisma.JsonNull
|
||||
};
|
||||
|
||||
exports.Prisma.NullableJsonNullValueInput = {
|
||||
DbNull: Prisma.DbNull,
|
||||
JsonNull: Prisma.JsonNull
|
||||
};
|
||||
|
||||
exports.Prisma.QueryMode = {
|
||||
default: 'default',
|
||||
insensitive: 'insensitive'
|
||||
@@ -603,7 +671,8 @@ exports.VerificationStep = exports.$Enums.VerificationStep = {
|
||||
UNVERIFIED: 'UNVERIFIED',
|
||||
STEP_ONE_SUBMITTED: 'STEP_ONE_SUBMITTED',
|
||||
STEP_TWO_SUBMITTED: 'STEP_TWO_SUBMITTED',
|
||||
VERIFIED: 'VERIFIED'
|
||||
VERIFIED: 'VERIFIED',
|
||||
REJECTED: 'REJECTED'
|
||||
};
|
||||
|
||||
exports.ProductImageRole = exports.$Enums.ProductImageRole = {
|
||||
@@ -665,6 +734,27 @@ exports.CommentStatus = exports.$Enums.CommentStatus = {
|
||||
REJECTED: 'REJECTED'
|
||||
};
|
||||
|
||||
exports.AccountingEntityType = exports.$Enums.AccountingEntityType = {
|
||||
CUSTOMER: 'CUSTOMER',
|
||||
PRODUCT: 'PRODUCT'
|
||||
};
|
||||
|
||||
exports.AccountingOutboxKind = exports.$Enums.AccountingOutboxKind = {
|
||||
CUSTOMER_PUSH: 'CUSTOMER_PUSH',
|
||||
PRODUCT_PUSH: 'PRODUCT_PUSH',
|
||||
ORDER_DOCUMENT: 'ORDER_DOCUMENT',
|
||||
ORDER_DOCUMENT_DELETE: 'ORDER_DOCUMENT_DELETE'
|
||||
};
|
||||
|
||||
exports.AccountingOutboxStatus = exports.$Enums.AccountingOutboxStatus = {
|
||||
PENDING: 'PENDING',
|
||||
SENDING: 'SENDING',
|
||||
SENT: 'SENT',
|
||||
FAILED: 'FAILED',
|
||||
NEEDS_REVIEW: 'NEEDS_REVIEW',
|
||||
RESOLVED: 'RESOLVED'
|
||||
};
|
||||
|
||||
exports.PromotionScope = exports.$Enums.PromotionScope = {
|
||||
PRODUCT: 'PRODUCT',
|
||||
CATEGORY: 'CATEGORY',
|
||||
@@ -693,6 +783,7 @@ exports.Prisma.ModelName = {
|
||||
AdminSession: 'AdminSession',
|
||||
SalesExpertProfile: 'SalesExpertProfile',
|
||||
Customer: 'Customer',
|
||||
CustomerFavorite: 'CustomerFavorite',
|
||||
CustomerAddress: 'CustomerAddress',
|
||||
Branch: 'Branch',
|
||||
Workshop: 'Workshop',
|
||||
@@ -715,6 +806,9 @@ exports.Prisma.ModelName = {
|
||||
Permission: 'Permission',
|
||||
Comment: 'Comment',
|
||||
Setting: 'Setting',
|
||||
AccountingLink: 'AccountingLink',
|
||||
AccountingOutbox: 'AccountingOutbox',
|
||||
AccountingIssue: 'AccountingIssue',
|
||||
Promotion: 'Promotion',
|
||||
PromotionExclusion: 'PromotionExclusion',
|
||||
BranchAllocation: 'BranchAllocation',
|
||||
|
||||
+8686
-172
File diff suppressed because it is too large
Load Diff
+102
-8
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "prisma-client-e72c5c915e96796902913fde6ec160a337ad8fec683314696d8bd7a12ac26468",
|
||||
"name": "prisma-client-9161dfb7f2a3ada2b72c27c7a91664048e7f0e5d5d265a0abff1e443711c8388",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"browser": "index-browser.js",
|
||||
|
||||
+233
-2
@@ -56,6 +56,13 @@ enum VerificationStep {
|
||||
STEP_ONE_SUBMITTED
|
||||
STEP_TWO_SUBMITTED
|
||||
VERIFIED
|
||||
|
||||
/// Legacy had THREE states per step, not two
|
||||
/// (MajorCustomerController.php:106-135): 1 = awaiting, 2 = approved,
|
||||
/// 3 = REJECTED. Mapping rejected customers to UNVERIFIED would put closed
|
||||
/// cases back in the review queue, so the owner ruled it out (2026-10-07).
|
||||
/// 14 legacy rows land here; see Customer.verificationRejectionReason.
|
||||
REJECTED
|
||||
}
|
||||
|
||||
/// Legacy kept retail and wholesale orders in two SEPARATE tables
|
||||
@@ -111,10 +118,15 @@ enum LedgerSourceType {
|
||||
// Identity
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Legacy `cities` is a tree (country > province > city > district) and so is
|
||||
/// this: a city hangs under its `parent`. In the admin pickers a top-level row
|
||||
/// (the country) is a heading, not selectable (henza_admin citiesModel.js:38).
|
||||
model City {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
province String?
|
||||
parentId String?
|
||||
parent City? @relation("CityTree", fields: [parentId], references: [id], onDelete: Restrict)
|
||||
children City[] @relation("CityTree")
|
||||
/// Legacy cities.percent / percent_aux — per-city wage surcharges the city
|
||||
/// screen edits through GET cities/get-percent/{id} and get-percent-aux/{id}.
|
||||
percent Decimal? @db.Decimal(6, 3)
|
||||
@@ -124,6 +136,8 @@ model City {
|
||||
branches Branch[]
|
||||
preOrders PreOrder[]
|
||||
salesExpertProfiles SalesExpertProfile[]
|
||||
|
||||
@@index([parentId])
|
||||
}
|
||||
|
||||
model AdminUser {
|
||||
@@ -172,6 +186,8 @@ model AdminUser {
|
||||
memberCode String? // legacy mcode
|
||||
|
||||
referredCustomers Customer[] @relation("ReferralAgent")
|
||||
/// Orders credited to this sales expert — see Order.salesAgentId.
|
||||
creditedOrders Order[] @relation("OrderSalesAgent")
|
||||
supervisedWorkshops Workshop[] @relation("WorkshopSupervisor")
|
||||
branchAllocations BranchAllocation[] @relation("AllocationAuthor")
|
||||
postedLedgerEntries LedgerEntry[]
|
||||
@@ -367,11 +383,32 @@ model Customer {
|
||||
lastName String?
|
||||
|
||||
mobile String @unique
|
||||
nationalCode String @unique
|
||||
/// Legacy customers.ncode. OPTIONAL since 2026-10-07: not one of the 1,616
|
||||
/// legacy customers has a national code (1595/1595 empty), and fabricating
|
||||
/// one is a data violation that would block later legal identity checks
|
||||
/// (owner ruling). Still @unique — in PostgreSQL NULLs do not collide, so
|
||||
/// the migration tool must write NULL and never '', which would collide on
|
||||
/// the second row.
|
||||
///
|
||||
/// Three service-layer workarounds exist ONLY because this used to be
|
||||
/// required, and they store the MOBILE NUMBER here:
|
||||
/// admin-customers.service.ts:625,685 and customer-auth.service.ts:258.
|
||||
/// Retiring them is a separate change; see docs/DATA-MIGRATION-MAP.md §10.4.
|
||||
nationalCode String? @unique
|
||||
passwordHash String
|
||||
cityId String?
|
||||
city City? @relation(fields: [cityId], references: [id])
|
||||
|
||||
/// Legacy customers.new_city — the city name exactly as the customer typed it
|
||||
/// at app sign-up (Major/CustomerController.php:36). Kept verbatim because
|
||||
/// the legacy city table has only 15 rows, so the typed name is often the only
|
||||
/// record of where the customer is (172 customers; 43 match no known city).
|
||||
/// cityId is set alongside it only on an exact, ي/ك-normalised name match.
|
||||
/// Legacy sign-up also forced city_id = 5 (Tehran) on everyone
|
||||
/// (Major/CustomerController.php:21), so for those rows the typed name, not
|
||||
/// cityId, is the truth. Owner ruling 2026-10-07: keep both.
|
||||
cityNameRaw String?
|
||||
|
||||
/// Legacy Customer.type — distinguishes بنکدار from مغازه دار, which the
|
||||
/// BankerList / ShopkeeperList screens filter on.
|
||||
kind CustomerKind @default(APP_SHOPKEEPER)
|
||||
@@ -393,6 +430,9 @@ model Customer {
|
||||
/// account (see SalesExpertProfile.tradingCustomerId). Empty for every
|
||||
/// ordinary customer.
|
||||
salesExpertTradingFor SalesExpertProfile? @relation("SalesExpertTradingAccount")
|
||||
/// Legacy orders that named THIS customer as their referrer — see
|
||||
/// Order.referrerCustomerId. History only.
|
||||
referredOrders Order[] @relation("OrderReferrerCustomer")
|
||||
|
||||
// Credit limit + cached ledger balances (source of truth is LedgerEntry; these
|
||||
// are denormalized for fast reads and reconciled by a scheduled job).
|
||||
@@ -400,6 +440,14 @@ model Customer {
|
||||
goldDebtGrams Decimal @default(0) @db.Decimal(14, 4) // legacy remain
|
||||
fiatDebtRial Decimal @default(0) @db.Decimal(20, 2) // legacy rial_remain
|
||||
|
||||
/// Balance as last read from the accounting software (D27), in the app's own
|
||||
/// sign convention (positive = owes). Shown to the customer when the
|
||||
/// integration is on; the credit-limit check still uses goldDebtGrams above.
|
||||
/// Null until the first successful sync.
|
||||
accountingGoldBalance Decimal? @db.Decimal(14, 4)
|
||||
accountingRialBalance Decimal? @db.Decimal(20, 2)
|
||||
accountingBalanceAt DateTime?
|
||||
|
||||
/// Legacy `customers.percent` — the per-customer making-charge offset. It is
|
||||
/// ADDED to the variant's making charge when pricing an order line
|
||||
/// (henza_pwa src/Mixins/product/index.ts:69-75:
|
||||
@@ -421,6 +469,30 @@ model Customer {
|
||||
businessLicenseUrl String?
|
||||
shopImageUrl String?
|
||||
|
||||
/// Why verification was rejected (VerificationStep.REJECTED).
|
||||
///
|
||||
/// Legacy recorded NO reason: none of its 49 `customers` columns is a
|
||||
/// reason/note field, and on rejection it only sent a fixed SMS. So the 14
|
||||
/// rows migrated from henza carry the sentinel
|
||||
/// «رد شده در سیستم قدیم — فاقد شرح علت ثبتشده» instead of NULL, so support
|
||||
/// knows why it is blank (owner ruling 2026-10-07). The migration tool
|
||||
/// writes it only WHERE this IS NULL, so a re-run cannot overwrite a real
|
||||
/// reason entered in the new system.
|
||||
verificationRejectionReason String?
|
||||
|
||||
/// NULL for rows migrated from henza — legacy recorded no rejection time.
|
||||
verificationRejectedAt DateTime?
|
||||
|
||||
/// Legacy customers.birth_date, populated on 917 of 1,595 rows. Kept for the
|
||||
/// birthday SMS / loyalty campaigns (owner ruling 2026-10-07).
|
||||
///
|
||||
/// ⚠ Legacy stores dates as JALALI STRINGS in other tables
|
||||
/// (cashes.recive_date is varchar, 88/89 values starting "14…"), so this
|
||||
/// timestamp may hold mis-entered Jalali years. Confirm the calendar with
|
||||
/// docs/migration-precheck-3.sql BEFORE backfilling: copying them unchanged
|
||||
/// would produce 917 wrong birthdays, worse for an SMS campaign than none.
|
||||
birthDate DateTime?
|
||||
|
||||
/// Legacy `image_id` — the profile photo (عکس پروفایل) on the sales-agent
|
||||
/// form. Legacy stored an FK to its Image table and read it back as a nested
|
||||
/// `image: { id, image }`, so this is a relation, not a bare URL string.
|
||||
@@ -449,12 +521,35 @@ model Customer {
|
||||
notifications NotificationRecipient[]
|
||||
activityLogs CustomerActivityLog[]
|
||||
otpChallenges CustomerOtp[]
|
||||
favorites CustomerFavorite[]
|
||||
|
||||
@@index([referredByAgentId])
|
||||
@@index([firstName])
|
||||
@@index([lastName])
|
||||
}
|
||||
|
||||
/// Legacy `fav` (251 rows) — saved products. The target had no model for this,
|
||||
/// so every customer's favourites would have been lost in the migration; the
|
||||
/// owner confirmed on 2026-10-06 that they matter.
|
||||
///
|
||||
/// Legacy `fav` has two columns and NO primary key. The composite key here
|
||||
/// supplies the constraint the source lacks (the precheck found 0 duplicate
|
||||
/// pairs, but the tool should still SELECT DISTINCT). 14 of the 251 rows point
|
||||
/// at a deleted customer and must be skipped.
|
||||
///
|
||||
/// Legacy's denormalised `product.count_fav` counter is deliberately NOT
|
||||
/// recreated as a column — derive it from this table.
|
||||
model CustomerFavorite {
|
||||
customerId String
|
||||
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
||||
productId String
|
||||
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@id([customerId, productId])
|
||||
@@index([productId])
|
||||
}
|
||||
|
||||
model CustomerAddress {
|
||||
id String @id @default(cuid())
|
||||
customerId String
|
||||
@@ -557,12 +652,21 @@ model Product {
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
/// Stock of the linked کارساخته as last read from the accounting software
|
||||
/// (D27). Weight for gram-type items, count for count-type ones — whichever
|
||||
/// the software reports. At zero or below the product is unavailable in the
|
||||
/// app (pre-order variants excepted). Null = not linked or never synced.
|
||||
accountingStockWeight Decimal? @db.Decimal(14, 4)
|
||||
accountingStockCount Decimal? @db.Decimal(14, 4)
|
||||
accountingStockAt DateTime?
|
||||
|
||||
variants ProductVariant[]
|
||||
images ProductImage[]
|
||||
collections CollectionProduct[]
|
||||
comments Comment[]
|
||||
promotions Promotion[]
|
||||
allocationItems BranchAllocationItem[]
|
||||
favoritedBy CustomerFavorite[]
|
||||
}
|
||||
|
||||
model ProductImage {
|
||||
@@ -644,6 +748,22 @@ model Order {
|
||||
isSuspendedOverCreditLimit Boolean @default(false)
|
||||
deliveredAt DateTime? // setting this is what triggers debt recognition
|
||||
|
||||
/// The sales expert this order is credited to, fixed when it is placed:
|
||||
/// the customer's referring expert AT THAT MOMENT. Legacy `orders.reagent_id`,
|
||||
/// set once at creation (ApiMobileController.php:601) and never moved, so a
|
||||
/// customer changing expert later does not move past sales or commission
|
||||
/// (owner ruling 2026-10-08). «میزان فروش» counts from here, never from the
|
||||
/// customer's current referrer.
|
||||
salesAgentId String?
|
||||
salesAgent AdminUser? @relation("OrderSalesAgent", fields: [salesAgentId], references: [id], onDelete: SetNull)
|
||||
|
||||
/// History only: a legacy order whose `reagent_id` named a customer who is
|
||||
/// not a sales expert (one such customer, 17 orders in 2022). Legacy counted
|
||||
/// those orders in that customer's own «میزان فروش»; this keeps that figure
|
||||
/// exact. Never set for a new order.
|
||||
referrerCustomerId String?
|
||||
referrerCustomer Customer? @relation("OrderReferrerCustomer", fields: [referrerCustomerId], references: [id], onDelete: SetNull)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@ -654,6 +774,8 @@ model Order {
|
||||
|
||||
@@index([customerId])
|
||||
@@index([status])
|
||||
@@index([salesAgentId])
|
||||
@@index([referrerCustomerId])
|
||||
}
|
||||
|
||||
model OrderItem {
|
||||
@@ -911,6 +1033,115 @@ model Setting {
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Accounting integration (D27) — the shop's accounting software, Tahesab or
|
||||
// Zarrin, chosen in the admin panel. Configuration lives in the Setting row
|
||||
// `accounting.config` (never served by the generic `const` routes). Plan:
|
||||
// docs/ACCOUNTING-INTEGRATION-PLAN.md.
|
||||
//
|
||||
// `provider` is the driver name ('tahesab' | 'zarrin'), a plain string like
|
||||
// SMS_PROVIDER so a new driver needs no migration. Links are per provider, so
|
||||
// switching provider and back loses nothing.
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
enum AccountingEntityType {
|
||||
CUSTOMER
|
||||
PRODUCT
|
||||
}
|
||||
|
||||
/// One app row ↔ one record in the accounting software (customer account or
|
||||
/// کارساخته).
|
||||
model AccountingLink {
|
||||
id String @id @default(cuid())
|
||||
provider String
|
||||
entityType AccountingEntityType
|
||||
entityId String
|
||||
/// The record's code in the accounting software (Tahesab Moshtari_Code /
|
||||
/// Zarrin idHesab; for products the Zarrin idKar or the Tahesab Name_Kar).
|
||||
externalCode String
|
||||
externalName String?
|
||||
/// How the link came to be: LINKED (matched by mobile/name), IMPORTED (app
|
||||
/// row created from the accounting record), PUSHED (accounting record created
|
||||
/// from the app row) or MANUAL (an admin typed the code).
|
||||
origin String
|
||||
/// The record as last read, for display only.
|
||||
snapshot Json?
|
||||
syncedAt DateTime @default(now())
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([provider, entityType, entityId])
|
||||
@@unique([provider, entityType, externalCode])
|
||||
@@index([entityType, entityId])
|
||||
}
|
||||
|
||||
enum AccountingOutboxKind {
|
||||
CUSTOMER_PUSH
|
||||
PRODUCT_PUSH
|
||||
ORDER_DOCUMENT
|
||||
ORDER_DOCUMENT_DELETE
|
||||
}
|
||||
|
||||
enum AccountingOutboxStatus {
|
||||
PENDING
|
||||
/// A call was in flight. Found at startup, it means the process died mid-call
|
||||
/// and the outcome is unknown — see AccountingOutboxWorker.
|
||||
SENDING
|
||||
SENT
|
||||
FAILED
|
||||
/// Unsafe to retry automatically (e.g. a Zarrin call timed out: Zarrin has no
|
||||
/// idempotency key, so a blind retry could post the document twice).
|
||||
NEEDS_REVIEW
|
||||
/// Closed by an admin from the log screen.
|
||||
RESOLVED
|
||||
}
|
||||
|
||||
/// Work to push to the accounting software. Written in the SAME transaction as
|
||||
/// the change that causes it, so an outage of the accounting software never
|
||||
/// loses an event and never blocks the app.
|
||||
model AccountingOutbox {
|
||||
id String @id @default(cuid())
|
||||
provider String
|
||||
kind AccountingOutboxKind
|
||||
entityId String
|
||||
status AccountingOutboxStatus @default(PENDING)
|
||||
attempts Int @default(0)
|
||||
nextAttemptAt DateTime @default(now())
|
||||
lastError String?
|
||||
/// What the accounting software returned as the document/record id.
|
||||
externalRef String?
|
||||
/// Per-step progress of multi-call jobs (a Zarrin document is one call per
|
||||
/// line), so a failure resumes where it stopped.
|
||||
progress Json?
|
||||
/// Last request/response, for the log screen. Never contains the token.
|
||||
lastRequest Json?
|
||||
lastResponse Json?
|
||||
sentAt DateTime?
|
||||
resolvedById String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([status, nextAttemptAt])
|
||||
@@index([kind, entityId])
|
||||
}
|
||||
|
||||
/// Something a pull found that needs a human: an account with no mobile, two
|
||||
/// accounts sharing one mobile, a کارساخته group with no category mapping.
|
||||
model AccountingIssue {
|
||||
id String @id @default(cuid())
|
||||
provider String
|
||||
kind String
|
||||
externalCode String
|
||||
entityId String?
|
||||
message String
|
||||
data Json?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
resolvedAt DateTime?
|
||||
|
||||
@@unique([provider, kind, externalCode])
|
||||
@@index([resolvedAt])
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Promotions / discounts — legacy views/promotion/*, served by
|
||||
// PromotionController (`product/variant/promotion`). The redesigned schema had
|
||||
|
||||
+97
-3
@@ -125,7 +125,7 @@ exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
|
||||
exports.Prisma.CityScalarFieldEnum = {
|
||||
id: 'id',
|
||||
name: 'name',
|
||||
province: 'province',
|
||||
parentId: 'parentId',
|
||||
percent: 'percent',
|
||||
percentAux: 'percentAux'
|
||||
};
|
||||
@@ -203,6 +203,7 @@ exports.Prisma.CustomerScalarFieldEnum = {
|
||||
nationalCode: 'nationalCode',
|
||||
passwordHash: 'passwordHash',
|
||||
cityId: 'cityId',
|
||||
cityNameRaw: 'cityNameRaw',
|
||||
kind: 'kind',
|
||||
phone: 'phone',
|
||||
address: 'address',
|
||||
@@ -214,11 +215,17 @@ exports.Prisma.CustomerScalarFieldEnum = {
|
||||
goldCreditLimitGrams: 'goldCreditLimitGrams',
|
||||
goldDebtGrams: 'goldDebtGrams',
|
||||
fiatDebtRial: 'fiatDebtRial',
|
||||
accountingGoldBalance: 'accountingGoldBalance',
|
||||
accountingRialBalance: 'accountingRialBalance',
|
||||
accountingBalanceAt: 'accountingBalanceAt',
|
||||
makingChargeOffsetPercent: 'makingChargeOffsetPercent',
|
||||
verificationStep: 'verificationStep',
|
||||
nationalIdImageUrl: 'nationalIdImageUrl',
|
||||
businessLicenseUrl: 'businessLicenseUrl',
|
||||
shopImageUrl: 'shopImageUrl',
|
||||
verificationRejectionReason: 'verificationRejectionReason',
|
||||
verificationRejectedAt: 'verificationRejectedAt',
|
||||
birthDate: 'birthDate',
|
||||
avatarFileId: 'avatarFileId',
|
||||
canPlaceOrders: 'canPlaceOrders',
|
||||
isActive: 'isActive',
|
||||
@@ -227,6 +234,12 @@ exports.Prisma.CustomerScalarFieldEnum = {
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.CustomerFavoriteScalarFieldEnum = {
|
||||
customerId: 'customerId',
|
||||
productId: 'productId',
|
||||
createdAt: 'createdAt'
|
||||
};
|
||||
|
||||
exports.Prisma.CustomerAddressScalarFieldEnum = {
|
||||
id: 'id',
|
||||
customerId: 'customerId',
|
||||
@@ -290,7 +303,10 @@ exports.Prisma.ProductScalarFieldEnum = {
|
||||
productCode: 'productCode',
|
||||
showType: 'showType',
|
||||
isActive: 'isActive',
|
||||
createdAt: 'createdAt'
|
||||
createdAt: 'createdAt',
|
||||
accountingStockWeight: 'accountingStockWeight',
|
||||
accountingStockCount: 'accountingStockCount',
|
||||
accountingStockAt: 'accountingStockAt'
|
||||
};
|
||||
|
||||
exports.Prisma.ProductImageScalarFieldEnum = {
|
||||
@@ -326,6 +342,8 @@ exports.Prisma.OrderScalarFieldEnum = {
|
||||
flatRialCharge: 'flatRialCharge',
|
||||
isSuspendedOverCreditLimit: 'isSuspendedOverCreditLimit',
|
||||
deliveredAt: 'deliveredAt',
|
||||
salesAgentId: 'salesAgentId',
|
||||
referrerCustomerId: 'referrerCustomerId',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
@@ -453,6 +471,51 @@ exports.Prisma.SettingScalarFieldEnum = {
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.AccountingLinkScalarFieldEnum = {
|
||||
id: 'id',
|
||||
provider: 'provider',
|
||||
entityType: 'entityType',
|
||||
entityId: 'entityId',
|
||||
externalCode: 'externalCode',
|
||||
externalName: 'externalName',
|
||||
origin: 'origin',
|
||||
snapshot: 'snapshot',
|
||||
syncedAt: 'syncedAt',
|
||||
createdAt: 'createdAt'
|
||||
};
|
||||
|
||||
exports.Prisma.AccountingOutboxScalarFieldEnum = {
|
||||
id: 'id',
|
||||
provider: 'provider',
|
||||
kind: 'kind',
|
||||
entityId: 'entityId',
|
||||
status: 'status',
|
||||
attempts: 'attempts',
|
||||
nextAttemptAt: 'nextAttemptAt',
|
||||
lastError: 'lastError',
|
||||
externalRef: 'externalRef',
|
||||
progress: 'progress',
|
||||
lastRequest: 'lastRequest',
|
||||
lastResponse: 'lastResponse',
|
||||
sentAt: 'sentAt',
|
||||
resolvedById: 'resolvedById',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.AccountingIssueScalarFieldEnum = {
|
||||
id: 'id',
|
||||
provider: 'provider',
|
||||
kind: 'kind',
|
||||
externalCode: 'externalCode',
|
||||
entityId: 'entityId',
|
||||
message: 'message',
|
||||
data: 'data',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt',
|
||||
resolvedAt: 'resolvedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.PromotionScalarFieldEnum = {
|
||||
id: 'id',
|
||||
title: 'title',
|
||||
@@ -567,6 +630,11 @@ exports.Prisma.JsonNullValueInput = {
|
||||
JsonNull: Prisma.JsonNull
|
||||
};
|
||||
|
||||
exports.Prisma.NullableJsonNullValueInput = {
|
||||
DbNull: Prisma.DbNull,
|
||||
JsonNull: Prisma.JsonNull
|
||||
};
|
||||
|
||||
exports.Prisma.QueryMode = {
|
||||
default: 'default',
|
||||
insensitive: 'insensitive'
|
||||
@@ -603,7 +671,8 @@ exports.VerificationStep = exports.$Enums.VerificationStep = {
|
||||
UNVERIFIED: 'UNVERIFIED',
|
||||
STEP_ONE_SUBMITTED: 'STEP_ONE_SUBMITTED',
|
||||
STEP_TWO_SUBMITTED: 'STEP_TWO_SUBMITTED',
|
||||
VERIFIED: 'VERIFIED'
|
||||
VERIFIED: 'VERIFIED',
|
||||
REJECTED: 'REJECTED'
|
||||
};
|
||||
|
||||
exports.ProductImageRole = exports.$Enums.ProductImageRole = {
|
||||
@@ -665,6 +734,27 @@ exports.CommentStatus = exports.$Enums.CommentStatus = {
|
||||
REJECTED: 'REJECTED'
|
||||
};
|
||||
|
||||
exports.AccountingEntityType = exports.$Enums.AccountingEntityType = {
|
||||
CUSTOMER: 'CUSTOMER',
|
||||
PRODUCT: 'PRODUCT'
|
||||
};
|
||||
|
||||
exports.AccountingOutboxKind = exports.$Enums.AccountingOutboxKind = {
|
||||
CUSTOMER_PUSH: 'CUSTOMER_PUSH',
|
||||
PRODUCT_PUSH: 'PRODUCT_PUSH',
|
||||
ORDER_DOCUMENT: 'ORDER_DOCUMENT',
|
||||
ORDER_DOCUMENT_DELETE: 'ORDER_DOCUMENT_DELETE'
|
||||
};
|
||||
|
||||
exports.AccountingOutboxStatus = exports.$Enums.AccountingOutboxStatus = {
|
||||
PENDING: 'PENDING',
|
||||
SENDING: 'SENDING',
|
||||
SENT: 'SENT',
|
||||
FAILED: 'FAILED',
|
||||
NEEDS_REVIEW: 'NEEDS_REVIEW',
|
||||
RESOLVED: 'RESOLVED'
|
||||
};
|
||||
|
||||
exports.PromotionScope = exports.$Enums.PromotionScope = {
|
||||
PRODUCT: 'PRODUCT',
|
||||
CATEGORY: 'CATEGORY',
|
||||
@@ -693,6 +783,7 @@ exports.Prisma.ModelName = {
|
||||
AdminSession: 'AdminSession',
|
||||
SalesExpertProfile: 'SalesExpertProfile',
|
||||
Customer: 'Customer',
|
||||
CustomerFavorite: 'CustomerFavorite',
|
||||
CustomerAddress: 'CustomerAddress',
|
||||
Branch: 'Branch',
|
||||
Workshop: 'Workshop',
|
||||
@@ -715,6 +806,9 @@ exports.Prisma.ModelName = {
|
||||
Permission: 'Permission',
|
||||
Comment: 'Comment',
|
||||
Setting: 'Setting',
|
||||
AccountingLink: 'AccountingLink',
|
||||
AccountingOutbox: 'AccountingOutbox',
|
||||
AccountingIssue: 'AccountingIssue',
|
||||
Promotion: 'Promotion',
|
||||
PromotionExclusion: 'PromotionExclusion',
|
||||
BranchAllocation: 'BranchAllocation',
|
||||
|
||||
Reference in New Issue
Block a user