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
}
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 {
id String @id @default(cuid())
userId String @unique

View File

@@ -352,6 +352,19 @@ function normalizeQuantityValue(value) {
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) {
if (!profile) {
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 = {
items: {
orderBy: [{ updatedAt: 'desc' }, { createdAt: 'desc' }],
@@ -864,6 +962,8 @@ export const resolvers = {
orderBy: { name: 'asc' },
}),
catalogProductTypeSettings: async (_, __, context) => listCatalogProductTypeSettings(context.prisma),
order: async (_, { id }, context) => {
const user = requireUser(context);
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) => {
const user = requireUser(context);
const normalizedProductId = normalizeText(productId);

View File

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