base commit
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
-- RedefineTables
|
||||
PRAGMA defer_foreign_keys=ON;
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_Product" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"title" TEXT NOT NULL,
|
||||
"slug" TEXT NOT NULL,
|
||||
"shortDescription" TEXT,
|
||||
"description" TEXT,
|
||||
"quantity" INTEGER NOT NULL DEFAULT 0,
|
||||
"materials" TEXT NOT NULL DEFAULT '[]',
|
||||
"priceCents" INTEGER NOT NULL,
|
||||
"imageUrl" TEXT,
|
||||
"published" BOOLEAN NOT NULL DEFAULT false,
|
||||
"inStock" BOOLEAN NOT NULL DEFAULT true,
|
||||
"leadTimeDays" INTEGER,
|
||||
"categoryId" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "Product_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_Product" ("categoryId", "createdAt", "description", "id", "imageUrl", "inStock", "leadTimeDays", "materials", "priceCents", "published", "quantity", "shortDescription", "slug", "title", "updatedAt") SELECT "categoryId", "createdAt", "description", "id", "imageUrl", "inStock", "leadTimeDays", "materials", "priceCents", "published", coalesce("quantity", 0) AS "quantity", "shortDescription", "slug", "title", "updatedAt" FROM "Product";
|
||||
DROP TABLE "Product";
|
||||
ALTER TABLE "new_Product" RENAME TO "Product";
|
||||
CREATE UNIQUE INDEX "Product_slug_key" ON "Product"("slug");
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA defer_foreign_keys=OFF;
|
||||
@@ -22,8 +22,8 @@ model Product {
|
||||
slug String @unique
|
||||
shortDescription String?
|
||||
description String?
|
||||
/// Количество на складе (если null — не ведём учёт)
|
||||
quantity Int?
|
||||
/// Количество на складе
|
||||
quantity Int @default(0)
|
||||
/// Материалы (список, например: ["хлопок","дерево"])
|
||||
materials String @default("[]")
|
||||
/// Цена в копейках (целое число, без дробной части)
|
||||
|
||||
@@ -89,8 +89,14 @@ export async function registerAdminProductRoutes(
|
||||
return
|
||||
}
|
||||
|
||||
let quantity = null
|
||||
if (!(body.quantity === undefined || body.quantity === null || body.quantity === '')) {
|
||||
let quantity = 0
|
||||
if (!inStock) {
|
||||
quantity = 1
|
||||
} else {
|
||||
if (body.quantity === undefined || body.quantity === null || body.quantity === '') {
|
||||
reply.code(400).send({ error: 'Укажите количество' })
|
||||
return
|
||||
}
|
||||
const n = Number(body.quantity)
|
||||
if (!Number.isFinite(n) || n < 0) {
|
||||
reply.code(400).send({ error: 'Некорректное количество (quantity ≥ 0)' })
|
||||
@@ -162,15 +168,15 @@ export async function registerAdminProductRoutes(
|
||||
if (body.quantity !== undefined) {
|
||||
const v = body.quantity
|
||||
if (v === null || v === '') {
|
||||
data.quantity = null
|
||||
} else {
|
||||
const n = Number(v)
|
||||
if (!Number.isFinite(n) || n < 0) {
|
||||
reply.code(400).send({ error: 'Некорректное количество (quantity ≥ 0)' })
|
||||
return
|
||||
}
|
||||
data.quantity = Math.floor(n)
|
||||
reply.code(400).send({ error: 'Укажите количество' })
|
||||
return
|
||||
}
|
||||
const n = Number(v)
|
||||
if (!Number.isFinite(n) || n < 0) {
|
||||
reply.code(400).send({ error: 'Некорректное количество (quantity ≥ 0)' })
|
||||
return
|
||||
}
|
||||
data.quantity = Math.floor(n)
|
||||
}
|
||||
if (body.materials !== undefined) {
|
||||
data.materials = JSON.stringify(parseMaterialsInput(body.materials))
|
||||
@@ -209,6 +215,9 @@ export async function registerAdminProductRoutes(
|
||||
if (nextInStock && data.leadTimeDays !== undefined) {
|
||||
data.leadTimeDays = null
|
||||
}
|
||||
if (!nextInStock) {
|
||||
data.quantity = 1
|
||||
}
|
||||
|
||||
const imagesUpdate =
|
||||
body.imageUrls !== undefined
|
||||
|
||||
@@ -29,7 +29,7 @@ export async function registerPublicCatalogRoutes(fastify, { mapProductForApi }
|
||||
const priceMaxParsed = typeof priceMaxRaw === 'string' ? Number(priceMaxRaw) : Number(priceMaxRaw)
|
||||
const priceMax = Number.isFinite(priceMaxParsed) && priceMaxParsed >= 0 ? Math.floor(priceMaxParsed) : null
|
||||
|
||||
const where = { published: true }
|
||||
const where = { published: true, quantity: { gt: 0 } }
|
||||
if (typeof categorySlug === 'string' && categorySlug.length > 0) {
|
||||
where.category = { slug: categorySlug }
|
||||
}
|
||||
@@ -70,7 +70,7 @@ export async function registerPublicCatalogRoutes(fastify, { mapProductForApi }
|
||||
fastify.get('/api/products/:id', async (request, reply) => {
|
||||
const { id } = request.params
|
||||
const product = await prisma.product.findFirst({
|
||||
where: { id, published: true },
|
||||
where: { id, published: true, quantity: { gt: 0 } },
|
||||
include: { category: true, images: { orderBy: { sort: 'asc' } } },
|
||||
})
|
||||
if (!product) {
|
||||
|
||||
+62
-24
@@ -393,10 +393,15 @@ export async function registerAuthRoutes(fastify) {
|
||||
const product = await prisma.product.findFirst({ where: { id: productId, published: true } })
|
||||
if (!product) return reply.code(404).send({ error: 'Товар не найден' })
|
||||
|
||||
const available = product.inStock ? product.quantity : 1
|
||||
const existing = await prisma.cartItem.findUnique({ where: { userId_productId: { userId, productId } } })
|
||||
const nextQty = (existing?.qty ?? 0) + Math.floor(qty)
|
||||
if (nextQty > available) return reply.code(409).send({ error: `Доступно: ${available} шт.` })
|
||||
|
||||
const item = await prisma.cartItem.upsert({
|
||||
where: { userId_productId: { userId, productId } },
|
||||
update: { qty: { increment: Math.floor(qty) } },
|
||||
create: { userId, productId, qty: Math.floor(qty) },
|
||||
update: { qty: nextQty },
|
||||
create: { userId, productId, qty: nextQty },
|
||||
})
|
||||
return reply.code(201).send({ item })
|
||||
},
|
||||
@@ -412,7 +417,7 @@ export async function registerAuthRoutes(fastify) {
|
||||
const qty = Number(qtyRaw)
|
||||
if (!Number.isFinite(qty) || qty < 0) return reply.code(400).send({ error: 'qty должен быть ≥ 0' })
|
||||
|
||||
const existing = await prisma.cartItem.findFirst({ where: { id, userId } })
|
||||
const existing = await prisma.cartItem.findFirst({ where: { id, userId }, include: { product: true } })
|
||||
if (!existing) return reply.code(404).send({ error: 'Позиция корзины не найдена' })
|
||||
|
||||
if (qty === 0) {
|
||||
@@ -420,7 +425,11 @@ export async function registerAuthRoutes(fastify) {
|
||||
return reply.code(204).send()
|
||||
}
|
||||
|
||||
const updated = await prisma.cartItem.update({ where: { id }, data: { qty: Math.floor(qty) } })
|
||||
const available = existing.product.inStock ? existing.product.quantity : 1
|
||||
const nextQty = Math.floor(qty)
|
||||
if (nextQty > available) return reply.code(409).send({ error: `Доступно: ${available} шт.` })
|
||||
|
||||
const updated = await prisma.cartItem.update({ where: { id }, data: { qty: nextQty } })
|
||||
return { item: updated }
|
||||
},
|
||||
)
|
||||
@@ -460,6 +469,13 @@ export async function registerAuthRoutes(fastify) {
|
||||
})
|
||||
if (cartItems.length === 0) return reply.code(400).send({ error: 'Корзина пуста' })
|
||||
|
||||
for (const ci of cartItems) {
|
||||
const available = ci.product.inStock ? ci.product.quantity : 1
|
||||
if (ci.qty > available) {
|
||||
return reply.code(409).send({ error: `Недостаточно товара: "${ci.product.title}". Доступно: ${available} шт.` })
|
||||
}
|
||||
}
|
||||
|
||||
const itemsPayload = cartItems.map((ci) => ({
|
||||
productId: ci.productId,
|
||||
qty: ci.qty,
|
||||
@@ -479,28 +495,50 @@ export async function registerAuthRoutes(fastify) {
|
||||
lng: address.lng,
|
||||
})
|
||||
|
||||
const created = await prisma.$transaction(async (tx) => {
|
||||
const order = await tx.order.create({
|
||||
data: {
|
||||
userId,
|
||||
status: 'PENDING_PAYMENT',
|
||||
totalCents,
|
||||
currency: 'RUB',
|
||||
addressSnapshotJson,
|
||||
comment: comment && comment.length ? comment : null,
|
||||
items: {
|
||||
create: itemsPayload.map((i) => ({
|
||||
productId: i.productId,
|
||||
qty: i.qty,
|
||||
titleSnapshot: i.titleSnapshot,
|
||||
priceCentsSnapshot: i.priceCentsSnapshot,
|
||||
})),
|
||||
let created
|
||||
try {
|
||||
created = await prisma.$transaction(async (tx) => {
|
||||
for (const ci of cartItems) {
|
||||
if (!ci.product.inStock) continue
|
||||
|
||||
const res = await tx.product.updateMany({
|
||||
where: { id: ci.productId, quantity: { gte: ci.qty } },
|
||||
data: { quantity: { decrement: ci.qty } },
|
||||
})
|
||||
if (res.count !== 1) {
|
||||
throw new Error(`Недостаточно товара: "${ci.product.title}"`)
|
||||
}
|
||||
|
||||
const p = await tx.product.findUnique({ where: { id: ci.productId }, select: { quantity: true } })
|
||||
if (p && p.quantity === 0) {
|
||||
await tx.product.update({ where: { id: ci.productId }, data: { published: false } })
|
||||
}
|
||||
}
|
||||
|
||||
const order = await tx.order.create({
|
||||
data: {
|
||||
userId,
|
||||
status: 'PENDING_PAYMENT',
|
||||
totalCents,
|
||||
currency: 'RUB',
|
||||
addressSnapshotJson,
|
||||
comment: comment && comment.length ? comment : null,
|
||||
items: {
|
||||
create: itemsPayload.map((i) => ({
|
||||
productId: i.productId,
|
||||
qty: i.qty,
|
||||
titleSnapshot: i.titleSnapshot,
|
||||
priceCentsSnapshot: i.priceCentsSnapshot,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
await tx.cartItem.deleteMany({ where: { userId } })
|
||||
return order
|
||||
})
|
||||
await tx.cartItem.deleteMany({ where: { userId } })
|
||||
return order
|
||||
})
|
||||
} catch (e) {
|
||||
return reply.code(409).send({ error: (e instanceof Error && e.message) || 'Недостаточно товара' })
|
||||
}
|
||||
|
||||
return reply.code(201).send({ orderId: created.id })
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user