feat(cart): update CartItem model to use composite key and adjust related logic
fix(cart): ensure cart item updates and deletions are based on cartId and itemId feat(buy): implement balance checks and updates during purchase process fix(sellable): ensure cart items are deleted when a sellable is removed feat(user): add conflict checks for shop creation to prevent ID collisions
This commit is contained in:
parent
f588672894
commit
8b4eaee510
File diff suppressed because one or more lines are too long
10
generated/prisma/index.d.ts
vendored
10
generated/prisma/index.d.ts
vendored
@ -12274,15 +12274,16 @@ export namespace Prisma {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type CartItemWhereUniqueInput = Prisma.AtLeast<{
|
export type CartItemWhereUniqueInput = Prisma.AtLeast<{
|
||||||
itemId?: string
|
cartId_itemId?: CartItemCartIdItemIdCompoundUniqueInput
|
||||||
AND?: CartItemWhereInput | CartItemWhereInput[]
|
AND?: CartItemWhereInput | CartItemWhereInput[]
|
||||||
OR?: CartItemWhereInput[]
|
OR?: CartItemWhereInput[]
|
||||||
NOT?: CartItemWhereInput | CartItemWhereInput[]
|
NOT?: CartItemWhereInput | CartItemWhereInput[]
|
||||||
|
itemId?: StringFilter<"CartItem"> | string
|
||||||
quantity?: IntFilter<"CartItem"> | number
|
quantity?: IntFilter<"CartItem"> | number
|
||||||
cartId?: StringFilter<"CartItem"> | string
|
cartId?: StringFilter<"CartItem"> | string
|
||||||
cart?: XOR<CartScalarRelationFilter, CartWhereInput>
|
cart?: XOR<CartScalarRelationFilter, CartWhereInput>
|
||||||
sellable?: XOR<SellableScalarRelationFilter, SellableWhereInput>
|
sellable?: XOR<SellableScalarRelationFilter, SellableWhereInput>
|
||||||
}, "itemId">
|
}, "cartId_itemId">
|
||||||
|
|
||||||
export type CartItemOrderByWithAggregationInput = {
|
export type CartItemOrderByWithAggregationInput = {
|
||||||
itemId?: SortOrder
|
itemId?: SortOrder
|
||||||
@ -13532,6 +13533,11 @@ export namespace Prisma {
|
|||||||
search: string
|
search: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CartItemCartIdItemIdCompoundUniqueInput = {
|
||||||
|
cartId: string
|
||||||
|
itemId: string
|
||||||
|
}
|
||||||
|
|
||||||
export type CartItemCountOrderByAggregateInput = {
|
export type CartItemCountOrderByAggregateInput = {
|
||||||
itemId?: SortOrder
|
itemId?: SortOrder
|
||||||
quantity?: SortOrder
|
quantity?: SortOrder
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "prisma-client-60cecad7f5344b2ab216d0c215477a4a9f0fb3ec9e5201c830e0c4ac3caafb77",
|
"name": "prisma-client-7f3601640bc9191b9770c2fe70db074b0364e5d01ec2def94d81cee0c9a18abc",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"types": "index.d.ts",
|
"types": "index.d.ts",
|
||||||
"browser": "default.js",
|
"browser": "default.js",
|
||||||
|
|||||||
@ -117,12 +117,14 @@ model Cart {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model CartItem {
|
model CartItem {
|
||||||
itemId String @id
|
itemId String
|
||||||
quantity Int
|
quantity Int
|
||||||
cartId String
|
cartId String
|
||||||
|
|
||||||
cart Cart @relation(fields: [cartId], references: [userId], onDelete: Cascade, onUpdate: Cascade)
|
cart Cart @relation(fields: [cartId], references: [userId], onDelete: Cascade, onUpdate: Cascade)
|
||||||
sellable Sellable @relation(fields: [itemId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
sellable Sellable @relation(fields: [itemId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||||
|
|
||||||
|
@@id([cartId, itemId])
|
||||||
}
|
}
|
||||||
|
|
||||||
//////////////////////
|
//////////////////////
|
||||||
|
|||||||
@ -263,7 +263,7 @@ const config = {
|
|||||||
"value": "prisma-client-js"
|
"value": "prisma-client-js"
|
||||||
},
|
},
|
||||||
"output": {
|
"output": {
|
||||||
"value": "/var/home/zaremate/Documents/cc-create-shop/generated/prisma",
|
"value": "/var/mnt/f5a90b51-c040-4601-a32c-9629866f15a2/Documents/cc-create-shop/generated/prisma",
|
||||||
"fromEnvVar": null
|
"fromEnvVar": null
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
@ -277,7 +277,7 @@ const config = {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"previewFeatures": [],
|
"previewFeatures": [],
|
||||||
"sourceFilePath": "/var/home/zaremate/Documents/cc-create-shop/prisma/schema.prisma",
|
"sourceFilePath": "/var/mnt/f5a90b51-c040-4601-a32c-9629866f15a2/Documents/cc-create-shop/prisma/schema.prisma",
|
||||||
"isCustomOutput": true
|
"isCustomOutput": true
|
||||||
},
|
},
|
||||||
"relativeEnvPaths": {
|
"relativeEnvPaths": {
|
||||||
@ -299,8 +299,8 @@ const config = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"inlineSchema": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n provider = \"prisma-client-js\"\n output = \"../generated/prisma\"\n}\n\ndatasource db {\n provider = \"mysql\"\n // NOTE: When using mysql or sqlserver, uncomment the @db.Text annotations in model Account below\n // Further reading:\n // https://next-auth.js.org/adapters/prisma#create-the-prisma-schema\n // https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#string\n url = env(\"DATABASE_URL\")\n}\n\n// Necessary for Next auth\nmodel Account {\n id String @id @default(cuid())\n userId String\n type String\n provider String\n providerAccountId String\n refresh_token String? @db.Text\n access_token String? // @db.Text\n expires_at Int?\n token_type String?\n scope String?\n id_token String? // @db.Text\n session_state String?\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n refresh_token_expires_in Int?\n\n @@unique([provider, providerAccountId])\n}\n\nmodel Session {\n id String @id @default(cuid())\n sessionToken String @unique\n userId String\n expires DateTime\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n}\n\nmodel User {\n id String @id @default(cuid())\n name String?\n email String? @unique\n emailVerified DateTime?\n image String?\n balance Float @default(1000)\n\n accounts Account[]\n sessions Session[]\n shops Shop[]\n carts Cart[]\n adresses Adress[]\n}\n\nmodel VerificationToken {\n identifier String\n token String @unique\n expires DateTime\n\n @@unique([identifier, token])\n}\n\n//////////////////////\n// SHOP\n//////////////////////\n\nmodel Shop {\n id Int @id\n userId String\n label String\n\n user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)\n items Item[]\n sellables Sellable[]\n}\n\nmodel Item {\n item_name String\n shopId Int\n stock Int\n\n shop Shop @relation(fields: [shopId], references: [id], onDelete: Cascade, onUpdate: Cascade)\n sellables Sellable[]\n\n @@id([item_name, shopId])\n}\n\nmodel Sellable {\n id String @id @default(cuid())\n item_name String\n shopId Int\n amount Int\n price Float\n enabled Boolean @default(true)\n\n shop Shop @relation(fields: [shopId], references: [id], onDelete: Cascade)\n item Item @relation(fields: [item_name, shopId], references: [item_name, shopId], onDelete: Cascade)\n\n cartItems CartItem[]\n}\n\n//////////////////////\n// CART\n//////////////////////\n\nmodel Cart {\n userId String @id\n\n user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)\n cartItems CartItem[]\n}\n\nmodel CartItem {\n itemId String @id\n quantity Int\n cartId String\n\n cart Cart @relation(fields: [cartId], references: [userId], onDelete: Cascade, onUpdate: Cascade)\n sellable Sellable @relation(fields: [itemId], references: [id], onDelete: Cascade, onUpdate: Cascade)\n}\n\n//////////////////////\n// ADDRESS\n//////////////////////\n\nmodel Adress {\n id String @id @default(cuid())\n userId String\n adress String\n\n user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)\n}\n",
|
"inlineSchema": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n provider = \"prisma-client-js\"\n output = \"../generated/prisma\"\n}\n\ndatasource db {\n provider = \"mysql\"\n // NOTE: When using mysql or sqlserver, uncomment the @db.Text annotations in model Account below\n // Further reading:\n // https://next-auth.js.org/adapters/prisma#create-the-prisma-schema\n // https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#string\n url = env(\"DATABASE_URL\")\n}\n\n// Necessary for Next auth\nmodel Account {\n id String @id @default(cuid())\n userId String\n type String\n provider String\n providerAccountId String\n refresh_token String? @db.Text\n access_token String? // @db.Text\n expires_at Int?\n token_type String?\n scope String?\n id_token String? // @db.Text\n session_state String?\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n refresh_token_expires_in Int?\n\n @@unique([provider, providerAccountId])\n}\n\nmodel Session {\n id String @id @default(cuid())\n sessionToken String @unique\n userId String\n expires DateTime\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n}\n\nmodel User {\n id String @id @default(cuid())\n name String?\n email String? @unique\n emailVerified DateTime?\n image String?\n balance Float @default(1000)\n\n accounts Account[]\n sessions Session[]\n shops Shop[]\n carts Cart[]\n adresses Adress[]\n}\n\nmodel VerificationToken {\n identifier String\n token String @unique\n expires DateTime\n\n @@unique([identifier, token])\n}\n\n//////////////////////\n// SHOP\n//////////////////////\n\nmodel Shop {\n id Int @id\n userId String\n label String\n\n user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)\n items Item[]\n sellables Sellable[]\n}\n\nmodel Item {\n item_name String\n shopId Int\n stock Int\n\n shop Shop @relation(fields: [shopId], references: [id], onDelete: Cascade, onUpdate: Cascade)\n sellables Sellable[]\n\n @@id([item_name, shopId])\n}\n\nmodel Sellable {\n id String @id @default(cuid())\n item_name String\n shopId Int\n amount Int\n price Float\n enabled Boolean @default(true)\n\n shop Shop @relation(fields: [shopId], references: [id], onDelete: Cascade)\n item Item @relation(fields: [item_name, shopId], references: [item_name, shopId], onDelete: Cascade)\n\n cartItems CartItem[]\n}\n\n//////////////////////\n// CART\n//////////////////////\n\nmodel Cart {\n userId String @id\n\n user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)\n cartItems CartItem[]\n}\n\nmodel CartItem {\n itemId String\n quantity Int\n cartId String\n\n cart Cart @relation(fields: [cartId], references: [userId], onDelete: Cascade, onUpdate: Cascade)\n sellable Sellable @relation(fields: [itemId], references: [id], onDelete: Cascade, onUpdate: Cascade)\n\n @@id([cartId, itemId])\n}\n\n//////////////////////\n// ADDRESS\n//////////////////////\n\nmodel Adress {\n id String @id @default(cuid())\n userId String\n adress String\n\n user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)\n}\n",
|
||||||
"inlineSchemaHash": "6067e34ba0a13fe49002e5198964784b3870ee91700e9649c3b9ffe64ee9c688",
|
"inlineSchemaHash": "f1f25ffbae59b74d980dd4ecdf5d9929994691bf2809d9a3468ab2985796fd99",
|
||||||
"copyEngine": true
|
"copyEngine": true
|
||||||
}
|
}
|
||||||
config.dirname = '/'
|
config.dirname = '/'
|
||||||
|
|||||||
@ -117,12 +117,14 @@ model Cart {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model CartItem {
|
model CartItem {
|
||||||
itemId String @id
|
itemId String
|
||||||
quantity Int
|
quantity Int
|
||||||
cartId String
|
cartId String
|
||||||
|
|
||||||
cart Cart @relation(fields: [cartId], references: [userId], onDelete: Cascade, onUpdate: Cascade)
|
cart Cart @relation(fields: [cartId], references: [userId], onDelete: Cascade, onUpdate: Cascade)
|
||||||
sellable Sellable @relation(fields: [itemId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
sellable Sellable @relation(fields: [itemId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||||
|
|
||||||
|
@@id([cartId, itemId])
|
||||||
}
|
}
|
||||||
|
|
||||||
//////////////////////
|
//////////////////////
|
||||||
|
|||||||
@ -24,7 +24,10 @@ export async function POST(request: Request) {
|
|||||||
|
|
||||||
// Fetch all cart items with related sellable and item to get shopId
|
// Fetch all cart items with related sellable and item to get shopId
|
||||||
const cartItemsWithShop = await db.cartItem.findMany({
|
const cartItemsWithShop = await db.cartItem.findMany({
|
||||||
where: { itemId: { in: cart.map((c) => c.id) } },
|
where: {
|
||||||
|
cartId: userId,
|
||||||
|
itemId: { in: cart.map((c) => c.id) },
|
||||||
|
},
|
||||||
include: {
|
include: {
|
||||||
sellable: {
|
sellable: {
|
||||||
include: {
|
include: {
|
||||||
@ -49,10 +52,27 @@ export async function POST(request: Request) {
|
|||||||
itemsByShop[shopId] ??= []; // initialize if undefined or null
|
itemsByShop[shopId] ??= []; // initialize if undefined or null
|
||||||
itemsByShop[shopId].push({
|
itemsByShop[shopId].push({
|
||||||
id: ci.sellable.item.item_name, // API expects item_name as id
|
id: ci.sellable.item.item_name, // API expects item_name as id
|
||||||
quantity: ci.quantity,
|
quantity: ci.quantity * ci.sellable.amount, // total quantity based on cart item quantity and sellable amount
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if user has enough balance for all items
|
||||||
|
const totalCost = cartItemsWithShop.reduce((sum, ci) => {
|
||||||
|
return sum + ci.quantity * ci.sellable.price;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
const user = await db.user.findUnique({
|
||||||
|
where: { id: userId },
|
||||||
|
select: { balance: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user || user.balance < totalCost) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Insufficient balance" },
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
console.log(itemsByShop);
|
console.log(itemsByShop);
|
||||||
|
|
||||||
// Send requests per shop
|
// Send requests per shop
|
||||||
@ -79,6 +99,27 @@ export async function POST(request: Request) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deduct the total cost from the user's balance
|
||||||
|
await db.user.update({
|
||||||
|
where: { id: userId },
|
||||||
|
data: { balance: { decrement: totalCost } },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add balance to shop owner
|
||||||
|
console.log("Updating shop owner balance for shop:", shopId);
|
||||||
|
const shop = await db.shop.findUnique({
|
||||||
|
where: { id: Number(shopId) },
|
||||||
|
select: { userId: true },
|
||||||
|
});
|
||||||
|
console.log("Shop owner userId:", shop?.userId);
|
||||||
|
console.log("Total cost to add to shop owner:", totalCost);
|
||||||
|
if (shop) {
|
||||||
|
await db.user.update({
|
||||||
|
where: { id: shop.userId },
|
||||||
|
data: { balance: { increment: totalCost } },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Delete successfully sent items from cart
|
// Delete successfully sent items from cart
|
||||||
const itemIds = items.map((i) => i.id);
|
const itemIds = items.map((i) => i.id);
|
||||||
await db.cartItem.deleteMany({
|
await db.cartItem.deleteMany({
|
||||||
|
|||||||
@ -39,29 +39,46 @@ export async function PATCH(request: Request) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Check if the item already exists in the cart
|
// Check if the item already exists in the cart
|
||||||
const existingCartItem = await db.cartItem.findUnique({
|
const existingCartItem = await db.cartItem.findFirst({
|
||||||
where: { itemId },
|
where: {
|
||||||
|
cartId: userId,
|
||||||
|
itemId,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingCartItem) {
|
if (existingCartItem) {
|
||||||
if (quantity <= 0 && quantity * -1 >= existingCartItem.quantity) {
|
const nextQuantity = existingCartItem.quantity + quantity;
|
||||||
await db.cartItem.delete({
|
|
||||||
where: { itemId },
|
if (nextQuantity <= 0) {
|
||||||
|
await db.cartItem.deleteMany({
|
||||||
|
where: {
|
||||||
|
cartId: userId,
|
||||||
|
itemId,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Update quantity
|
await db.cartItem.updateMany({
|
||||||
await db.cartItem.update({
|
where: {
|
||||||
where: { itemId },
|
cartId: userId,
|
||||||
data: { quantity: existingCartItem.quantity + quantity },
|
itemId,
|
||||||
|
},
|
||||||
|
data: { quantity: nextQuantity },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (quantity < 0) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Cannot remove an item that is not in the cart" },
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Add new item
|
// Add new item
|
||||||
await db.cartItem.create({
|
await db.cartItem.create({
|
||||||
data: {
|
data: {
|
||||||
itemId,
|
itemId,
|
||||||
quantity,
|
quantity,
|
||||||
cartId: userId,
|
cartId: cart.userId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -97,6 +97,10 @@ export async function DELETE(request: Request) {
|
|||||||
return NextResponse.json({ error: "Item not found" }, { status: 404 });
|
return NextResponse.json({ error: "Item not found" }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await db.cartItem.deleteMany({
|
||||||
|
where: { itemId: item.id },
|
||||||
|
});
|
||||||
|
|
||||||
await db.sellable.delete({
|
await db.sellable.delete({
|
||||||
where: {
|
where: {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
|
|||||||
@ -141,6 +141,22 @@ export async function POST(request: Request) {
|
|||||||
const shopsToCreate = body.shops.filter((s) => !currentShopIds.has(s.id));
|
const shopsToCreate = body.shops.filter((s) => !currentShopIds.has(s.id));
|
||||||
|
|
||||||
if (shopsToCreate.length > 0) {
|
if (shopsToCreate.length > 0) {
|
||||||
|
const conflictingShops = await db.shop.findMany({
|
||||||
|
where: {
|
||||||
|
id: { in: shopsToCreate.map((s) => s.id) },
|
||||||
|
NOT: { userId: session.user.id },
|
||||||
|
},
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (conflictingShops.length > 0) {
|
||||||
|
const conflictIds = conflictingShops.map((s) => s.id).join(", ");
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: `Shop ID(s) already claimed by another user: ${conflictIds}` },
|
||||||
|
{ status: 409 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await db.shop.createMany({
|
await db.shop.createMany({
|
||||||
data: shopsToCreate.map((s) => ({
|
data: shopsToCreate.map((s) => ({
|
||||||
id: s.id,
|
id: s.id,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user