Initial admin edits
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "AppBanner" ADD COLUMN "bannerLink" TEXT;
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
-- Preserve customer ↔ company links where only CustomerBrand existed (match by brand/company name or slug)
|
||||||
|
UPDATE "Customer" c
|
||||||
|
SET "companyId" = comp.id
|
||||||
|
FROM "CustomerBrand" cb
|
||||||
|
JOIN "Brand" b ON b.id = cb."brandId"
|
||||||
|
JOIN "Company" comp ON LOWER(TRIM(comp.name)) = LOWER(TRIM(b.name))
|
||||||
|
WHERE cb."customerId" = c.id
|
||||||
|
AND c."companyId" IS NULL;
|
||||||
|
|
||||||
|
UPDATE "Customer" c
|
||||||
|
SET "companyId" = comp.id
|
||||||
|
FROM "CustomerBrand" cb
|
||||||
|
JOIN "Brand" b ON b.id = cb."brandId"
|
||||||
|
JOIN "Company" comp ON b.slug IS NOT NULL AND comp.slug = b.slug
|
||||||
|
WHERE cb."customerId" = c.id
|
||||||
|
AND c."companyId" IS NULL;
|
||||||
|
|
||||||
|
ALTER TABLE "Product" DROP CONSTRAINT IF EXISTS "Product_brandId_fkey";
|
||||||
|
DROP INDEX IF EXISTS "Product_brandId_idx";
|
||||||
|
ALTER TABLE "Product" DROP COLUMN IF EXISTS "brandId";
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS "CustomerBrand";
|
||||||
|
DROP TABLE IF EXISTS "Brand";
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
-- نادمی تکس: شرکت اصلی (دریافت پرداخت + پیشفرض محصولات بدون شرکت مشخص)
|
||||||
|
INSERT INTO "Company" (
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"slug",
|
||||||
|
"description",
|
||||||
|
"isActive",
|
||||||
|
"displayOrder",
|
||||||
|
"cashInvoiceValidityHours",
|
||||||
|
"updatedAt"
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
'company-nademitex',
|
||||||
|
'نادمی تکس',
|
||||||
|
'nademitex',
|
||||||
|
'شرکت اصلی — تمام پرداختهای اپ به حسابهای این شرکت واریز میشود',
|
||||||
|
true,
|
||||||
|
0,
|
||||||
|
8,
|
||||||
|
CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
ON CONFLICT ("slug") DO UPDATE SET
|
||||||
|
"name" = EXCLUDED."name",
|
||||||
|
"description" = EXCLUDED."description",
|
||||||
|
"isActive" = true;
|
||||||
|
|
||||||
|
-- حسابهای بانکی فقط برای دریافت پرداخت نادمی تکس
|
||||||
|
UPDATE "BankAccount"
|
||||||
|
SET "companyId" = (SELECT "id" FROM "Company" WHERE "slug" = 'nademitex' LIMIT 1)
|
||||||
|
WHERE "companyId" IS NOT NULL;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Product" ADD COLUMN "specs" TEXT;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE "SystemSettings" ADD COLUMN "supportWhatsapp" TEXT;
|
||||||
|
ALTER TABLE "SystemSettings" ADD COLUMN "supportTelegram" TEXT;
|
||||||
|
ALTER TABLE "SystemSettings" ADD COLUMN "supportInstagram" TEXT;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- AlterEnum
|
||||||
|
ALTER TYPE "SettlementType" ADD VALUE 'cash_check';
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "SystemSettings" ADD COLUMN "defaultSettlementCashCheck" BOOLEAN NOT NULL DEFAULT false;
|
||||||
+11
-32
@@ -11,6 +11,7 @@ datasource db {
|
|||||||
enum SettlementType {
|
enum SettlementType {
|
||||||
cash
|
cash
|
||||||
check
|
check
|
||||||
|
cash_check
|
||||||
}
|
}
|
||||||
|
|
||||||
enum CheckRegistrationMode {
|
enum CheckRegistrationMode {
|
||||||
@@ -175,6 +176,8 @@ model Product {
|
|||||||
code String
|
code String
|
||||||
color String
|
color String
|
||||||
description String?
|
description String?
|
||||||
|
/// مشخصات آزاد محصول (نمایش در specs-extra اپ)
|
||||||
|
specs String? @db.Text
|
||||||
minMeterage Float @default(30)
|
minMeterage Float @default(30)
|
||||||
orderMultiplier Float @default(10)
|
orderMultiplier Float @default(10)
|
||||||
/// قیمت هر متر (تومان) برای نمایش کاتالوگ / پیشفاکتور
|
/// قیمت هر متر (تومان) برای نمایش کاتالوگ / پیشفاکتور
|
||||||
@@ -194,12 +197,10 @@ model Product {
|
|||||||
subtitle String?
|
subtitle String?
|
||||||
rollMeters Float @default(50)
|
rollMeters Float @default(50)
|
||||||
isNew Boolean @default(false)
|
isNew Boolean @default(false)
|
||||||
brandId String?
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||||||
brand Brand? @relation(fields: [brandId], references: [id], onDelete: SetNull)
|
|
||||||
cartItems CartItem[]
|
cartItems CartItem[]
|
||||||
orderItems OrderItem[]
|
orderItems OrderItem[]
|
||||||
favorites ProductFavorite[]
|
favorites ProductFavorite[]
|
||||||
@@ -208,7 +209,6 @@ model Product {
|
|||||||
|
|
||||||
@@unique([companyId, code])
|
@@unique([companyId, code])
|
||||||
@@index([companyId])
|
@@index([companyId])
|
||||||
@@index([brandId])
|
|
||||||
@@index([usageCategory])
|
@@index([usageCategory])
|
||||||
@@index([season])
|
@@index([season])
|
||||||
}
|
}
|
||||||
@@ -254,7 +254,6 @@ model Customer {
|
|||||||
favorites ProductFavorite[]
|
favorites ProductFavorite[]
|
||||||
catalogRequests CatalogRequest[]
|
catalogRequests CatalogRequest[]
|
||||||
productSuggestions ProductSuggestion[]
|
productSuggestions ProductSuggestion[]
|
||||||
customerBrands CustomerBrand[]
|
|
||||||
addresses CustomerAddress[]
|
addresses CustomerAddress[]
|
||||||
|
|
||||||
@@index([companyId])
|
@@index([companyId])
|
||||||
@@ -485,8 +484,9 @@ model SystemSettings {
|
|||||||
id String @id @default("default")
|
id String @id @default("default")
|
||||||
maxMeteragePerInvoice Int @default(5000)
|
maxMeteragePerInvoice Int @default(5000)
|
||||||
cashInvoiceValidityHours Int @default(8)
|
cashInvoiceValidityHours Int @default(8)
|
||||||
defaultSettlementCheck Boolean @default(true)
|
defaultSettlementCheck Boolean @default(true)
|
||||||
defaultSettlementCash Boolean @default(true)
|
defaultSettlementCash Boolean @default(true)
|
||||||
|
defaultSettlementCashCheck Boolean @default(false)
|
||||||
/// حداقل سفارش: تعداد طاقه از هر محصول (§۱۹)
|
/// حداقل سفارش: تعداد طاقه از هر محصول (§۱۹)
|
||||||
minOrderRollsPerProduct Int @default(1)
|
minOrderRollsPerProduct Int @default(1)
|
||||||
/// نمایش قیمت عمومی در کاتالوگ (پیشفرض خاموش)
|
/// نمایش قیمت عمومی در کاتالوگ (پیشفرض خاموش)
|
||||||
@@ -502,6 +502,9 @@ model SystemSettings {
|
|||||||
smsShippingUpdate Boolean @default(true)
|
smsShippingUpdate Boolean @default(true)
|
||||||
supportPhone String?
|
supportPhone String?
|
||||||
supportEmail String?
|
supportEmail String?
|
||||||
|
supportWhatsapp String?
|
||||||
|
supportTelegram String?
|
||||||
|
supportInstagram String?
|
||||||
/// تصاویر دستههای کاربرد/فصل اپ کاربر: { trousers: dataUrl, summer: dataUrl, ... }
|
/// تصاویر دستههای کاربرد/فصل اپ کاربر: { trousers: dataUrl, summer: dataUrl, ... }
|
||||||
categoryImagesJson Json?
|
categoryImagesJson Json?
|
||||||
/// تعاریف دستهها (کاربرد/فصل). اگر خالی باشد از مقادیر پیشفرض کد استفاده میشود.
|
/// تعاریف دستهها (کاربرد/فصل). اگر خالی باشد از مقادیر پیشفرض کد استفاده میشود.
|
||||||
@@ -517,6 +520,8 @@ model AppBanner {
|
|||||||
subtitle String?
|
subtitle String?
|
||||||
ctaLabel String?
|
ctaLabel String?
|
||||||
ctaLink String?
|
ctaLink String?
|
||||||
|
/// لینک کلیک روی تصویر بنر (مستقل از لینک دکمه)
|
||||||
|
bannerLink String?
|
||||||
imageUrl String?
|
imageUrl String?
|
||||||
displayOrder Int @default(0)
|
displayOrder Int @default(0)
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
@@ -627,32 +632,6 @@ model ProductFavorite {
|
|||||||
@@index([customerId])
|
@@index([customerId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model Brand {
|
|
||||||
id String @id @default(cuid())
|
|
||||||
name String
|
|
||||||
slug String? @unique
|
|
||||||
description String?
|
|
||||||
isActive Boolean @default(true)
|
|
||||||
displayOrder Int @default(0)
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
|
|
||||||
customers CustomerBrand[]
|
|
||||||
products Product[]
|
|
||||||
}
|
|
||||||
|
|
||||||
model CustomerBrand {
|
|
||||||
customerId String
|
|
||||||
brandId String
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
|
|
||||||
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
|
||||||
brand Brand @relation(fields: [brandId], references: [id], onDelete: Cascade)
|
|
||||||
|
|
||||||
@@id([customerId, brandId])
|
|
||||||
@@index([brandId])
|
|
||||||
}
|
|
||||||
|
|
||||||
model CustomerLevelDef {
|
model CustomerLevelDef {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
code String @unique
|
code String @unique
|
||||||
|
|||||||
+17
-6
@@ -66,6 +66,17 @@ async function main() {
|
|||||||
|
|
||||||
await prisma.company.createMany({
|
await prisma.company.createMany({
|
||||||
data: [
|
data: [
|
||||||
|
{
|
||||||
|
id: 'company-nademitex',
|
||||||
|
name: 'نادمی تکس',
|
||||||
|
slug: 'nademitex',
|
||||||
|
description: 'شرکت اصلی — دریافت تمام پرداختهای اپ',
|
||||||
|
isActive: true,
|
||||||
|
displayOrder: 0,
|
||||||
|
maxMeteragePerInvoice: null,
|
||||||
|
cashInvoiceValidityHours: 8,
|
||||||
|
cashInvoiceValidUntilHour: 17,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'company-par',
|
id: 'company-par',
|
||||||
name: 'شرکت پَر',
|
name: 'شرکت پَر',
|
||||||
@@ -95,9 +106,9 @@ async function main() {
|
|||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
id: 'ba-par-1',
|
id: 'ba-par-1',
|
||||||
companyId: 'company-par',
|
companyId: 'company-nademitex',
|
||||||
bankName: 'بانک ملی',
|
bankName: 'بانک ملی',
|
||||||
accountHolder: 'شرکت پَر',
|
accountHolder: 'نادمی تکس',
|
||||||
accountNumber: '0123456789',
|
accountNumber: '0123456789',
|
||||||
cardNumber: '6037-9912-3456-7890',
|
cardNumber: '6037-9912-3456-7890',
|
||||||
iban: 'IR120170000000012345678901',
|
iban: 'IR120170000000012345678901',
|
||||||
@@ -106,9 +117,9 @@ async function main() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ba-par-2',
|
id: 'ba-par-2',
|
||||||
companyId: 'company-par',
|
companyId: 'company-nademitex',
|
||||||
bankName: 'بانک صادرات',
|
bankName: 'بانک صادرات',
|
||||||
accountHolder: 'شرکت پَر',
|
accountHolder: 'نادمی تکس',
|
||||||
accountNumber: '9876543210',
|
accountNumber: '9876543210',
|
||||||
cardNumber: '6037-6912-3456-7891',
|
cardNumber: '6037-6912-3456-7891',
|
||||||
iban: 'IR620190000000098765432101',
|
iban: 'IR620190000000098765432101',
|
||||||
@@ -117,9 +128,9 @@ async function main() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ba-fam-1',
|
id: 'ba-fam-1',
|
||||||
companyId: 'company-fam-baft',
|
companyId: 'company-nademitex',
|
||||||
bankName: 'بانک ملت',
|
bankName: 'بانک ملت',
|
||||||
accountHolder: 'فامبافت',
|
accountHolder: 'نادمی تکس',
|
||||||
accountNumber: '1122334455',
|
accountNumber: '1122334455',
|
||||||
cardNumber: '6104-3378-9012-3456',
|
cardNumber: '6104-3378-9012-3456',
|
||||||
iban: 'IR450120000000011223344556',
|
iban: 'IR450120000000011223344556',
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "AppBanner" ADD COLUMN "bannerLink" TEXT;
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
-- Preserve customer ↔ company links where only CustomerBrand existed (match by brand/company name or slug)
|
||||||
|
UPDATE "Customer" c
|
||||||
|
SET "companyId" = comp.id
|
||||||
|
FROM "CustomerBrand" cb
|
||||||
|
JOIN "Brand" b ON b.id = cb."brandId"
|
||||||
|
JOIN "Company" comp ON LOWER(TRIM(comp.name)) = LOWER(TRIM(b.name))
|
||||||
|
WHERE cb."customerId" = c.id
|
||||||
|
AND c."companyId" IS NULL;
|
||||||
|
|
||||||
|
UPDATE "Customer" c
|
||||||
|
SET "companyId" = comp.id
|
||||||
|
FROM "CustomerBrand" cb
|
||||||
|
JOIN "Brand" b ON b.id = cb."brandId"
|
||||||
|
JOIN "Company" comp ON b.slug IS NOT NULL AND comp.slug = b.slug
|
||||||
|
WHERE cb."customerId" = c.id
|
||||||
|
AND c."companyId" IS NULL;
|
||||||
|
|
||||||
|
ALTER TABLE "Product" DROP CONSTRAINT IF EXISTS "Product_brandId_fkey";
|
||||||
|
DROP INDEX IF EXISTS "Product_brandId_idx";
|
||||||
|
ALTER TABLE "Product" DROP COLUMN IF EXISTS "brandId";
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS "CustomerBrand";
|
||||||
|
DROP TABLE IF EXISTS "Brand";
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
-- نادمی تکس: شرکت اصلی (دریافت پرداخت + پیشفرض محصولات بدون شرکت مشخص)
|
||||||
|
INSERT INTO "Company" (
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"slug",
|
||||||
|
"description",
|
||||||
|
"isActive",
|
||||||
|
"displayOrder",
|
||||||
|
"cashInvoiceValidityHours",
|
||||||
|
"updatedAt"
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
'company-nademitex',
|
||||||
|
'نادمی تکس',
|
||||||
|
'nademitex',
|
||||||
|
'شرکت اصلی — تمام پرداختهای اپ به حسابهای این شرکت واریز میشود',
|
||||||
|
true,
|
||||||
|
0,
|
||||||
|
8,
|
||||||
|
CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
ON CONFLICT ("slug") DO UPDATE SET
|
||||||
|
"name" = EXCLUDED."name",
|
||||||
|
"description" = EXCLUDED."description",
|
||||||
|
"isActive" = true;
|
||||||
|
|
||||||
|
-- حسابهای بانکی فقط برای دریافت پرداخت نادمی تکس
|
||||||
|
UPDATE "BankAccount"
|
||||||
|
SET "companyId" = (SELECT "id" FROM "Company" WHERE "slug" = 'nademitex' LIMIT 1)
|
||||||
|
WHERE "companyId" IS NOT NULL;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Product" ADD COLUMN "specs" TEXT;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE "SystemSettings" ADD COLUMN "supportWhatsapp" TEXT;
|
||||||
|
ALTER TABLE "SystemSettings" ADD COLUMN "supportTelegram" TEXT;
|
||||||
|
ALTER TABLE "SystemSettings" ADD COLUMN "supportInstagram" TEXT;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- AlterEnum
|
||||||
|
ALTER TYPE "SettlementType" ADD VALUE 'cash_check';
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "SystemSettings" ADD COLUMN "defaultSettlementCashCheck" BOOLEAN NOT NULL DEFAULT false;
|
||||||
+28
-49
@@ -11,6 +11,7 @@ datasource db {
|
|||||||
enum SettlementType {
|
enum SettlementType {
|
||||||
cash
|
cash
|
||||||
check
|
check
|
||||||
|
cash_check
|
||||||
}
|
}
|
||||||
|
|
||||||
enum CheckRegistrationMode {
|
enum CheckRegistrationMode {
|
||||||
@@ -175,6 +176,8 @@ model Product {
|
|||||||
code String
|
code String
|
||||||
color String
|
color String
|
||||||
description String?
|
description String?
|
||||||
|
/// مشخصات آزاد محصول (نمایش در specs-extra اپ)
|
||||||
|
specs String? @db.Text
|
||||||
minMeterage Float @default(30)
|
minMeterage Float @default(30)
|
||||||
orderMultiplier Float @default(10)
|
orderMultiplier Float @default(10)
|
||||||
/// قیمت هر متر (تومان) برای نمایش کاتالوگ / پیشفاکتور
|
/// قیمت هر متر (تومان) برای نمایش کاتالوگ / پیشفاکتور
|
||||||
@@ -194,12 +197,10 @@ model Product {
|
|||||||
subtitle String?
|
subtitle String?
|
||||||
rollMeters Float @default(50)
|
rollMeters Float @default(50)
|
||||||
isNew Boolean @default(false)
|
isNew Boolean @default(false)
|
||||||
brandId String?
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||||||
brand Brand? @relation(fields: [brandId], references: [id], onDelete: SetNull)
|
|
||||||
cartItems CartItem[]
|
cartItems CartItem[]
|
||||||
orderItems OrderItem[]
|
orderItems OrderItem[]
|
||||||
favorites ProductFavorite[]
|
favorites ProductFavorite[]
|
||||||
@@ -208,7 +209,6 @@ model Product {
|
|||||||
|
|
||||||
@@unique([companyId, code])
|
@@unique([companyId, code])
|
||||||
@@index([companyId])
|
@@index([companyId])
|
||||||
@@index([brandId])
|
|
||||||
@@index([usageCategory])
|
@@index([usageCategory])
|
||||||
@@index([season])
|
@@index([season])
|
||||||
}
|
}
|
||||||
@@ -254,7 +254,6 @@ model Customer {
|
|||||||
favorites ProductFavorite[]
|
favorites ProductFavorite[]
|
||||||
catalogRequests CatalogRequest[]
|
catalogRequests CatalogRequest[]
|
||||||
productSuggestions ProductSuggestion[]
|
productSuggestions ProductSuggestion[]
|
||||||
customerBrands CustomerBrand[]
|
|
||||||
addresses CustomerAddress[]
|
addresses CustomerAddress[]
|
||||||
|
|
||||||
@@index([companyId])
|
@@index([companyId])
|
||||||
@@ -482,31 +481,35 @@ model Shipment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model SystemSettings {
|
model SystemSettings {
|
||||||
id String @id @default("default")
|
id String @id @default("default")
|
||||||
maxMeteragePerInvoice Int @default(5000)
|
maxMeteragePerInvoice Int @default(5000)
|
||||||
cashInvoiceValidityHours Int @default(8)
|
cashInvoiceValidityHours Int @default(8)
|
||||||
defaultSettlementCheck Boolean @default(true)
|
defaultSettlementCheck Boolean @default(true)
|
||||||
defaultSettlementCash Boolean @default(true)
|
defaultSettlementCash Boolean @default(true)
|
||||||
|
defaultSettlementCashCheck Boolean @default(false)
|
||||||
/// حداقل سفارش: تعداد طاقه از هر محصول (§۱۹)
|
/// حداقل سفارش: تعداد طاقه از هر محصول (§۱۹)
|
||||||
minOrderRollsPerProduct Int @default(1)
|
minOrderRollsPerProduct Int @default(1)
|
||||||
/// نمایش قیمت عمومی در کاتالوگ (پیشفرض خاموش)
|
/// نمایش قیمت عمومی در کاتالوگ (پیشفرض خاموش)
|
||||||
showCatalogPublicPrice Boolean @default(false)
|
showCatalogPublicPrice Boolean @default(false)
|
||||||
/// اجازه قیمتگذاری توسط واسطه (§۱۹)
|
/// اجازه قیمتگذاری توسط واسطه (§۱۹)
|
||||||
allowAgentPricing Boolean @default(false)
|
allowAgentPricing Boolean @default(false)
|
||||||
/// کف قیمت واحد برای قیمتگذاری واسطه (ریال) — اختیاری
|
/// کف قیمت واحد برای قیمتگذاری واسطه (ریال) — اختیاری
|
||||||
agentMinUnitPrice Int?
|
agentMinUnitPrice Int?
|
||||||
smsOrderRegistered Boolean @default(true)
|
smsOrderRegistered Boolean @default(true)
|
||||||
smsPricingReady Boolean @default(true)
|
smsPricingReady Boolean @default(true)
|
||||||
smsPaymentReminder Boolean @default(true)
|
smsPaymentReminder Boolean @default(true)
|
||||||
smsCheckReview Boolean @default(true)
|
smsCheckReview Boolean @default(true)
|
||||||
smsShippingUpdate Boolean @default(true)
|
smsShippingUpdate Boolean @default(true)
|
||||||
supportPhone String?
|
supportPhone String?
|
||||||
supportEmail String?
|
supportEmail String?
|
||||||
|
supportWhatsapp String?
|
||||||
|
supportTelegram String?
|
||||||
|
supportInstagram String?
|
||||||
/// تصاویر دستههای کاربرد/فصل اپ کاربر: { trousers: dataUrl, summer: dataUrl, ... }
|
/// تصاویر دستههای کاربرد/فصل اپ کاربر: { trousers: dataUrl, summer: dataUrl, ... }
|
||||||
categoryImagesJson Json?
|
categoryImagesJson Json?
|
||||||
/// تعاریف دستهها (کاربرد/فصل). اگر خالی باشد از مقادیر پیشفرض کد استفاده میشود.
|
/// تعاریف دستهها (کاربرد/فصل). اگر خالی باشد از مقادیر پیشفرض کد استفاده میشود.
|
||||||
categoryDefsJson Json?
|
categoryDefsJson Json?
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
/// بنرهای قابل مدیریت بخشهای اپ کاربر
|
/// بنرهای قابل مدیریت بخشهای اپ کاربر
|
||||||
@@ -517,6 +520,8 @@ model AppBanner {
|
|||||||
subtitle String?
|
subtitle String?
|
||||||
ctaLabel String?
|
ctaLabel String?
|
||||||
ctaLink String?
|
ctaLink String?
|
||||||
|
/// لینک کلیک روی تصویر بنر (مستقل از لینک دکمه)
|
||||||
|
bannerLink String?
|
||||||
imageUrl String?
|
imageUrl String?
|
||||||
displayOrder Int @default(0)
|
displayOrder Int @default(0)
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
@@ -627,32 +632,6 @@ model ProductFavorite {
|
|||||||
@@index([customerId])
|
@@index([customerId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model Brand {
|
|
||||||
id String @id @default(cuid())
|
|
||||||
name String
|
|
||||||
slug String? @unique
|
|
||||||
description String?
|
|
||||||
isActive Boolean @default(true)
|
|
||||||
displayOrder Int @default(0)
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
|
|
||||||
customers CustomerBrand[]
|
|
||||||
products Product[]
|
|
||||||
}
|
|
||||||
|
|
||||||
model CustomerBrand {
|
|
||||||
customerId String
|
|
||||||
brandId String
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
|
|
||||||
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
|
||||||
brand Brand @relation(fields: [brandId], references: [id], onDelete: Cascade)
|
|
||||||
|
|
||||||
@@id([customerId, brandId])
|
|
||||||
@@index([brandId])
|
|
||||||
}
|
|
||||||
|
|
||||||
model CustomerLevelDef {
|
model CustomerLevelDef {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
code String @unique
|
code String @unique
|
||||||
|
|||||||
+28
-28
File diff suppressed because one or more lines are too long
+367
-446
File diff suppressed because one or more lines are too long
@@ -19,7 +19,6 @@ import { CartsModule } from './carts/carts.module';
|
|||||||
import { NotificationsModule } from './notifications/notifications.module';
|
import { NotificationsModule } from './notifications/notifications.module';
|
||||||
import { AgentModule } from './agent/agent.module';
|
import { AgentModule } from './agent/agent.module';
|
||||||
import { CatalogRequestsModule } from './catalog-requests/catalog-requests.module';
|
import { CatalogRequestsModule } from './catalog-requests/catalog-requests.module';
|
||||||
import { BrandsModule } from './brands/brands.module';
|
|
||||||
import { LevelsModule } from './levels/levels.module';
|
import { LevelsModule } from './levels/levels.module';
|
||||||
import { SuggestionsModule } from './suggestions/suggestions.module';
|
import { SuggestionsModule } from './suggestions/suggestions.module';
|
||||||
import { BannersModule } from './banners/banners.module';
|
import { BannersModule } from './banners/banners.module';
|
||||||
@@ -46,7 +45,6 @@ import { InternalMessagesModule } from './internal-messages/internal-messages.mo
|
|||||||
NotificationsModule,
|
NotificationsModule,
|
||||||
AgentModule,
|
AgentModule,
|
||||||
CatalogRequestsModule,
|
CatalogRequestsModule,
|
||||||
BrandsModule,
|
|
||||||
LevelsModule,
|
LevelsModule,
|
||||||
SuggestionsModule,
|
SuggestionsModule,
|
||||||
BannersModule,
|
BannersModule,
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { BadRequestException } from '@nestjs/common';
|
||||||
|
import { normalizeBannerLink } from './banner-link.util';
|
||||||
|
|
||||||
|
describe('normalizeBannerLink', () => {
|
||||||
|
it('allows empty values', () => {
|
||||||
|
expect(normalizeBannerLink(null)).toBeNull();
|
||||||
|
expect(normalizeBannerLink('')).toBeNull();
|
||||||
|
expect(normalizeBannerLink(' ')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts internal paths', () => {
|
||||||
|
expect(normalizeBannerLink('/categories')).toBe('/categories');
|
||||||
|
expect(normalizeBannerLink(' /products?q=1 ')).toBe('/products?q=1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts http(s) URLs', () => {
|
||||||
|
expect(normalizeBannerLink('https://example.com/path')).toBe(
|
||||||
|
'https://example.com/path',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects invalid links', () => {
|
||||||
|
expect(() => normalizeBannerLink('ftp://x.com')).toThrow(BadRequestException);
|
||||||
|
expect(() => normalizeBannerLink('not a url')).toThrow(BadRequestException);
|
||||||
|
expect(() => normalizeBannerLink('/bad path')).toThrow(BadRequestException);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { BadRequestException } from '@nestjs/common';
|
||||||
|
|
||||||
|
/** مسیر داخلی (`/…`) یا URL با http/https */
|
||||||
|
export function normalizeBannerLink(value?: string | null): string | null {
|
||||||
|
if (value == null || value === '') return null;
|
||||||
|
const link = value.trim();
|
||||||
|
if (!link) return null;
|
||||||
|
if (link.startsWith('/')) {
|
||||||
|
if (!/^\/[^\s]*$/.test(link)) {
|
||||||
|
throw new BadRequestException('لینک بنر نامعتبر است');
|
||||||
|
}
|
||||||
|
return link;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const url = new URL(link);
|
||||||
|
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
||||||
|
throw new Error('invalid protocol');
|
||||||
|
}
|
||||||
|
return link;
|
||||||
|
} catch {
|
||||||
|
throw new BadRequestException('لینک بنر نامعتبر است');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import { normalizeBannerLink } from './banner-link.util';
|
||||||
import { CreateBannerDto, UpdateBannerDto } from './dto/banner.dto';
|
import { CreateBannerDto, UpdateBannerDto } from './dto/banner.dto';
|
||||||
|
|
||||||
const ALLOWED_SLOTS = new Set(['home_hero', 'products_hero', 'categories_hero']);
|
const ALLOWED_SLOTS = new Set(['home_hero', 'products_hero', 'categories_hero']);
|
||||||
@@ -28,6 +29,7 @@ export class BannersService {
|
|||||||
subtitle: dto.subtitle?.trim() || null,
|
subtitle: dto.subtitle?.trim() || null,
|
||||||
ctaLabel: dto.ctaLabel?.trim() || null,
|
ctaLabel: dto.ctaLabel?.trim() || null,
|
||||||
ctaLink: dto.ctaLink?.trim() || null,
|
ctaLink: dto.ctaLink?.trim() || null,
|
||||||
|
bannerLink: normalizeBannerLink(dto.bannerLink),
|
||||||
imageUrl: this.normalizeImage(dto.imageUrl),
|
imageUrl: this.normalizeImage(dto.imageUrl),
|
||||||
displayOrder: dto.displayOrder ?? 0,
|
displayOrder: dto.displayOrder ?? 0,
|
||||||
isActive: dto.isActive ?? true,
|
isActive: dto.isActive ?? true,
|
||||||
@@ -52,6 +54,10 @@ export class BannersService {
|
|||||||
dto.ctaLabel === undefined ? undefined : dto.ctaLabel?.trim() || null,
|
dto.ctaLabel === undefined ? undefined : dto.ctaLabel?.trim() || null,
|
||||||
ctaLink:
|
ctaLink:
|
||||||
dto.ctaLink === undefined ? undefined : dto.ctaLink?.trim() || null,
|
dto.ctaLink === undefined ? undefined : dto.ctaLink?.trim() || null,
|
||||||
|
bannerLink:
|
||||||
|
dto.bannerLink === undefined
|
||||||
|
? undefined
|
||||||
|
: normalizeBannerLink(dto.bannerLink),
|
||||||
imageUrl:
|
imageUrl:
|
||||||
dto.imageUrl === undefined
|
dto.imageUrl === undefined
|
||||||
? undefined
|
? undefined
|
||||||
@@ -109,6 +115,7 @@ export class BannersService {
|
|||||||
subtitle: string | null;
|
subtitle: string | null;
|
||||||
ctaLabel: string | null;
|
ctaLabel: string | null;
|
||||||
ctaLink: string | null;
|
ctaLink: string | null;
|
||||||
|
bannerLink: string | null;
|
||||||
imageUrl: string | null;
|
imageUrl: string | null;
|
||||||
displayOrder: number;
|
displayOrder: number;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
@@ -125,6 +132,7 @@ export class BannersService {
|
|||||||
subtitle: row.subtitle ?? '',
|
subtitle: row.subtitle ?? '',
|
||||||
ctaLabel: row.ctaLabel ?? '',
|
ctaLabel: row.ctaLabel ?? '',
|
||||||
ctaLink: row.ctaLink ?? '',
|
ctaLink: row.ctaLink ?? '',
|
||||||
|
bannerLink: row.bannerLink ?? '',
|
||||||
imageUrl: row.imageUrl ?? '',
|
imageUrl: row.imageUrl ?? '',
|
||||||
displayOrder: row.displayOrder,
|
displayOrder: row.displayOrder,
|
||||||
isActive: row.isActive,
|
isActive: row.isActive,
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ export class CreateBannerDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
ctaLink?: string | null;
|
ctaLink?: string | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
bannerLink?: string | null;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
imageUrl?: string | null;
|
imageUrl?: string | null;
|
||||||
@@ -79,6 +83,10 @@ export class UpdateBannerDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
ctaLink?: string | null;
|
ctaLink?: string | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
bannerLink?: string | null;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
imageUrl?: string | null;
|
imageUrl?: string | null;
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
import { Body, Controller, Get, Param, Patch, Post, Query } from '@nestjs/common';
|
|
||||||
import { BrandsService } from './brands.service';
|
|
||||||
import { AuthType } from '../common/decorators/auth.decorators';
|
|
||||||
import { CreateBrandDto, UpdateBrandDto } from './dto/brand.dto';
|
|
||||||
import { IsArray, IsString } from 'class-validator';
|
|
||||||
|
|
||||||
class SetBrandsBody {
|
|
||||||
@IsArray()
|
|
||||||
@IsString({ each: true })
|
|
||||||
brandIds!: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
@Controller()
|
|
||||||
export class BrandsController {
|
|
||||||
constructor(private readonly brandsService: BrandsService) {}
|
|
||||||
|
|
||||||
@AuthType('admin')
|
|
||||||
@Get('admin/brands')
|
|
||||||
list(@Query('activeOnly') activeOnly?: string) {
|
|
||||||
return this.brandsService.list(activeOnly === 'true');
|
|
||||||
}
|
|
||||||
|
|
||||||
@AuthType('admin')
|
|
||||||
@Post('admin/brands')
|
|
||||||
create(@Body() dto: CreateBrandDto) {
|
|
||||||
return this.brandsService.create(dto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@AuthType('admin')
|
|
||||||
@Patch('admin/brands/:id')
|
|
||||||
update(@Param('id') id: string, @Body() dto: UpdateBrandDto) {
|
|
||||||
return this.brandsService.update(id, dto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@AuthType('admin')
|
|
||||||
@Get('admin/customers/:customerId/brands')
|
|
||||||
customerBrands(@Param('customerId') customerId: string) {
|
|
||||||
return this.brandsService.listCustomerBrands(customerId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@AuthType('admin')
|
|
||||||
@Post('admin/customers/:customerId/brands')
|
|
||||||
setCustomerBrands(
|
|
||||||
@Param('customerId') customerId: string,
|
|
||||||
@Body() body: SetBrandsBody,
|
|
||||||
) {
|
|
||||||
return this.brandsService.setCustomerBrands(customerId, body.brandIds ?? []);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { BrandsController } from './brands.controller';
|
|
||||||
import { BrandsService } from './brands.service';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
controllers: [BrandsController],
|
|
||||||
providers: [BrandsService],
|
|
||||||
exports: [BrandsService],
|
|
||||||
})
|
|
||||||
export class BrandsModule {}
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
|
||||||
import { CreateBrandDto, UpdateBrandDto } from './dto/brand.dto';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class BrandsService {
|
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
|
||||||
|
|
||||||
async list(activeOnly?: boolean) {
|
|
||||||
const rows = await this.prisma.brand.findMany({
|
|
||||||
where: activeOnly ? { isActive: true } : undefined,
|
|
||||||
orderBy: [{ displayOrder: 'asc' }, { name: 'asc' }],
|
|
||||||
include: { _count: { select: { products: true } } },
|
|
||||||
});
|
|
||||||
return rows.map((b) => ({
|
|
||||||
id: b.id,
|
|
||||||
name: b.name,
|
|
||||||
slug: b.slug ?? undefined,
|
|
||||||
description: b.description ?? undefined,
|
|
||||||
isActive: b.isActive,
|
|
||||||
displayOrder: b.displayOrder,
|
|
||||||
productsCount: b._count.products,
|
|
||||||
createdAt: b.createdAt.toISOString(),
|
|
||||||
updatedAt: b.updatedAt.toISOString(),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
async create(dto: CreateBrandDto) {
|
|
||||||
const brand = await this.prisma.brand.create({
|
|
||||||
data: {
|
|
||||||
name: dto.name.trim(),
|
|
||||||
slug: dto.slug?.trim() || null,
|
|
||||||
description: dto.description?.trim() || null,
|
|
||||||
displayOrder: dto.displayOrder ?? 0,
|
|
||||||
isActive: dto.isActive ?? true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return this.map(brand);
|
|
||||||
}
|
|
||||||
|
|
||||||
async update(id: string, dto: UpdateBrandDto) {
|
|
||||||
await this.ensure(id);
|
|
||||||
const brand = await this.prisma.brand.update({
|
|
||||||
where: { id },
|
|
||||||
data: {
|
|
||||||
name: dto.name?.trim(),
|
|
||||||
slug:
|
|
||||||
dto.slug === undefined
|
|
||||||
? undefined
|
|
||||||
: dto.slug?.trim() || null,
|
|
||||||
description:
|
|
||||||
dto.description === undefined
|
|
||||||
? undefined
|
|
||||||
: dto.description?.trim() || null,
|
|
||||||
displayOrder: dto.displayOrder,
|
|
||||||
isActive: dto.isActive,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return this.map(brand);
|
|
||||||
}
|
|
||||||
|
|
||||||
async setCustomerBrands(customerId: string, brandIds: string[]) {
|
|
||||||
const customer = await this.prisma.customer.findUnique({
|
|
||||||
where: { id: customerId },
|
|
||||||
});
|
|
||||||
if (!customer) throw new NotFoundException('مشتری یافت نشد');
|
|
||||||
await this.prisma.customerBrand.deleteMany({ where: { customerId } });
|
|
||||||
if (brandIds.length) {
|
|
||||||
await this.prisma.customerBrand.createMany({
|
|
||||||
data: brandIds.map((brandId) => ({ customerId, brandId })),
|
|
||||||
skipDuplicates: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return this.listCustomerBrands(customerId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async listCustomerBrands(customerId: string) {
|
|
||||||
const rows = await this.prisma.customerBrand.findMany({
|
|
||||||
where: { customerId },
|
|
||||||
include: { brand: true },
|
|
||||||
});
|
|
||||||
return rows.map((r) => this.map(r.brand));
|
|
||||||
}
|
|
||||||
|
|
||||||
private async ensure(id: string) {
|
|
||||||
const row = await this.prisma.brand.findUnique({ where: { id } });
|
|
||||||
if (!row) throw new NotFoundException('برند یافت نشد');
|
|
||||||
return row;
|
|
||||||
}
|
|
||||||
|
|
||||||
private map(b: {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
slug: string | null;
|
|
||||||
description: string | null;
|
|
||||||
isActive: boolean;
|
|
||||||
displayOrder: number;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}) {
|
|
||||||
return {
|
|
||||||
id: b.id,
|
|
||||||
name: b.name,
|
|
||||||
slug: b.slug ?? undefined,
|
|
||||||
description: b.description ?? undefined,
|
|
||||||
isActive: b.isActive,
|
|
||||||
displayOrder: b.displayOrder,
|
|
||||||
createdAt: b.createdAt.toISOString(),
|
|
||||||
updatedAt: b.updatedAt.toISOString(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import {
|
|
||||||
IsBoolean,
|
|
||||||
IsInt,
|
|
||||||
IsOptional,
|
|
||||||
IsString,
|
|
||||||
Min,
|
|
||||||
MinLength,
|
|
||||||
} from 'class-validator';
|
|
||||||
import { Type } from 'class-transformer';
|
|
||||||
|
|
||||||
export class CreateBrandDto {
|
|
||||||
@IsString()
|
|
||||||
@MinLength(1)
|
|
||||||
name!: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
slug?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
description?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@Type(() => Number)
|
|
||||||
@IsInt()
|
|
||||||
@Min(0)
|
|
||||||
displayOrder?: number;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
isActive?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UpdateBrandDto {
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
@MinLength(1)
|
|
||||||
name?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
slug?: string | null;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
description?: string | null;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@Type(() => Number)
|
|
||||||
@IsInt()
|
|
||||||
@Min(0)
|
|
||||||
displayOrder?: number;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
isActive?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SetCustomerBrandsDto {
|
|
||||||
brands!: string[];
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,12 @@ import {
|
|||||||
Injectable,
|
Injectable,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { CashPaymentStatus, OrderStatus, SettlementType } from '@prisma/client';
|
import {
|
||||||
|
CashPaymentStatus,
|
||||||
|
CheckRegistrationMode,
|
||||||
|
OrderStatus,
|
||||||
|
SettlementType,
|
||||||
|
} from '@prisma/client';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import {
|
import {
|
||||||
SubmitCashReceiptDto,
|
SubmitCashReceiptDto,
|
||||||
@@ -12,7 +17,10 @@ import {
|
|||||||
} from './dto/cash-payment.dto';
|
} from './dto/cash-payment.dto';
|
||||||
import { joinFullName } from '../common/utils/name';
|
import { joinFullName } from '../common/utils/name';
|
||||||
import { NotificationsService } from '../notifications/notifications.service';
|
import { NotificationsService } from '../notifications/notifications.service';
|
||||||
|
import { requirePaymentRecipientCompany } from '../companies/default-company';
|
||||||
|
import {
|
||||||
|
usesCashReceiptPhase,
|
||||||
|
} from '../common/utils/settlement-type';
|
||||||
/** بعد از تأیید پرداخت یا ارسال، رسید جدید نباید وضعیت را به عقب برگرداند. */
|
/** بعد از تأیید پرداخت یا ارسال، رسید جدید نباید وضعیت را به عقب برگرداند. */
|
||||||
const RECEIPT_SUBMITTABLE_STATUSES: OrderStatus[] = [
|
const RECEIPT_SUBMITTABLE_STATUSES: OrderStatus[] = [
|
||||||
OrderStatus.cash_in_progress,
|
OrderStatus.cash_in_progress,
|
||||||
@@ -42,7 +50,7 @@ export class CashPaymentsService {
|
|||||||
if (order.userId !== userId) {
|
if (order.userId !== userId) {
|
||||||
throw new ForbiddenException('دسترسی به این سفارش مجاز نیست');
|
throw new ForbiddenException('دسترسی به این سفارش مجاز نیست');
|
||||||
}
|
}
|
||||||
if (order.settlementType !== SettlementType.cash) {
|
if (!usesCashReceiptPhase(order.settlementType)) {
|
||||||
throw new BadRequestException('این سفارش تسویه نقدی نیست');
|
throw new BadRequestException('این سفارش تسویه نقدی نیست');
|
||||||
}
|
}
|
||||||
if (!RECEIPT_SUBMITTABLE_STATUSES.includes(order.status)) {
|
if (!RECEIPT_SUBMITTABLE_STATUSES.includes(order.status)) {
|
||||||
@@ -59,18 +67,11 @@ export class CashPaymentsService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const companyIds = [
|
const paymentRecipient = await requirePaymentRecipientCompany(this.prisma);
|
||||||
...new Set(
|
|
||||||
[
|
|
||||||
order.companyId,
|
|
||||||
...order.items.map((item) => item.companyId).filter(Boolean),
|
|
||||||
].filter(Boolean),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
const account = await this.prisma.bankAccount.findFirst({
|
const account = await this.prisma.bankAccount.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: dto.destinationAccountId,
|
id: dto.destinationAccountId,
|
||||||
companyId: { in: companyIds },
|
companyId: paymentRecipient.id,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -175,7 +176,10 @@ export class CashPaymentsService {
|
|||||||
// رد رسید باید مشتری را به مرحله پرداخت برگرداند تا بتواند رسید تازه بفرستد.
|
// رد رسید باید مشتری را به مرحله پرداخت برگرداند تا بتواند رسید تازه بفرستد.
|
||||||
let nextStatus: OrderStatus | null = null;
|
let nextStatus: OrderStatus | null = null;
|
||||||
if (dto.status === CashPaymentStatus.approved) {
|
if (dto.status === CashPaymentStatus.approved) {
|
||||||
nextStatus = OrderStatus.cash_confirmed;
|
nextStatus =
|
||||||
|
receipt.order.settlementType === SettlementType.cash_check
|
||||||
|
? OrderStatus.check_in_progress
|
||||||
|
: OrderStatus.cash_confirmed;
|
||||||
} else if (dto.status === CashPaymentStatus.rejected) {
|
} else if (dto.status === CashPaymentStatus.rejected) {
|
||||||
nextStatus = OrderStatus.awaiting_cash_payment;
|
nextStatus = OrderStatus.awaiting_cash_payment;
|
||||||
} else if (dto.status === CashPaymentStatus.expired) {
|
} else if (dto.status === CashPaymentStatus.expired) {
|
||||||
@@ -185,7 +189,16 @@ export class CashPaymentsService {
|
|||||||
if (nextStatus && nextStatus !== receipt.order.status) {
|
if (nextStatus && nextStatus !== receipt.order.status) {
|
||||||
await tx.order.update({
|
await tx.order.update({
|
||||||
where: { id: receipt.orderId },
|
where: { id: receipt.orderId },
|
||||||
data: { status: nextStatus },
|
data: {
|
||||||
|
status: nextStatus,
|
||||||
|
...(nextStatus === OrderStatus.check_in_progress &&
|
||||||
|
receipt.order.settlementType === SettlementType.cash_check
|
||||||
|
? {
|
||||||
|
checkMode:
|
||||||
|
receipt.order.checkMode ?? CheckRegistrationMode.full,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
await tx.orderStatusHistory.create({
|
await tx.orderStatusHistory.create({
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { BadRequestException } from '@nestjs/common';
|
||||||
|
import { dayRangeForRegisteredOn } from './catalog-request-date.util';
|
||||||
|
|
||||||
|
describe('dayRangeForRegisteredOn', () => {
|
||||||
|
it('builds start/end of civil day in Asia/Tehran', () => {
|
||||||
|
const { gte, lt } = dayRangeForRegisteredOn('2026-10-01');
|
||||||
|
expect(gte.toISOString()).toBe('2026-09-30T20:30:00.000Z');
|
||||||
|
expect(lt.toISOString()).toBe('2026-10-01T20:30:00.000Z');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects invalid input', () => {
|
||||||
|
expect(() => dayRangeForRegisteredOn('01-10-2026')).toThrow(
|
||||||
|
BadRequestException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { BadRequestException } from '@nestjs/common';
|
||||||
|
|
||||||
|
/** Iran standard time — same civil day as Jalali date picker (`YYYY-MM-DD`). */
|
||||||
|
const TEHRAN_DAY_START = /^(\d{4})-(\d{2})-(\d{2})$/;
|
||||||
|
|
||||||
|
/** Calendar day range in Asia/Tehran for `YYYY-MM-DD` (registration date filter). */
|
||||||
|
export function dayRangeForRegisteredOn(
|
||||||
|
registeredOn: string,
|
||||||
|
): { gte: Date; lt: Date } {
|
||||||
|
const m = registeredOn.trim().match(TEHRAN_DAY_START);
|
||||||
|
if (!m) {
|
||||||
|
throw new BadRequestException('فرمت تاریخ ثبت نامعتبر است');
|
||||||
|
}
|
||||||
|
const y = Number(m[1]);
|
||||||
|
const mo = Number(m[2]);
|
||||||
|
const d = Number(m[3]);
|
||||||
|
if (mo < 1 || mo > 12 || d < 1 || d > 31) {
|
||||||
|
throw new BadRequestException('فرمت تاریخ ثبت نامعتبر است');
|
||||||
|
}
|
||||||
|
const gte = new Date(
|
||||||
|
`${m[1]}-${m[2]}-${m[3]}T00:00:00+03:30`,
|
||||||
|
);
|
||||||
|
const lt = new Date(gte.getTime() + 24 * 60 * 60 * 1000);
|
||||||
|
if (Number.isNaN(gte.getTime()) || Number.isNaN(lt.getTime())) {
|
||||||
|
throw new BadRequestException('فرمت تاریخ ثبت نامعتبر است');
|
||||||
|
}
|
||||||
|
return { gte, lt };
|
||||||
|
}
|
||||||
@@ -26,8 +26,11 @@ export class CatalogRequestsController {
|
|||||||
@AuthType('admin')
|
@AuthType('admin')
|
||||||
@Roles('admin', 'sales', 'warehouse')
|
@Roles('admin', 'sales', 'warehouse')
|
||||||
@Get('admin/catalog-requests')
|
@Get('admin/catalog-requests')
|
||||||
adminList(@Query('status') status?: CatalogRequestStatus) {
|
adminList(
|
||||||
return this.service.listAdmin(status);
|
@Query('status') status?: CatalogRequestStatus,
|
||||||
|
@Query('registeredOn') registeredOn?: string,
|
||||||
|
) {
|
||||||
|
return this.service.listAdmin(status, registeredOn);
|
||||||
}
|
}
|
||||||
|
|
||||||
@AuthType('admin')
|
@AuthType('admin')
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
UpdateCatalogRequestStatusDto,
|
UpdateCatalogRequestStatusDto,
|
||||||
UpdateCatalogItemHolderDto,
|
UpdateCatalogItemHolderDto,
|
||||||
} from './dto/catalog-request.dto';
|
} from './dto/catalog-request.dto';
|
||||||
|
import { dayRangeForRegisteredOn } from './catalog-request-date.util';
|
||||||
|
|
||||||
const STATUS_FLOW: CatalogRequestStatus[] = [
|
const STATUS_FLOW: CatalogRequestStatus[] = [
|
||||||
'new',
|
'new',
|
||||||
@@ -27,9 +28,18 @@ const STATUS_FLOW: CatalogRequestStatus[] = [
|
|||||||
export class CatalogRequestsService {
|
export class CatalogRequestsService {
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
async listAdmin(status?: CatalogRequestStatus) {
|
async listAdmin(status?: CatalogRequestStatus, registeredOn?: string) {
|
||||||
|
const createdAt =
|
||||||
|
registeredOn?.trim()
|
||||||
|
? dayRangeForRegisteredOn(registeredOn.trim())
|
||||||
|
: undefined;
|
||||||
const rows = await this.prisma.catalogRequest.findMany({
|
const rows = await this.prisma.catalogRequest.findMany({
|
||||||
where: status ? { status } : undefined,
|
where: {
|
||||||
|
...(status ? { status } : {}),
|
||||||
|
...(createdAt
|
||||||
|
? { createdAt: { gte: createdAt.gte, lt: createdAt.lt } }
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
include: {
|
include: {
|
||||||
customer: {
|
customer: {
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import {
|
||||||
|
filterSelectOptionsBySearch,
|
||||||
|
optionMatchesSelectSearch,
|
||||||
|
} from './select-option-search.util';
|
||||||
|
|
||||||
|
describe('select-option-search', () => {
|
||||||
|
const options = [
|
||||||
|
{ value: '1', label: 'پارچه مخمل' },
|
||||||
|
{ value: '2', label: 'كتان سفيد' },
|
||||||
|
{ value: '3', label: 'ژاکارد' },
|
||||||
|
];
|
||||||
|
|
||||||
|
it('matches Persian labels case-insensitively', () => {
|
||||||
|
expect(optionMatchesSelectSearch('پارچه مخمل', 'مخمل')).toBe(true);
|
||||||
|
expect(optionMatchesSelectSearch('پارچه مخمل', 'MVM')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizes Arabic yeh/kaf and Persian digits', () => {
|
||||||
|
expect(optionMatchesSelectSearch('کتان سفید', 'سفيد')).toBe(true);
|
||||||
|
expect(optionMatchesSelectSearch('کتان سفید', 'كتان')).toBe(true);
|
||||||
|
expect(optionMatchesSelectSearch('کد ۱۲۳', '123')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters option list', () => {
|
||||||
|
expect(filterSelectOptionsBySearch(options, 'كت').map((o) => o.value)).toEqual([
|
||||||
|
'2',
|
||||||
|
]);
|
||||||
|
expect(filterSelectOptionsBySearch(options, '')).toHaveLength(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/** Shared with admin/user select components — keep in sync with selectOptionSearch.ts */
|
||||||
|
export function normalizeForSelectSearch(text: string): string {
|
||||||
|
const persianDigits = '۰۱۲۳۴۵۶۷۸۹'
|
||||||
|
const normalized = text.replace(/[۰-۹]/g, (digit) =>
|
||||||
|
String(persianDigits.indexOf(digit)),
|
||||||
|
)
|
||||||
|
return normalized
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/ي/g, 'ی')
|
||||||
|
.replace(/ك/g, 'ک')
|
||||||
|
.normalize('NFKC')
|
||||||
|
.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function optionMatchesSelectSearch(label: string, query: string): boolean {
|
||||||
|
const q = normalizeForSelectSearch(query)
|
||||||
|
if (!q) return true
|
||||||
|
return normalizeForSelectSearch(label).includes(q)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterSelectOptionsBySearch<T extends { label: string }>(
|
||||||
|
options: T[],
|
||||||
|
query: string,
|
||||||
|
): T[] {
|
||||||
|
const trimmed = query.trim()
|
||||||
|
if (!trimmed) return options
|
||||||
|
return options.filter((option) => optionMatchesSelectSearch(option.label, trimmed))
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ export const ORDER_TRANSITIONS: Record<OrderStatus, OrderStatus[]> = {
|
|||||||
],
|
],
|
||||||
awaiting_cash_confirmation: [
|
awaiting_cash_confirmation: [
|
||||||
OrderStatus.cash_confirmed,
|
OrderStatus.cash_confirmed,
|
||||||
|
OrderStatus.check_in_progress,
|
||||||
OrderStatus.awaiting_cash_payment,
|
OrderStatus.awaiting_cash_payment,
|
||||||
],
|
],
|
||||||
cash_confirmed: [OrderStatus.final_approved],
|
cash_confirmed: [OrderStatus.final_approved],
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { SettlementType } from '@prisma/client';
|
||||||
|
|
||||||
|
/** تسویههایی که ابتدا (یا تنها) مرحله واریز نقدی / رسید دارند. */
|
||||||
|
export function usesCashReceiptPhase(
|
||||||
|
type: SettlementType | null | undefined,
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
type === SettlementType.cash || type === SettlementType.cash_check
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** تسویههایی که ثبت و بررسی چک دارند (خالص چکی یا نقدی-چکی پس از تأیید نقدی). */
|
||||||
|
export function usesCheckSettlement(
|
||||||
|
type: SettlementType | null | undefined,
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
type === SettlementType.check || type === SettlementType.cash_check
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -35,6 +35,12 @@ export class CompaniesController {
|
|||||||
return this.companiesService.findAll(true);
|
return this.companiesService.findAll(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@Get('payment/bank-accounts')
|
||||||
|
listPaymentBankAccounts() {
|
||||||
|
return this.companiesService.listPaymentBankAccounts();
|
||||||
|
}
|
||||||
|
|
||||||
@AuthType('any')
|
@AuthType('any')
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
findOne(@Param('id') id: string) {
|
findOne(@Param('id') id: string) {
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
|
BadRequestException,
|
||||||
Injectable,
|
Injectable,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
ConflictException,
|
ConflictException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
isPaymentRecipientCompany,
|
||||||
|
requirePaymentRecipientCompany,
|
||||||
|
} from './default-company';
|
||||||
import { Prisma } from '@prisma/client';
|
import { Prisma } from '@prisma/client';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { resolveMaxMeterage } from '../common/utils/meterage';
|
import { resolveMaxMeterage } from '../common/utils/meterage';
|
||||||
@@ -146,8 +151,21 @@ export class CompaniesService {
|
|||||||
return this.mapCompany(company);
|
return this.mapCompany(company);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** حسابهای واریز پرداخت مشتری — همیشه نادمی تکس */
|
||||||
|
async listPaymentBankAccounts() {
|
||||||
|
const recipient = await requirePaymentRecipientCompany(this.prisma);
|
||||||
|
return this.listBankAccountsForCompany(recipient.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* لیست حساب برای اپ/ادمین: پرداخت همیشه به نادمی تکس است، نه شرکت مالک محصول.
|
||||||
|
*/
|
||||||
async listBankAccounts(companyId: string) {
|
async listBankAccounts(companyId: string) {
|
||||||
await this.ensureCompany(companyId);
|
await this.ensureCompany(companyId);
|
||||||
|
return this.listPaymentBankAccounts();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async listBankAccountsForCompany(companyId: string) {
|
||||||
const accounts = await this.prisma.bankAccount.findMany({
|
const accounts = await this.prisma.bankAccount.findMany({
|
||||||
where: { companyId },
|
where: { companyId },
|
||||||
orderBy: { displayOrder: 'asc' },
|
orderBy: { displayOrder: 'asc' },
|
||||||
@@ -156,10 +174,16 @@ export class CompaniesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createBankAccount(companyId: string, dto: CreateBankAccountDto) {
|
async createBankAccount(companyId: string, dto: CreateBankAccountDto) {
|
||||||
await this.ensureCompany(companyId);
|
const company = await this.ensureCompany(companyId);
|
||||||
|
if (!isPaymentRecipientCompany(company)) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'حساب بانکی فقط برای شرکت نادمی تکس (دریافتکننده پرداخت) ثبت میشود.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const recipient = await requirePaymentRecipientCompany(this.prisma);
|
||||||
const account = await this.prisma.bankAccount.create({
|
const account = await this.prisma.bankAccount.create({
|
||||||
data: {
|
data: {
|
||||||
companyId,
|
companyId: recipient.id,
|
||||||
bankName: dto.bankName.trim(),
|
bankName: dto.bankName.trim(),
|
||||||
accountHolder: dto.accountHolder.trim(),
|
accountHolder: dto.accountHolder.trim(),
|
||||||
accountNumber: dto.accountNumber.trim(),
|
accountNumber: dto.accountNumber.trim(),
|
||||||
@@ -234,6 +258,7 @@ export class CompaniesService {
|
|||||||
id: company.id,
|
id: company.id,
|
||||||
name: company.name,
|
name: company.name,
|
||||||
slug: company.slug,
|
slug: company.slug,
|
||||||
|
isPaymentRecipient: isPaymentRecipientCompany(company),
|
||||||
description: company.description ?? '',
|
description: company.description ?? '',
|
||||||
logoUrl: company.logoUrl,
|
logoUrl: company.logoUrl,
|
||||||
logo: company.logoUrl ?? undefined,
|
logo: company.logoUrl ?? undefined,
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import {
|
||||||
|
PAYMENT_RECIPIENT_COMPANY_SLUG,
|
||||||
|
isPaymentRecipientCompany,
|
||||||
|
} from './default-company';
|
||||||
|
|
||||||
|
describe('default-company', () => {
|
||||||
|
it('uses stable slug for payment recipient', () => {
|
||||||
|
expect(PAYMENT_RECIPIENT_COMPANY_SLUG).toBe('nademitex');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('detects payment recipient by slug', () => {
|
||||||
|
expect(isPaymentRecipientCompany({ slug: 'nademitex' })).toBe(true);
|
||||||
|
expect(isPaymentRecipientCompany({ slug: 'par' })).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { BadRequestException, NotFoundException } from '@nestjs/common';
|
||||||
|
import type { Company, PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
/** Slug ثابت شرکت اصلی — منبع حقیقت برای دریافت پرداخت و پیشفرض محصول */
|
||||||
|
export const PAYMENT_RECIPIENT_COMPANY_SLUG = 'nademitex';
|
||||||
|
|
||||||
|
type PrismaLike = Pick<PrismaClient, 'company'>;
|
||||||
|
|
||||||
|
export async function findPaymentRecipientCompany(
|
||||||
|
prisma: PrismaLike,
|
||||||
|
): Promise<Company | null> {
|
||||||
|
return prisma.company.findFirst({
|
||||||
|
where: { slug: PAYMENT_RECIPIENT_COMPANY_SLUG, isActive: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function requirePaymentRecipientCompany(
|
||||||
|
prisma: PrismaLike,
|
||||||
|
): Promise<Company> {
|
||||||
|
const company = await findPaymentRecipientCompany(prisma);
|
||||||
|
if (!company) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
'شرکت اصلی (نادمی تکس) در سیستم تعریف نشده است. لطفاً با پشتیبانی تماس بگیرید.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return company;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** شرکت مرتبط با محصول؛ در صورت نبود مقدار معتبر، نادمی تکس */
|
||||||
|
export async function resolveProductCompanyId(
|
||||||
|
prisma: PrismaLike,
|
||||||
|
companyId?: string | null,
|
||||||
|
): Promise<string> {
|
||||||
|
const trimmed = companyId?.trim();
|
||||||
|
if (trimmed) {
|
||||||
|
const row = await prisma.company.findUnique({ where: { id: trimmed } });
|
||||||
|
if (!row) {
|
||||||
|
throw new BadRequestException('شرکت انتخابشده معتبر نیست');
|
||||||
|
}
|
||||||
|
if (!row.isActive) {
|
||||||
|
throw new BadRequestException('شرکت انتخابشده غیرفعال است');
|
||||||
|
}
|
||||||
|
return row.id;
|
||||||
|
}
|
||||||
|
return (await requirePaymentRecipientCompany(prisma)).id;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPaymentRecipientCompany(company: {
|
||||||
|
slug: string;
|
||||||
|
}): boolean {
|
||||||
|
return company.slug === PAYMENT_RECIPIENT_COMPANY_SLUG;
|
||||||
|
}
|
||||||
@@ -23,6 +23,10 @@ import {
|
|||||||
} from './dto/order.dto';
|
} from './dto/order.dto';
|
||||||
import { generateOrderNumber } from '../common/utils/order-number';
|
import { generateOrderNumber } from '../common/utils/order-number';
|
||||||
import { calculateCheckRass } from '../common/utils/check-rass';
|
import { calculateCheckRass } from '../common/utils/check-rass';
|
||||||
|
import {
|
||||||
|
usesCashReceiptPhase,
|
||||||
|
usesCheckSettlement,
|
||||||
|
} from '../common/utils/settlement-type';
|
||||||
import { earliestCashInvoiceExpiry } from '../common/utils/cash-expiry';
|
import { earliestCashInvoiceExpiry } from '../common/utils/cash-expiry';
|
||||||
import {
|
import {
|
||||||
resolveAdminStatusFilter,
|
resolveAdminStatusFilter,
|
||||||
@@ -217,25 +221,34 @@ export class OrdersService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isCash = dto.settlementType === SettlementType.cash;
|
const cashFirst = usesCashReceiptPhase(dto.settlementType);
|
||||||
const nextStatus = isCash
|
const checkOnly =
|
||||||
|
dto.settlementType === SettlementType.check;
|
||||||
|
const nextStatus = cashFirst
|
||||||
? OrderStatus.awaiting_cash_payment
|
? OrderStatus.awaiting_cash_payment
|
||||||
: OrderStatus.check_in_progress;
|
: OrderStatus.check_in_progress;
|
||||||
assertTransition(order.status, nextStatus);
|
assertTransition(order.status, nextStatus);
|
||||||
|
|
||||||
const cashCompanies = isCash
|
const cashCompanies = cashFirst
|
||||||
? await this.companiesForOrder(order)
|
? await this.companiesForOrder(order)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
const settlementNote =
|
||||||
|
dto.settlementType === SettlementType.cash
|
||||||
|
? 'مشتری تسویه نقدی را انتخاب کرد'
|
||||||
|
: dto.settlementType === SettlementType.cash_check
|
||||||
|
? 'مشتری تسویه نقدی-چکی را انتخاب کرد'
|
||||||
|
: 'مشتری تسویه چکی را انتخاب کرد';
|
||||||
|
|
||||||
await this.prisma.$transaction(async (tx) => {
|
await this.prisma.$transaction(async (tx) => {
|
||||||
await tx.order.update({
|
await tx.order.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: {
|
data: {
|
||||||
settlementType: dto.settlementType,
|
settlementType: dto.settlementType,
|
||||||
checkMode: isCash
|
checkMode: checkOnly
|
||||||
? null
|
? (dto.checkMode ?? CheckRegistrationMode.full)
|
||||||
: (dto.checkMode ?? CheckRegistrationMode.full),
|
: null,
|
||||||
cashInvoiceExpiresAt: isCash
|
cashInvoiceExpiresAt: cashFirst
|
||||||
? earliestCashInvoiceExpiry(cashCompanies)
|
? earliestCashInvoiceExpiry(cashCompanies)
|
||||||
: null,
|
: null,
|
||||||
cashReminderSentAt: null,
|
cashReminderSentAt: null,
|
||||||
@@ -248,9 +261,7 @@ export class OrdersService {
|
|||||||
fromStatus: order.status,
|
fromStatus: order.status,
|
||||||
toStatus: nextStatus,
|
toStatus: nextStatus,
|
||||||
changedBy: 'customer',
|
changedBy: 'customer',
|
||||||
note: isCash
|
note: settlementNote,
|
||||||
? 'مشتری تسویه نقدی را انتخاب کرد'
|
|
||||||
: 'مشتری تسویه چکی را انتخاب کرد',
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -575,7 +586,7 @@ export class OrdersService {
|
|||||||
if (order.userId !== userId) {
|
if (order.userId !== userId) {
|
||||||
throw new ForbiddenException('دسترسی به این سفارش مجاز نیست');
|
throw new ForbiddenException('دسترسی به این سفارش مجاز نیست');
|
||||||
}
|
}
|
||||||
if (order.settlementType !== SettlementType.check) {
|
if (!usesCheckSettlement(order.settlementType)) {
|
||||||
throw new BadRequestException('این سفارش تسویه چکی نیست');
|
throw new BadRequestException('این سفارش تسویه چکی نیست');
|
||||||
}
|
}
|
||||||
if (!CHECK_EDITABLE_STATUSES.includes(order.status)) {
|
if (!CHECK_EDITABLE_STATUSES.includes(order.status)) {
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
|
import { ValidationPipe } from '@nestjs/common';
|
||||||
|
import { plainToInstance } from 'class-transformer';
|
||||||
|
import { validate } from 'class-validator';
|
||||||
import {
|
import {
|
||||||
colorVariantImageLists,
|
colorVariantImageLists,
|
||||||
parseColorEntries,
|
parseColorEntries,
|
||||||
parseGeneralImageUrls,
|
parseGeneralImageUrls,
|
||||||
} from './color-entries';
|
} from './color-entries';
|
||||||
import { serializeColorsJsonForDb } from './dto/product-color-json.dto';
|
import {
|
||||||
|
ProductColorJsonItemDto,
|
||||||
|
serializeColorsJsonForDb,
|
||||||
|
} from './dto/product-color-json.dto';
|
||||||
|
import { UpdateProductDto } from './dto/product.dto';
|
||||||
|
|
||||||
describe('product image separation', () => {
|
describe('product image separation', () => {
|
||||||
it('keeps general images separate from color variant urls', () => {
|
it('keeps general images separate from color variant urls', () => {
|
||||||
@@ -66,6 +73,50 @@ describe('product image separation', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('accepts colorsJson through Nest ValidationPipe (same as PATCH /products)', async () => {
|
||||||
|
const pipe = new ValidationPipe({
|
||||||
|
whitelist: true,
|
||||||
|
transform: true,
|
||||||
|
forbidNonWhitelisted: true,
|
||||||
|
transformOptions: { enableImplicitConversion: true },
|
||||||
|
});
|
||||||
|
const body = {
|
||||||
|
name: 'تست',
|
||||||
|
color: 'قرمز',
|
||||||
|
colorsJson: [
|
||||||
|
{
|
||||||
|
name: 'قرمز شرابی',
|
||||||
|
imageUrl: 'data:image/jpeg;base64,abc',
|
||||||
|
imageUrls: ['data:image/jpeg;base64,abc'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const dto = await pipe.transform(body, {
|
||||||
|
type: 'body',
|
||||||
|
metatype: UpdateProductDto,
|
||||||
|
});
|
||||||
|
expect(dto.colorsJson?.[0]).toBeInstanceOf(ProductColorJsonItemDto);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts colorsJson name/imageUrl/imageUrls under whitelist validation', async () => {
|
||||||
|
const dto = plainToInstance(UpdateProductDto, {
|
||||||
|
name: 'تست',
|
||||||
|
colorsJson: [
|
||||||
|
{
|
||||||
|
name: 'قرمز شرابی',
|
||||||
|
imageUrl: 'data:image/jpeg;base64,abc',
|
||||||
|
imageUrls: ['data:image/jpeg;base64,abc'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const errors = await validate(dto, {
|
||||||
|
whitelist: true,
|
||||||
|
forbidNonWhitelisted: true,
|
||||||
|
});
|
||||||
|
expect(errors).toEqual([]);
|
||||||
|
expect(dto.colorsJson?.[0]).toBeInstanceOf(ProductColorJsonItemDto);
|
||||||
|
});
|
||||||
|
|
||||||
it('does not merge color images into general list when only imageUrlsJson is set', () => {
|
it('does not merge color images into general list when only imageUrlsJson is set', () => {
|
||||||
const entries = parseColorEntries([{ name: 'Red', imageUrl: 'https://cdn/r.jpg' }]);
|
const entries = parseColorEntries([{ name: 'Red', imageUrl: 'https://cdn/r.jpg' }]);
|
||||||
expect(parseGeneralImageUrls(['https://cdn/g.jpg'], entries)).toEqual([
|
expect(parseGeneralImageUrls(['https://cdn/g.jpg'], entries)).toEqual([
|
||||||
|
|||||||
@@ -1,4 +1,54 @@
|
|||||||
import { IsArray, IsOptional, IsString, MinLength } from 'class-validator';
|
import {
|
||||||
|
IsArray,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
MinLength,
|
||||||
|
ValidatorConstraint,
|
||||||
|
ValidatorConstraintInterface,
|
||||||
|
registerDecorator,
|
||||||
|
type ValidationOptions,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
|
@ValidatorConstraint({ name: 'isColorsJson', async: false })
|
||||||
|
export class IsColorsJsonConstraint implements ValidatorConstraintInterface {
|
||||||
|
validate(value: unknown): boolean {
|
||||||
|
if (value === undefined || value === null) return true;
|
||||||
|
if (!Array.isArray(value)) return false;
|
||||||
|
for (const item of value) {
|
||||||
|
if (!(item instanceof ProductColorJsonItemDto)) return false;
|
||||||
|
if (!item.name?.trim()) return false;
|
||||||
|
if (
|
||||||
|
item.imageUrl !== undefined &&
|
||||||
|
item.imageUrl !== null &&
|
||||||
|
typeof item.imageUrl !== 'string'
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (item.imageUrls !== undefined && item.imageUrls !== null) {
|
||||||
|
if (!Array.isArray(item.imageUrls)) return false;
|
||||||
|
if (!item.imageUrls.every((u) => typeof u === 'string')) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultMessage(): string {
|
||||||
|
return 'فرمت colorsJson نامعتبر است';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** اعتبارسنجی colorsJson بعد از transform — بدون ValidateNested (سازگار با whitelist) */
|
||||||
|
export function IsColorsJson(validationOptions?: ValidationOptions) {
|
||||||
|
return function register(object: object, propertyName: string) {
|
||||||
|
registerDecorator({
|
||||||
|
target: object.constructor,
|
||||||
|
propertyName,
|
||||||
|
options: validationOptions,
|
||||||
|
constraints: [],
|
||||||
|
validator: IsColorsJsonConstraint,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export class ProductColorJsonItemDto {
|
export class ProductColorJsonItemDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
@@ -38,7 +88,7 @@ export function transformColorsJson(value: unknown): ProductColorJsonItemDto[] |
|
|||||||
for (const item of value) {
|
for (const item of value) {
|
||||||
if (typeof item === 'string') {
|
if (typeof item === 'string') {
|
||||||
const name = item.trim();
|
const name = item.trim();
|
||||||
if (name) out.push({ name });
|
if (name) out.push(Object.assign(new ProductColorJsonItemDto(), { name }));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (item && typeof item === 'object') {
|
if (item && typeof item === 'object') {
|
||||||
@@ -54,11 +104,13 @@ export function transformColorsJson(value: unknown): ProductColorJsonItemDto[] |
|
|||||||
typeof raw.imageUrl === 'string' && raw.imageUrl.trim()
|
typeof raw.imageUrl === 'string' && raw.imageUrl.trim()
|
||||||
? raw.imageUrl.trim()
|
? raw.imageUrl.trim()
|
||||||
: undefined;
|
: undefined;
|
||||||
out.push({
|
out.push(
|
||||||
name,
|
Object.assign(new ProductColorJsonItemDto(), {
|
||||||
imageUrl,
|
name,
|
||||||
imageUrls: imageUrls?.length ? imageUrls : imageUrl ? [imageUrl] : undefined,
|
imageUrl,
|
||||||
});
|
imageUrls: imageUrls?.length ? imageUrls : imageUrl ? [imageUrl] : undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
|
|||||||
@@ -7,21 +7,19 @@ import {
|
|||||||
IsString,
|
IsString,
|
||||||
Min,
|
Min,
|
||||||
MinLength,
|
MinLength,
|
||||||
ValidateNested,
|
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
import { Transform, Type } from 'class-transformer';
|
import { Transform, Type } from 'class-transformer';
|
||||||
import {
|
import {
|
||||||
|
IsColorsJson,
|
||||||
ProductColorJsonItemDto,
|
ProductColorJsonItemDto,
|
||||||
transformColorsJson,
|
transformColorsJson,
|
||||||
} from './product-color-json.dto';
|
} from './product-color-json.dto';
|
||||||
|
|
||||||
export class CreateProductDto {
|
export class CreateProductDto {
|
||||||
@IsString()
|
/** خالی = شرکت پیشفرض (نادمی تکس) */
|
||||||
companyId!: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
brandId?: string | null;
|
companyId?: string;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@MinLength(1)
|
@MinLength(1)
|
||||||
@@ -44,6 +42,11 @@ export class CreateProductDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
|
/** مشخصات آزاد — نمایش در specs-extra اپ */
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
specs?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Type(() => Number)
|
@Type(() => Number)
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@@ -139,8 +142,7 @@ export class CreateProductDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Transform(({ value }) => transformColorsJson(value))
|
@Transform(({ value }) => transformColorsJson(value))
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@ValidateNested({ each: true })
|
@IsColorsJson()
|
||||||
@Type(() => ProductColorJsonItemDto)
|
|
||||||
colorsJson?: ProductColorJsonItemDto[];
|
colorsJson?: ProductColorJsonItemDto[];
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@@ -149,18 +151,23 @@ export class CreateProductDto {
|
|||||||
imageUrlsJson?: string[];
|
imageUrlsJson?: string[];
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
tagsJson?: string[];
|
tagsJson?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** همهٔ فیلدهای Create بهجز companyId — شامل brandId برای PATCH ادمین */
|
/** همهٔ فیلدهای Create بهجز companyId — companyId در PATCH برای تغییر شرکت محصول */
|
||||||
export class UpdateProductDto extends PartialType(
|
export class UpdateProductDto extends PartialType(
|
||||||
OmitType(CreateProductDto, ['companyId'] as const),
|
OmitType(CreateProductDto, ['companyId'] as const),
|
||||||
) {
|
) {
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
companyId?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Transform(({ value }) => transformColorsJson(value))
|
@Transform(({ value }) => transformColorsJson(value))
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@ValidateNested({ each: true })
|
@IsColorsJson()
|
||||||
@Type(() => ProductColorJsonItemDto)
|
|
||||||
colorsJson?: ProductColorJsonItemDto[];
|
colorsJson?: ProductColorJsonItemDto[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { isProductUnavailableForApp } from './product-availability';
|
||||||
|
|
||||||
|
describe('isProductUnavailableForApp', () => {
|
||||||
|
it('treats admin-inactive products as unavailable', () => {
|
||||||
|
expect(isProductUnavailableForApp({ isActive: false, stock: 10 })).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps active products available when stock is unset or positive', () => {
|
||||||
|
expect(isProductUnavailableForApp({ isActive: true })).toBe(false);
|
||||||
|
expect(isProductUnavailableForApp({ isActive: true, stock: 4 })).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('treats zero stock as unavailable', () => {
|
||||||
|
expect(isProductUnavailableForApp({ isActive: true, stock: 0 })).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/** App lists keep inactive products; they are shown as unavailable via `isActive`. */
|
||||||
|
export function isProductUnavailableForApp(product: {
|
||||||
|
isActive: boolean;
|
||||||
|
stock?: number | null;
|
||||||
|
}): boolean {
|
||||||
|
if (!product.isActive) return true;
|
||||||
|
return product.stock != null && product.stock <= 0;
|
||||||
|
}
|
||||||
@@ -109,4 +109,10 @@ export class ProductsController {
|
|||||||
setActive(@Param('id') id: string, @Body() dto: SetProductActiveDto) {
|
setActive(@Param('id') id: string, @Body() dto: SetProductActiveDto) {
|
||||||
return this.productsService.setActive(id, dto.isActive);
|
return this.productsService.setActive(id, dto.isActive);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AuthType('admin')
|
||||||
|
@Delete(':id')
|
||||||
|
remove(@Param('id') id: string) {
|
||||||
|
return this.productsService.remove(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
BadRequestException,
|
|
||||||
ConflictException,
|
ConflictException,
|
||||||
Injectable,
|
Injectable,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
@@ -9,6 +8,7 @@ import { PrismaService } from '../prisma/prisma.service';
|
|||||||
import { CreateProductDto, UpdateProductDto } from './dto/product.dto';
|
import { CreateProductDto, UpdateProductDto } from './dto/product.dto';
|
||||||
import { serializeColorsJsonForDb } from './dto/product-color-json.dto';
|
import { serializeColorsJsonForDb } from './dto/product-color-json.dto';
|
||||||
import { loadCatalogCategories } from '../catalog/category-defs';
|
import { loadCatalogCategories } from '../catalog/category-defs';
|
||||||
|
import { resolveProductCompanyId } from '../companies/default-company';
|
||||||
import {
|
import {
|
||||||
colorVariantImageLists,
|
colorVariantImageLists,
|
||||||
parseColorEntries,
|
parseColorEntries,
|
||||||
@@ -17,7 +17,6 @@ import {
|
|||||||
|
|
||||||
const productInclude = {
|
const productInclude = {
|
||||||
company: { select: { name: true } },
|
company: { select: { name: true } },
|
||||||
brand: { select: { id: true, name: true } },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -118,17 +117,20 @@ export class ProductsService {
|
|||||||
async create(dto: CreateProductDto, preservedColorsJson?: unknown) {
|
async create(dto: CreateProductDto, preservedColorsJson?: unknown) {
|
||||||
const code = (dto.code || dto.productCode || '').trim();
|
const code = (dto.code || dto.productCode || '').trim();
|
||||||
if (!code) throw new ConflictException('کد محصول الزامی است');
|
if (!code) throw new ConflictException('کد محصول الزامی است');
|
||||||
const brandId = await this.resolveBrandId(dto.brandId ?? null);
|
const companyId = await resolveProductCompanyId(
|
||||||
|
this.prisma,
|
||||||
|
dto.companyId,
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const product = await this.prisma.product.create({
|
const product = await this.prisma.product.create({
|
||||||
data: {
|
data: {
|
||||||
companyId: dto.companyId,
|
companyId,
|
||||||
brandId,
|
|
||||||
name: dto.name.trim(),
|
name: dto.name.trim(),
|
||||||
code,
|
code,
|
||||||
color: dto.color.trim(),
|
color: dto.color.trim(),
|
||||||
description: dto.description?.trim() || null,
|
description: dto.description?.trim() || null,
|
||||||
|
specs: dto.specs?.trim() || null,
|
||||||
minMeterage: dto.minMeterage ?? 30,
|
minMeterage: dto.minMeterage ?? 30,
|
||||||
orderMultiplier: dto.orderMultiplier ?? dto.orderStep ?? 10,
|
orderMultiplier: dto.orderMultiplier ?? dto.orderStep ?? 10,
|
||||||
basePrice: this.pickNumber(dto.basePrice, dto.price),
|
basePrice: this.pickNumber(dto.basePrice, dto.price),
|
||||||
@@ -177,13 +179,18 @@ export class ProductsService {
|
|||||||
: dto.productCode !== undefined
|
: dto.productCode !== undefined
|
||||||
? dto.productCode.trim()
|
? dto.productCode.trim()
|
||||||
: undefined;
|
: undefined;
|
||||||
const brandId = await this.resolveBrandId(dto.brandId);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const product = await this.prisma.product.update({
|
const product = await this.prisma.product.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: {
|
data: {
|
||||||
...(brandId !== undefined ? { brandId } : {}),
|
...(dto.companyId !== undefined
|
||||||
|
? {
|
||||||
|
companyId: await resolveProductCompanyId(
|
||||||
|
this.prisma,
|
||||||
|
dto.companyId,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
name: dto.name?.trim(),
|
name: dto.name?.trim(),
|
||||||
code,
|
code,
|
||||||
color: dto.color?.trim(),
|
color: dto.color?.trim(),
|
||||||
@@ -191,6 +198,8 @@ export class ProductsService {
|
|||||||
dto.description === undefined
|
dto.description === undefined
|
||||||
? undefined
|
? undefined
|
||||||
: dto.description?.trim() || null,
|
: dto.description?.trim() || null,
|
||||||
|
specs:
|
||||||
|
dto.specs === undefined ? undefined : dto.specs?.trim() || null,
|
||||||
minMeterage: dto.minMeterage,
|
minMeterage: dto.minMeterage,
|
||||||
orderMultiplier:
|
orderMultiplier:
|
||||||
dto.orderMultiplier !== undefined
|
dto.orderMultiplier !== undefined
|
||||||
@@ -268,6 +277,35 @@ export class ProductsService {
|
|||||||
return this.map(product);
|
return this.map(product);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async remove(id: string) {
|
||||||
|
await this.ensure(id);
|
||||||
|
const catalogRequestCount = await this.prisma.catalogRequestItem.count({
|
||||||
|
where: { productId: id },
|
||||||
|
});
|
||||||
|
if (catalogRequestCount > 0) {
|
||||||
|
throw new ConflictException(
|
||||||
|
'این محصول در درخواست کالیته ثبت شده و قابل حذف نیست.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await this.prisma.$transaction([
|
||||||
|
this.prisma.cartItem.deleteMany({ where: { productId: id } }),
|
||||||
|
this.prisma.product.delete({ where: { id } }),
|
||||||
|
]);
|
||||||
|
} catch (e) {
|
||||||
|
if (
|
||||||
|
e instanceof Prisma.PrismaClientKnownRequestError &&
|
||||||
|
e.code === 'P2003'
|
||||||
|
) {
|
||||||
|
throw new ConflictException(
|
||||||
|
'این محصول به دادههای دیگر وابسته است و قابل حذف نیست.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
async listFavoriteIds(customerId: string) {
|
async listFavoriteIds(customerId: string) {
|
||||||
const rows = await this.prisma.productFavorite.findMany({
|
const rows = await this.prisma.productFavorite.findMany({
|
||||||
where: { customerId },
|
where: { customerId },
|
||||||
@@ -316,17 +354,6 @@ export class ProductsService {
|
|||||||
return product;
|
return product;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async resolveBrandId(
|
|
||||||
brandId?: string | null,
|
|
||||||
): Promise<string | null | undefined> {
|
|
||||||
if (brandId === undefined) return undefined;
|
|
||||||
const id = brandId?.trim() || null;
|
|
||||||
if (!id) return null;
|
|
||||||
const brand = await this.prisma.brand.findUnique({ where: { id } });
|
|
||||||
if (!brand) throw new BadRequestException('برند یافت نشد');
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
private map(
|
private map(
|
||||||
product: {
|
product: {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -335,6 +362,7 @@ export class ProductsService {
|
|||||||
code: string;
|
code: string;
|
||||||
color: string;
|
color: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
|
specs?: string | null;
|
||||||
minMeterage: number;
|
minMeterage: number;
|
||||||
orderMultiplier: number;
|
orderMultiplier: number;
|
||||||
basePrice: number | null;
|
basePrice: number | null;
|
||||||
@@ -343,7 +371,6 @@ export class ProductsService {
|
|||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
company: { name: string };
|
company: { name: string };
|
||||||
brand?: { id: string; name: string } | null;
|
|
||||||
usageCategory?: string;
|
usageCategory?: string;
|
||||||
season?: string;
|
season?: string;
|
||||||
widthCm?: number | null;
|
widthCm?: number | null;
|
||||||
@@ -381,13 +408,12 @@ export class ProductsService {
|
|||||||
catalogCode: product.code,
|
catalogCode: product.code,
|
||||||
/** @deprecated use `name` */
|
/** @deprecated use `name` */
|
||||||
catalogName: product.name,
|
catalogName: product.name,
|
||||||
brandId: product.brand?.id ?? null,
|
|
||||||
brandName: product.brand?.name ?? null,
|
|
||||||
name: product.name,
|
name: product.name,
|
||||||
code: product.code,
|
code: product.code,
|
||||||
productCode: product.code,
|
productCode: product.code,
|
||||||
color: product.color,
|
color: product.color,
|
||||||
description: product.description ?? undefined,
|
description: product.description ?? undefined,
|
||||||
|
specs: product.specs?.trim() ? product.specs : undefined,
|
||||||
minMeterage: product.minMeterage,
|
minMeterage: product.minMeterage,
|
||||||
orderMultiplier: product.orderMultiplier,
|
orderMultiplier: product.orderMultiplier,
|
||||||
orderStep: product.orderMultiplier,
|
orderStep: product.orderMultiplier,
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ export class UpdateSettingsDto {
|
|||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
defaultSettlementCash?: boolean;
|
defaultSettlementCash?: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
defaultSettlementCashCheck?: boolean;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
smsOrderRegistered?: boolean;
|
smsOrderRegistered?: boolean;
|
||||||
@@ -70,6 +74,18 @@ export class UpdateSettingsDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
supportEmail?: string | null;
|
supportEmail?: string | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
supportWhatsapp?: string | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
supportTelegram?: string | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
supportInstagram?: string | null;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsObject()
|
@IsObject()
|
||||||
categoryImagesJson?: Record<string, string | null>;
|
categoryImagesJson?: Record<string, string | null>;
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ export class SettingsService {
|
|||||||
return {
|
return {
|
||||||
supportPhone: settings.supportPhone,
|
supportPhone: settings.supportPhone,
|
||||||
supportEmail: settings.supportEmail,
|
supportEmail: settings.supportEmail,
|
||||||
|
supportWhatsapp: settings.supportWhatsapp,
|
||||||
|
supportTelegram: settings.supportTelegram,
|
||||||
|
supportInstagram: settings.supportInstagram,
|
||||||
|
defaultSettlementCash: settings.defaultSettlementCash,
|
||||||
|
defaultSettlementCheck: settings.defaultSettlementCheck,
|
||||||
|
defaultSettlementCashCheck: settings.defaultSettlementCashCheck ?? false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,6 +49,7 @@ export class SettingsService {
|
|||||||
cashInvoiceValidityHours: dto.cashInvoiceValidityHours,
|
cashInvoiceValidityHours: dto.cashInvoiceValidityHours,
|
||||||
defaultSettlementCheck: dto.defaultSettlementCheck,
|
defaultSettlementCheck: dto.defaultSettlementCheck,
|
||||||
defaultSettlementCash: dto.defaultSettlementCash,
|
defaultSettlementCash: dto.defaultSettlementCash,
|
||||||
|
defaultSettlementCashCheck: dto.defaultSettlementCashCheck,
|
||||||
minOrderRollsPerProduct: dto.minOrderRollsPerProduct,
|
minOrderRollsPerProduct: dto.minOrderRollsPerProduct,
|
||||||
showCatalogPublicPrice: dto.showCatalogPublicPrice,
|
showCatalogPublicPrice: dto.showCatalogPublicPrice,
|
||||||
allowAgentPricing: dto.allowAgentPricing,
|
allowAgentPricing: dto.allowAgentPricing,
|
||||||
@@ -63,6 +70,18 @@ export class SettingsService {
|
|||||||
dto.supportEmail === undefined
|
dto.supportEmail === undefined
|
||||||
? undefined
|
? undefined
|
||||||
: dto.supportEmail?.trim() || null,
|
: dto.supportEmail?.trim() || null,
|
||||||
|
supportWhatsapp:
|
||||||
|
dto.supportWhatsapp === undefined
|
||||||
|
? undefined
|
||||||
|
: dto.supportWhatsapp?.trim() || null,
|
||||||
|
supportTelegram:
|
||||||
|
dto.supportTelegram === undefined
|
||||||
|
? undefined
|
||||||
|
: dto.supportTelegram?.trim() || null,
|
||||||
|
supportInstagram:
|
||||||
|
dto.supportInstagram === undefined
|
||||||
|
? undefined
|
||||||
|
: dto.supportInstagram?.trim() || null,
|
||||||
categoryDefsJson: nextDefs,
|
categoryDefsJson: nextDefs,
|
||||||
categoryImagesJson:
|
categoryImagesJson:
|
||||||
dto.categoryImagesJson === undefined
|
dto.categoryImagesJson === undefined
|
||||||
@@ -87,6 +106,7 @@ export class SettingsService {
|
|||||||
cashInvoiceValidityHours: number;
|
cashInvoiceValidityHours: number;
|
||||||
defaultSettlementCheck: boolean;
|
defaultSettlementCheck: boolean;
|
||||||
defaultSettlementCash: boolean;
|
defaultSettlementCash: boolean;
|
||||||
|
defaultSettlementCashCheck?: boolean;
|
||||||
minOrderRollsPerProduct?: number;
|
minOrderRollsPerProduct?: number;
|
||||||
showCatalogPublicPrice?: boolean;
|
showCatalogPublicPrice?: boolean;
|
||||||
allowAgentPricing?: boolean;
|
allowAgentPricing?: boolean;
|
||||||
@@ -98,6 +118,9 @@ export class SettingsService {
|
|||||||
smsShippingUpdate: boolean;
|
smsShippingUpdate: boolean;
|
||||||
supportPhone: string | null;
|
supportPhone: string | null;
|
||||||
supportEmail: string | null;
|
supportEmail: string | null;
|
||||||
|
supportWhatsapp?: string | null;
|
||||||
|
supportTelegram?: string | null;
|
||||||
|
supportInstagram?: string | null;
|
||||||
categoryImagesJson?: unknown;
|
categoryImagesJson?: unknown;
|
||||||
categoryDefsJson?: unknown;
|
categoryDefsJson?: unknown;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
@@ -109,6 +132,7 @@ export class SettingsService {
|
|||||||
cashInvoiceValidityHours: settings.cashInvoiceValidityHours,
|
cashInvoiceValidityHours: settings.cashInvoiceValidityHours,
|
||||||
defaultSettlementCheck: settings.defaultSettlementCheck,
|
defaultSettlementCheck: settings.defaultSettlementCheck,
|
||||||
defaultSettlementCash: settings.defaultSettlementCash,
|
defaultSettlementCash: settings.defaultSettlementCash,
|
||||||
|
defaultSettlementCashCheck: settings.defaultSettlementCashCheck ?? false,
|
||||||
minOrderRollsPerProduct: settings.minOrderRollsPerProduct ?? 1,
|
minOrderRollsPerProduct: settings.minOrderRollsPerProduct ?? 1,
|
||||||
showCatalogPublicPrice: settings.showCatalogPublicPrice ?? false,
|
showCatalogPublicPrice: settings.showCatalogPublicPrice ?? false,
|
||||||
allowAgentPricing: settings.allowAgentPricing ?? false,
|
allowAgentPricing: settings.allowAgentPricing ?? false,
|
||||||
@@ -120,6 +144,9 @@ export class SettingsService {
|
|||||||
smsShippingUpdate: settings.smsShippingUpdate,
|
smsShippingUpdate: settings.smsShippingUpdate,
|
||||||
supportPhone: settings.supportPhone,
|
supportPhone: settings.supportPhone,
|
||||||
supportEmail: settings.supportEmail,
|
supportEmail: settings.supportEmail,
|
||||||
|
supportWhatsapp: settings.supportWhatsapp ?? null,
|
||||||
|
supportTelegram: settings.supportTelegram ?? null,
|
||||||
|
supportInstagram: settings.supportInstagram ?? null,
|
||||||
categoryImagesJson: asImageMap(settings.categoryImagesJson),
|
categoryImagesJson: asImageMap(settings.categoryImagesJson),
|
||||||
categoryDefsJson: defs,
|
categoryDefsJson: defs,
|
||||||
updatedAt: settings.updatedAt.toISOString(),
|
updatedAt: settings.updatedAt.toISOString(),
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const PRODUCT_INCLUDE = {
|
|||||||
color: true,
|
color: true,
|
||||||
subtitle: true,
|
subtitle: true,
|
||||||
isNew: true,
|
isNew: true,
|
||||||
|
isActive: true,
|
||||||
colorsJson: true,
|
colorsJson: true,
|
||||||
imageUrlsJson: true,
|
imageUrlsJson: true,
|
||||||
widthCm: true,
|
widthCm: true,
|
||||||
@@ -142,6 +143,7 @@ export class SuggestionsService {
|
|||||||
color: string;
|
color: string;
|
||||||
subtitle: string | null;
|
subtitle: string | null;
|
||||||
isNew: boolean;
|
isNew: boolean;
|
||||||
|
isActive: boolean;
|
||||||
colorsJson: unknown;
|
colorsJson: unknown;
|
||||||
imageUrlsJson: unknown;
|
imageUrlsJson: unknown;
|
||||||
widthCm: number | null;
|
widthCm: number | null;
|
||||||
@@ -180,6 +182,7 @@ export class SuggestionsService {
|
|||||||
widthCm: r.product.widthCm ?? undefined,
|
widthCm: r.product.widthCm ?? undefined,
|
||||||
composition: r.product.composition?.trim() || undefined,
|
composition: r.product.composition?.trim() || undefined,
|
||||||
isNew: r.product.isNew,
|
isNew: r.product.isNew,
|
||||||
|
productIsActive: r.product.isActive,
|
||||||
note: r.note ?? undefined,
|
note: r.note ?? undefined,
|
||||||
isActive: r.isActive,
|
isActive: r.isActive,
|
||||||
createdAt: r.createdAt.toISOString(),
|
createdAt: r.createdAt.toISOString(),
|
||||||
|
|||||||
Reference in New Issue
Block a user