Add catalog product type settings

This commit is contained in:
Ruslan Bakiev
2026-04-09 16:03:32 +07:00
parent da31e21406
commit 2cd8d0b612
4 changed files with 171 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
-- CreateTable
CREATE TABLE "CatalogProductTypeSetting" (
"id" TEXT NOT NULL,
"productType" TEXT NOT NULL,
"showQuantityPerBox" BOOLEAN NOT NULL DEFAULT false,
"allowCustomLength" BOOLEAN NOT NULL DEFAULT false,
"customLengthMinM" INTEGER,
"customLengthMaxM" INTEGER,
"customLengthStepM" INTEGER,
"allowCustomSleeveBrand" BOOLEAN NOT NULL DEFAULT false,
"allowCustomLabel" BOOLEAN NOT NULL DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "CatalogProductTypeSetting_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "CatalogProductTypeSetting_productType_key" ON "CatalogProductTypeSetting"("productType");

View File

@@ -190,6 +190,20 @@ model Product {
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
model CatalogProductTypeSetting {
id String @id @default(cuid())
productType String @unique
showQuantityPerBox Boolean @default(false)
allowCustomLength Boolean @default(false)
customLengthMinM Int?
customLengthMaxM Int?
customLengthStepM Int?
allowCustomSleeveBrand Boolean @default(false)
allowCustomLabel Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Cart { model Cart {
id String @id @default(cuid()) id String @id @default(cuid())
userId String @unique userId String @unique

View File

@@ -352,6 +352,19 @@ function normalizeQuantityValue(value) {
return Math.floor(normalized); return Math.floor(normalized);
} }
function normalizeOptionalPositiveInt(value) {
if (value == null || value === '') {
return null;
}
const normalized = Number(value);
if (!Number.isInteger(normalized) || normalized <= 0) {
throw new Error('Catalog setting values must be positive integers.');
}
return normalized;
}
function isCounterpartyProfileComplete(profile) { function isCounterpartyProfileComplete(profile) {
if (!profile) { if (!profile) {
return false; return false;
@@ -418,6 +431,91 @@ function defaultCartParameters(product) {
}; };
} }
function presentCatalogProductTypeSetting(setting) {
return {
productType: setting.productType,
showQuantityPerBox: Boolean(setting.showQuantityPerBox),
allowCustomLength: Boolean(setting.allowCustomLength),
customLengthMinM: setting.customLengthMinM ?? null,
customLengthMaxM: setting.customLengthMaxM ?? null,
customLengthStepM: setting.customLengthStepM ?? null,
allowCustomSleeveBrand: Boolean(setting.allowCustomSleeveBrand),
allowCustomLabel: Boolean(setting.allowCustomLabel),
};
}
function normalizeCatalogProductTypeSettingInput(input) {
const productType = normalizeText(input.productType);
if (!productType) {
throw new Error('Product type is required.');
}
const allowCustomLength = Boolean(input.allowCustomLength);
const customLengthMinM = allowCustomLength ? normalizeOptionalPositiveInt(input.customLengthMinM) : null;
const customLengthMaxM = allowCustomLength ? normalizeOptionalPositiveInt(input.customLengthMaxM) : null;
const customLengthStepM = allowCustomLength ? normalizeOptionalPositiveInt(input.customLengthStepM) : null;
if (allowCustomLength) {
if (customLengthMinM == null || customLengthMaxM == null || customLengthStepM == null) {
throw new Error('Length customization requires min, max, and step values.');
}
if (customLengthMinM > customLengthMaxM) {
throw new Error('Length min must be less than or equal to max.');
}
}
return {
productType,
showQuantityPerBox: Boolean(input.showQuantityPerBox),
allowCustomLength,
customLengthMinM,
customLengthMaxM,
customLengthStepM,
allowCustomSleeveBrand: Boolean(input.allowCustomSleeveBrand),
allowCustomLabel: Boolean(input.allowCustomLabel),
};
}
async function listCatalogProductTypeSettings(prisma) {
const [products, persistedSettings] = await Promise.all([
prisma.product.findMany({
where: {
isActive: true,
NOT: {
productType: null,
},
},
select: { productType: true },
distinct: ['productType'],
orderBy: { productType: 'asc' },
}),
prisma.catalogProductTypeSetting.findMany({
orderBy: { productType: 'asc' },
}),
]);
const productTypes = new Set([
...products.map((item) => normalizeText(item.productType)).filter(Boolean),
...persistedSettings.map((item) => item.productType),
]);
const settingsByType = new Map(persistedSettings.map((item) => [item.productType, item]));
return [...productTypes]
.sort((a, b) => a.localeCompare(b, 'ru'))
.map((productType) => presentCatalogProductTypeSetting({
productType,
showQuantityPerBox: false,
allowCustomLength: false,
customLengthMinM: null,
customLengthMaxM: null,
customLengthStepM: null,
allowCustomSleeveBrand: false,
allowCustomLabel: false,
...settingsByType.get(productType),
}));
}
const cartInclude = { const cartInclude = {
items: { items: {
orderBy: [{ updatedAt: 'desc' }, { createdAt: 'desc' }], orderBy: [{ updatedAt: 'desc' }, { createdAt: 'desc' }],
@@ -864,6 +962,8 @@ export const resolvers = {
orderBy: { name: 'asc' }, orderBy: { name: 'asc' },
}), }),
catalogProductTypeSettings: async (_, __, context) => listCatalogProductTypeSettings(context.prisma),
order: async (_, { id }, context) => { order: async (_, { id }, context) => {
const user = requireUser(context); const user = requireUser(context);
const order = await context.prisma.order.findUnique({ const order = await context.prisma.order.findUnique({
@@ -1440,6 +1540,19 @@ export const resolvers = {
}); });
}, },
upsertCatalogProductTypeSetting: async (_, { input }, context) => {
requireManagerAccess(context);
const payload = normalizeCatalogProductTypeSettingInput(input);
const persisted = await context.prisma.catalogProductTypeSetting.upsert({
where: { productType: payload.productType },
update: payload,
create: payload,
});
return presentCatalogProductTypeSetting(persisted);
},
addProductToCart: async (_, { productId }, context) => { addProductToCart: async (_, { productId }, context) => {
const user = requireUser(context); const user = requireUser(context);
const normalizedProductId = normalizeText(productId); const normalizedProductId = normalizeText(productId);

View File

@@ -248,6 +248,17 @@ type Product {
availableInWarehouses: [ProductWarehouseBalance!]! availableInWarehouses: [ProductWarehouseBalance!]!
} }
type CatalogProductTypeSetting {
productType: String!
showQuantityPerBox: Boolean!
allowCustomLength: Boolean!
customLengthMinM: Int
customLengthMaxM: Int
customLengthStepM: Int
allowCustomSleeveBrand: Boolean!
allowCustomLabel: Boolean!
}
type CartItem { type CartItem {
id: ID! id: ID!
productId: ID! productId: ID!
@@ -411,6 +422,7 @@ type Query {
integrationSyncDashboard: IntegrationSyncDashboard! integrationSyncDashboard: IntegrationSyncDashboard!
managerNotificationHistory(userId: ID!, channel: MessengerType!, limit: Int = 50): [NotificationHistoryItem!]! managerNotificationHistory(userId: ID!, channel: MessengerType!, limit: Int = 50): [NotificationHistoryItem!]!
clientProducts: [Product!]! clientProducts: [Product!]!
catalogProductTypeSettings: [CatalogProductTypeSetting!]!
order(id: ID!): Order order(id: ID!): Order
myOrders: [Order!]! myOrders: [Order!]!
myCurrentOrders: [Order!]! myCurrentOrders: [Order!]!
@@ -491,6 +503,17 @@ input UpdateCartItemQuantityInput {
quantity: Float! quantity: Float!
} }
input UpsertCatalogProductTypeSettingInput {
productType: String!
showQuantityPerBox: Boolean!
allowCustomLength: Boolean!
customLengthMinM: Int
customLengthMaxM: Int
customLengthStepM: Int
allowCustomSleeveBrand: Boolean!
allowCustomLabel: Boolean!
}
input ReadyOrderItemInput { input ReadyOrderItemInput {
productId: ID! productId: ID!
quantity: Float! quantity: Float!
@@ -554,6 +577,7 @@ type Mutation {
connectMessenger(input: ConnectMessengerInput!): MessengerConnection! connectMessenger(input: ConnectMessengerInput!): MessengerConnection!
deleteMyMessengerConnection(connectionId: ID!): Boolean! deleteMyMessengerConnection(connectionId: ID!): Boolean!
upsertMyCounterpartyProfile(input: UpsertMyCounterpartyProfileInput!): CounterpartyProfile! upsertMyCounterpartyProfile(input: UpsertMyCounterpartyProfileInput!): CounterpartyProfile!
upsertCatalogProductTypeSetting(input: UpsertCatalogProductTypeSettingInput!): CatalogProductTypeSetting!
addProductToCart(productId: ID!): Cart! addProductToCart(productId: ID!): Cart!
updateCartItemQuantity(input: UpdateCartItemQuantityInput!): Cart! updateCartItemQuantity(input: UpdateCartItemQuantityInput!): Cart!
removeCartItem(productId: ID!): Cart! removeCartItem(productId: ID!): Cart!