From fa7274af73811d026b63c3d2a90fe44c2a55fc6f Mon Sep 17 00:00:00 2001 From: Mohammed Fabinsha Date: Tue, 5 Aug 2025 11:13:37 +0530 Subject: [PATCH 01/32] feat: Update Schema for StockLedger --- .../migration.sql | 5 ++++ .../migration.sql | 19 +++++++++++++ libs/models/prisma/schema.prisma | 27 +++++++++++++++---- 3 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 libs/models/prisma/migrations/20250805053034_add_stock_balance/migration.sql create mode 100644 libs/models/prisma/migrations/20250805054240_add_stock_ledger/migration.sql diff --git a/libs/models/prisma/migrations/20250805053034_add_stock_balance/migration.sql b/libs/models/prisma/migrations/20250805053034_add_stock_balance/migration.sql new file mode 100644 index 00000000..4cc9a426 --- /dev/null +++ b/libs/models/prisma/migrations/20250805053034_add_stock_balance/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "Item" ADD COLUMN "stockBalance" INTEGER NOT NULL DEFAULT 0; + +-- AlterTable +ALTER TABLE "ItemVariant" ADD COLUMN "stockBalance" INTEGER NOT NULL DEFAULT 0; diff --git a/libs/models/prisma/migrations/20250805054240_add_stock_ledger/migration.sql b/libs/models/prisma/migrations/20250805054240_add_stock_ledger/migration.sql new file mode 100644 index 00000000..fb9cb4a3 --- /dev/null +++ b/libs/models/prisma/migrations/20250805054240_add_stock_ledger/migration.sql @@ -0,0 +1,19 @@ +-- CreateTable +CREATE TABLE "StockLedger" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "qty" INTEGER NOT NULL, + "balance" INTEGER NOT NULL, + "itemId" UUID NOT NULL, + "itemVariantId" UUID NOT NULL, + "remarks" TEXT, + "refId" TEXT, + + CONSTRAINT "StockLedger_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "StockLedger" ADD CONSTRAINT "StockLedger_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "Item"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "StockLedger" ADD CONSTRAINT "StockLedger_itemVariantId_fkey" FOREIGN KEY ("itemVariantId") REFERENCES "ItemVariant"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/libs/models/prisma/schema.prisma b/libs/models/prisma/schema.prisma index 938b23ee..79d29ab0 100644 --- a/libs/models/prisma/schema.prisma +++ b/libs/models/prisma/schema.prisma @@ -161,9 +161,10 @@ model Item { discountedPrice Float? slug String @unique - stockStatus StockStatus @default(AVAILABLE) - gender Gender[] @default([MEN, WOMEN]) - hasVariants Boolean @default(false) + stockStatus StockStatus @default(AVAILABLE) + stockBalance Int @default(0) + gender Gender[] @default([MEN, WOMEN]) + hasVariants Boolean @default(false) images Json @default("[]") wishlistItems WishlistItem[] @@ -180,8 +181,9 @@ model Item { sizeChart String? - createdAt DateTime @default(now()) @db.Timestamptz(3) - updatedAt DateTime @default(now()) @db.Timestamptz(3) + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime @default(now()) @db.Timestamptz(3) + StockLedger StockLedger[] } model ItemVariant { @@ -189,6 +191,7 @@ model ItemVariant { item Item @relation(fields: [itemId], references: [id]) itemId String @db.Uuid stockStatus StockStatus @default(AVAILABLE) + stockBalance Int @default(0) sku String? enabled Boolean @default(true) title String @@ -204,6 +207,7 @@ model ItemVariant { CartItem CartItem[] WishlistItem WishlistItem[] OrderItem OrderItem[] + StockLedger StockLedger[] } model VariantAttributeValue { @@ -517,6 +521,19 @@ model Tax { updatedAt DateTime @default(now()) @db.Timestamptz(3) } +model StockLedger { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + createdAt DateTime @default(now()) @db.Timestamptz(3) + qty Int + balance Int + item Item @relation(fields: [itemId], references: [id]) + itemId String @db.Uuid + itemVariant ItemVariant @relation(fields: [itemVariantId], references: [id]) + itemVariantId String @db.Uuid + remarks String? + refId String? +} + model SMSLog { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid context String From d4d200ce84f9d7d6578298eebace3591b57ead9c Mon Sep 17 00:00:00 2001 From: Mohammed Fabinsha Date: Tue, 12 Aug 2025 10:04:26 +0530 Subject: [PATCH 02/32] wip: StockBalance And Reconcile --- libs/items/src/services/index.ts | 1 + .../services/stocks/getStockBalance/index.ts | 2 + .../stocks/getStockBalance/service.ts | 29 ++++++++++++ .../services/stocks/getStockBalance/types.ts | 15 ++++++ .../services/stocks/getStockBalance/zod.ts | 8 ++++ libs/items/src/services/stocks/index.ts | 2 + .../stocks/reconcileInventory/index.ts | 3 ++ .../stocks/reconcileInventory/service.ts | 46 +++++++++++++++++++ .../stocks/reconcileInventory/types.ts | 10 ++++ .../services/stocks/reconcileInventory/zod.ts | 11 +++++ .../stocks/getStockBalance/getStockBalance.ts | 19 ++++++++ .../src/swr/stocks/getStockBalance/index.ts | 2 + .../src/swr/stocks/getStockBalance/service.ts | 24 ++++++++++ libs/items/src/swr/stocks/index.ts | 3 ++ libs/items/src/swr/stocks/keys.ts | 5 ++ 15 files changed, 180 insertions(+) create mode 100644 libs/items/src/services/stocks/getStockBalance/index.ts create mode 100644 libs/items/src/services/stocks/getStockBalance/service.ts create mode 100644 libs/items/src/services/stocks/getStockBalance/types.ts create mode 100644 libs/items/src/services/stocks/getStockBalance/zod.ts create mode 100644 libs/items/src/services/stocks/index.ts create mode 100644 libs/items/src/services/stocks/reconcileInventory/index.ts create mode 100644 libs/items/src/services/stocks/reconcileInventory/service.ts create mode 100644 libs/items/src/services/stocks/reconcileInventory/types.ts create mode 100644 libs/items/src/services/stocks/reconcileInventory/zod.ts create mode 100644 libs/items/src/swr/stocks/getStockBalance/getStockBalance.ts create mode 100644 libs/items/src/swr/stocks/getStockBalance/index.ts create mode 100644 libs/items/src/swr/stocks/getStockBalance/service.ts create mode 100644 libs/items/src/swr/stocks/index.ts create mode 100644 libs/items/src/swr/stocks/keys.ts diff --git a/libs/items/src/services/index.ts b/libs/items/src/services/index.ts index 1aa5f981..d0b30092 100644 --- a/libs/items/src/services/index.ts +++ b/libs/items/src/services/index.ts @@ -9,6 +9,7 @@ export * from './cart/get-cart'; export * from './cart/remove-from-cart'; export * from './categories'; export * from './items'; +export * from './stocks/reconcileInventory'; export * from './variants'; export * from './wishlist/add-to-wishlist'; export * from './wishlist/get-wishlist'; diff --git a/libs/items/src/services/stocks/getStockBalance/index.ts b/libs/items/src/services/stocks/getStockBalance/index.ts new file mode 100644 index 00000000..45237b7d --- /dev/null +++ b/libs/items/src/services/stocks/getStockBalance/index.ts @@ -0,0 +1,2 @@ +export * from './service'; +export * from './types'; diff --git a/libs/items/src/services/stocks/getStockBalance/service.ts b/libs/items/src/services/stocks/getStockBalance/service.ts new file mode 100644 index 00000000..f1ab6515 --- /dev/null +++ b/libs/items/src/services/stocks/getStockBalance/service.ts @@ -0,0 +1,29 @@ +import { getPrismaClient } from '@vestido-ecommerce/models'; + +import { getStockBalanceSchema, getStockBalanceSchemaType } from './zod'; + +export async function getStockBalance(data: getStockBalanceSchemaType) { + const prisma = getPrismaClient(); + + const validatedData = getStockBalanceSchema.parse(data); + + let latestStockBalanceDetails; + + if (validatedData.itemVariantId === null) { + latestStockBalanceDetails = await prisma.stockLedger.findFirst({ + where: { itemId: validatedData.itemId }, + orderBy: { createdAt: 'desc' }, + select: { itemId: true, itemVariantId: true, balance: true }, + }); + } else { + latestStockBalanceDetails = await prisma.stockLedger.findFirst({ + where: { + itemId: validatedData.itemId, + itemVariantId: validatedData.itemVariantId, + }, + orderBy: { createdAt: 'desc' }, + select: { itemId: true, itemVariantId: true, balance: true }, + }); + } + return latestStockBalanceDetails; +} diff --git a/libs/items/src/services/stocks/getStockBalance/types.ts b/libs/items/src/services/stocks/getStockBalance/types.ts new file mode 100644 index 00000000..004470cd --- /dev/null +++ b/libs/items/src/services/stocks/getStockBalance/types.ts @@ -0,0 +1,15 @@ +import { VestidoResponse } from '@vestido-ecommerce/utils'; + +import { getStockBalance } from './service'; +import { getStockBalanceSchemaType } from './zod'; + +export type getStockBalanceRequest = { + data: getStockBalanceSchemaType; +}; + +export type getStockBalanceResponse = { + data: Awaited>; +}; + +export type getStockBalanceSWRResponse = + VestidoResponse; diff --git a/libs/items/src/services/stocks/getStockBalance/zod.ts b/libs/items/src/services/stocks/getStockBalance/zod.ts new file mode 100644 index 00000000..64d2d20b --- /dev/null +++ b/libs/items/src/services/stocks/getStockBalance/zod.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; + +export const getStockBalanceSchema = z.object({ + itemId: z.string(), + itemVariantId: z.string().optional(), +}); + +export type getStockBalanceSchemaType = z.infer; diff --git a/libs/items/src/services/stocks/index.ts b/libs/items/src/services/stocks/index.ts new file mode 100644 index 00000000..aea7a01c --- /dev/null +++ b/libs/items/src/services/stocks/index.ts @@ -0,0 +1,2 @@ +export * from './getStockBalance'; +export * from './reconcileInventory'; diff --git a/libs/items/src/services/stocks/reconcileInventory/index.ts b/libs/items/src/services/stocks/reconcileInventory/index.ts new file mode 100644 index 00000000..8a9411ad --- /dev/null +++ b/libs/items/src/services/stocks/reconcileInventory/index.ts @@ -0,0 +1,3 @@ +export * from './service'; +export * from './types'; +export * from './zod'; diff --git a/libs/items/src/services/stocks/reconcileInventory/service.ts b/libs/items/src/services/stocks/reconcileInventory/service.ts new file mode 100644 index 00000000..9c960978 --- /dev/null +++ b/libs/items/src/services/stocks/reconcileInventory/service.ts @@ -0,0 +1,46 @@ +import { getPrismaClient } from '@vestido-ecommerce/models'; + +import { stockUpdateSchema } from './zod'; +import { stockUpdateSchemaType } from './zod'; + +export async function reconcileInventory(data: stockUpdateSchemaType) { + const prisma = getPrismaClient(); + const validatedData = stockUpdateSchema.parse(data); + + await prisma.$transaction(async (prisma) => { + if (validatedData.itemVariantId === null) { + await prisma.item.update({ + where: { + id: validatedData.itemId, + }, + data: { + stockBalance: validatedData.qty, + }, + }); + } else { + await prisma.itemVariant.update({ + where: { + id: validatedData.itemVariantId, + }, + data: { + stockBalance: validatedData.qty, + }, + }); + } + + await prisma.stockLedger.create({ + data: { + qty: validatedData.qty, + balance: validatedData.qty, + itemId: validatedData.itemId, + itemVariantId: validatedData.itemVariantId + ? validatedData.itemVariantId + : ' ', + remarks: validatedData.remarks, + refId: validatedData.refId, + }, + }); + }); + + return; +} diff --git a/libs/items/src/services/stocks/reconcileInventory/types.ts b/libs/items/src/services/stocks/reconcileInventory/types.ts new file mode 100644 index 00000000..1b0db554 --- /dev/null +++ b/libs/items/src/services/stocks/reconcileInventory/types.ts @@ -0,0 +1,10 @@ +import { Item } from '@prisma/client'; + +import { stockUpdateSchemaType } from './zod'; + +export type stockUpdateRequest = { + data: stockUpdateSchemaType; +}; +export type stockUpdateResponse = { + data: Item; +}; diff --git a/libs/items/src/services/stocks/reconcileInventory/zod.ts b/libs/items/src/services/stocks/reconcileInventory/zod.ts new file mode 100644 index 00000000..1e06af04 --- /dev/null +++ b/libs/items/src/services/stocks/reconcileInventory/zod.ts @@ -0,0 +1,11 @@ +import { z } from 'zod'; + +export const stockUpdateSchema = z.object({ + refId: z.string().optional(), + itemId: z.string(), + itemVariantId: z.string().optional(), + qty: z.coerce.number(), + remarks: z.string(), +}); + +export type stockUpdateSchemaType = z.infer; diff --git a/libs/items/src/swr/stocks/getStockBalance/getStockBalance.ts b/libs/items/src/swr/stocks/getStockBalance/getStockBalance.ts new file mode 100644 index 00000000..095954ff --- /dev/null +++ b/libs/items/src/swr/stocks/getStockBalance/getStockBalance.ts @@ -0,0 +1,19 @@ +import useSWRImmutable from 'swr/immutable'; + +import { useAuth } from '@vestido-ecommerce/auth/client'; + +import { getStockBalanceSWRResponse } from 'libs/items/src/services/stocks'; +import { StockSWRKeys } from '../keys'; +import { getStockBalance } from './service'; + +export function useStockBal(args: string | null) { + const { authHeaders } = useAuth(); + const key = [StockSWRKeys.STOCKBALANCE, StockSWRKeys.GET, itemId]; + return useSWRImmutable( + key, + () => getStockBalance({ ...args }, authHeaders), + { + keepPreviousData: true, + }, + ); +} diff --git a/libs/items/src/swr/stocks/getStockBalance/index.ts b/libs/items/src/swr/stocks/getStockBalance/index.ts new file mode 100644 index 00000000..ca1ecf50 --- /dev/null +++ b/libs/items/src/swr/stocks/getStockBalance/index.ts @@ -0,0 +1,2 @@ +export * from './getStockBalance'; +export * from './service'; diff --git a/libs/items/src/swr/stocks/getStockBalance/service.ts b/libs/items/src/swr/stocks/getStockBalance/service.ts new file mode 100644 index 00000000..2cf2c225 --- /dev/null +++ b/libs/items/src/swr/stocks/getStockBalance/service.ts @@ -0,0 +1,24 @@ +import { handleVestidoErrorResponse } from '@vestido-ecommerce/utils'; + +import { + getStockBalanceResponse, + getStockBalanceRequest, +} from 'libs/items/src/services/stocks'; + +export async function getStockBalance( + args: getStockBalanceRequest, + headers?: Record, +): Promise { + const url = `/api/stocks/`; + const r = await fetch(url, { + headers: { + ...(headers ?? {}), + }, + }); + if (!r.ok) { + await handleVestidoErrorResponse(r); + } + const data = await r.json(); + + return data as getStockBalanceResponse; +} diff --git a/libs/items/src/swr/stocks/index.ts b/libs/items/src/swr/stocks/index.ts new file mode 100644 index 00000000..1b494318 --- /dev/null +++ b/libs/items/src/swr/stocks/index.ts @@ -0,0 +1,3 @@ +'use client'; + +export * from './getStockBalance'; diff --git a/libs/items/src/swr/stocks/keys.ts b/libs/items/src/swr/stocks/keys.ts new file mode 100644 index 00000000..469e4105 --- /dev/null +++ b/libs/items/src/swr/stocks/keys.ts @@ -0,0 +1,5 @@ +export enum StockSWRKeys { + STOCKBALANCE = 'stockbalance', + RECONCILE = 'reconcile', + GET = 'get', +} From e68abe97bbc40ea0cec316b4e20b0f3f62e65e12 Mon Sep 17 00:00:00 2001 From: Fathima Sayeeda Date: Wed, 13 Aug 2025 19:33:05 +0530 Subject: [PATCH 03/32] fix: Make Order Creation a Prisma Transaction --- .../services/orders/create-order/service.ts | 161 ++++++++---------- 1 file changed, 68 insertions(+), 93 deletions(-) diff --git a/libs/orders/src/services/orders/create-order/service.ts b/libs/orders/src/services/orders/create-order/service.ts index 4b5c0a28..e1df89c1 100644 --- a/libs/orders/src/services/orders/create-order/service.ts +++ b/libs/orders/src/services/orders/create-order/service.ts @@ -1,3 +1,5 @@ +import type { Payment } from '@prisma/client'; + import { sendSMS, SMSSenderID, SMSTemplate } from '@vestido-ecommerce/fast2sms'; import { clearCartOnOrderCreation } from '@vestido-ecommerce/items'; import { getPrismaClient } from '@vestido-ecommerce/models'; @@ -15,9 +17,7 @@ export async function createOrder(_data: CreateOrderSchemaType) { CreateOrderSchema.parse(_data); const shippingdetails = await prisma.customerAddress.findUnique({ - where: { - id: addressId, - }, + where: { id: addressId }, }); const { @@ -28,73 +28,80 @@ export async function createOrder(_data: CreateOrderSchemaType) { grandTotal, itemsWithTax, } = await calculateTotal({ - addressId: addressId, + addressId, orderItems: _data.orderItems, paymentType, couponCode, }); - const newOrder = await prisma.order.create({ - data: { - ...data, - createdAt: new Date(), - /** - * For Cash on Delivery, the order status is set to CONFIRMED immediately. - * We are anticipating an error from the payment gateway, hence setting the order status to PENDING. - */ - orderStatus: paymentType == 'CASH_ON_DELIVERY' ? 'CONFIRMED' : 'PENDING', - totalPrice: itemsPrice - totalTax, - totalTax: totalTax, - totalCharges: shippingCharges, - totalDiscount: couponDiscount, - grandTotal: grandTotal, - couponCode: couponCode, - customer: { - connect: { - id: customerId, - }, - }, - shippingAddress: { - connect: { - id: addressId, - }, - }, - orderItems: { - createMany: { - data: itemsWithTax.map((item) => ({ - itemId: item.itemId, - price: item.price, - qty: item.qty, - variantId: item.variantId, - taxTitle: item.taxTitle, - taxRate: item.taxRate, - taxInclusive: item.taxInclusive, - taxAmount: item.taxAmount, // Added taxAmount here - status: paymentType == 'CASH_ON_DELIVERY' ? 'CONFIRMED' : 'PENDING', - })), - }, - }, - }, - }); - - let newPayment = null; - if (paymentType == 'CASH_ON_DELIVERY') { - newPayment = await prisma.payment.create({ + // Transaction: returns the created order + payment + const { newOrder, newPayment } = await prisma.$transaction(async (tx) => { + const newOrder = await tx.order.create({ data: { - order: { - connect: { - id: newOrder.id, + ...data, + createdAt: new Date(), + orderStatus: + paymentType === 'CASH_ON_DELIVERY' ? 'CONFIRMED' : 'PENDING', + totalPrice: itemsPrice - totalTax, + totalTax, + totalCharges: shippingCharges, + totalDiscount: couponDiscount, + grandTotal, + couponCode, + customer: { connect: { id: customerId } }, + shippingAddress: { connect: { id: addressId } }, + orderItems: { + createMany: { + data: itemsWithTax.map((item) => ({ + itemId: item.itemId, + price: item.price, + qty: item.qty, + variantId: item.variantId, + taxTitle: item.taxTitle, + taxRate: item.taxRate, + taxInclusive: item.taxInclusive, + taxAmount: item.taxAmount, + status: + paymentType === 'CASH_ON_DELIVERY' ? 'CONFIRMED' : 'PENDING', + })), }, }, - paymentGateway: 'CASH_ON_DELIVERY', - paymentGatewayRef: 'Null', - moreDetails: 'Null', - currency: 'INR', - amount: grandTotal, - status: 'PENDING', }, }); + let newPayment: Payment | null = null; + + if (paymentType === 'CASH_ON_DELIVERY') { + newPayment = await tx.payment.create({ + data: { + order: { connect: { id: newOrder.id } }, + paymentGateway: 'CASH_ON_DELIVERY', + paymentGatewayRef: 'Null', + moreDetails: 'Null', + currency: 'INR', + amount: grandTotal, + status: 'PENDING', + }, + }); + } else if (paymentType === 'REPLACEMENT_ORDER') { + newPayment = await tx.payment.create({ + data: { + order: { connect: { id: newOrder.id } }, + paymentGateway: 'REPLACEMENT_ORDER', + paymentGatewayRef: 'Null', + moreDetails: 'Null', + currency: 'INR', + amount: 0, + status: 'CAPTURED', + }, + }); + } + + return { newOrder, newPayment }; + }); + + // ==== Side effects (outside transaction) ==== + if (paymentType === 'CASH_ON_DELIVERY') { const totalItems = itemsWithTax .reduce((sum, item) => sum + item.qty, 0) .toString(); @@ -115,45 +122,13 @@ export async function createOrder(_data: CreateOrderSchemaType) { name: 'SendOTPFailed', message: 'Failed to send ORDER_PLACED_SMS', httpStatus: 500, - context: { - newOrder, - error: e, - }, + context: { newOrder, error: e }, }); } } - // Clear Cart on Confirmation await clearCartOnOrderCreation(newOrder.id); - } else if (paymentType == 'REPLACEMENT_ORDER') { - newPayment = await prisma.payment.create({ - data: { - order: { - connect: { - id: newOrder.id, - }, - }, - paymentGateway: 'REPLACEMENT_ORDER', - paymentGatewayRef: 'Null', - moreDetails: 'Null', - currency: 'INR', - amount: 0, - status: 'CAPTURED', - }, - }); } - return { - order: newOrder, - payment: newPayment, - }; - /** - * return { - * order: newOrder, - * paymentGateway: "", - * paymentGatewayArgs: { - * ... - * } - * } - */ + return { order: newOrder, payment: newPayment }; } From 7486bfeb2e3b163a1ce52c7102a98e9e0e7e789d Mon Sep 17 00:00:00 2001 From: Fathima Sayeeda Date: Wed, 13 Aug 2025 20:58:17 +0530 Subject: [PATCH 04/32] feat: ReserveInventory Service --- libs/items/src/services/index.ts | 1 + libs/items/src/services/stocks/index.ts | 1 + .../services/stocks/reserveInventory/index.ts | 3 + .../stocks/reserveInventory/service.ts | 80 +++++++++++++++++++ .../services/stocks/reserveInventory/types.ts | 10 +++ .../services/stocks/reserveInventory/zod.ts | 15 ++++ 6 files changed, 110 insertions(+) create mode 100644 libs/items/src/services/stocks/reserveInventory/index.ts create mode 100644 libs/items/src/services/stocks/reserveInventory/service.ts create mode 100644 libs/items/src/services/stocks/reserveInventory/types.ts create mode 100644 libs/items/src/services/stocks/reserveInventory/zod.ts diff --git a/libs/items/src/services/index.ts b/libs/items/src/services/index.ts index d0b30092..28de229a 100644 --- a/libs/items/src/services/index.ts +++ b/libs/items/src/services/index.ts @@ -10,6 +10,7 @@ export * from './cart/remove-from-cart'; export * from './categories'; export * from './items'; export * from './stocks/reconcileInventory'; +export * from './stocks/reserveInventory'; export * from './variants'; export * from './wishlist/add-to-wishlist'; export * from './wishlist/get-wishlist'; diff --git a/libs/items/src/services/stocks/index.ts b/libs/items/src/services/stocks/index.ts index aea7a01c..699eff27 100644 --- a/libs/items/src/services/stocks/index.ts +++ b/libs/items/src/services/stocks/index.ts @@ -1,2 +1,3 @@ export * from './getStockBalance'; export * from './reconcileInventory'; +export * from './reserveInventory'; diff --git a/libs/items/src/services/stocks/reserveInventory/index.ts b/libs/items/src/services/stocks/reserveInventory/index.ts new file mode 100644 index 00000000..8a9411ad --- /dev/null +++ b/libs/items/src/services/stocks/reserveInventory/index.ts @@ -0,0 +1,3 @@ +export * from './service'; +export * from './types'; +export * from './zod'; diff --git a/libs/items/src/services/stocks/reserveInventory/service.ts b/libs/items/src/services/stocks/reserveInventory/service.ts new file mode 100644 index 00000000..4e9d0770 --- /dev/null +++ b/libs/items/src/services/stocks/reserveInventory/service.ts @@ -0,0 +1,80 @@ +import type { PrismaTransactionalClient } from '@vestido-ecommerce/models'; +import { VestidoError } from '@vestido-ecommerce/utils'; + +import { reserveInventorySchema, reserveInventorySchemaType } from './zod'; + +type StockRow = { stockBalance: number }; + +export async function reserveInventory( + prisma: PrismaTransactionalClient, + data: reserveInventorySchemaType, +) { + const validatedData = reserveInventorySchema.parse(data); + + for (const item of validatedData.items) { + let currentStock: StockRow | undefined; + + if (item.itemVariantId === null) { + [currentStock] = await prisma.$queryRaw` + SELECT "stockBalance" + FROM "Item" + WHERE "id" = ${item.itemId} + FOR UPDATE; + `; + } else { + [currentStock] = await prisma.$queryRaw` + SELECT "stockBalance" + FROM "ItemVariant" + WHERE "id" = ${item.itemVariantId} + FOR UPDATE; + `; + } + + if (!currentStock) { + throw new VestidoError({ + name: 'Item Not Found', + message: `Item ${item.itemId} not found`, + httpStatus: 400, + }); + } + + if (currentStock.stockBalance < item.qty) { + throw new VestidoError({ + name: 'Not Enough Stock', + message: `Not enough stock to reserve for item ${item.itemId}`, + httpStatus: 400, + }); + } + + if (item.itemVariantId === null) { + await prisma.item.update({ + where: { + id: item.itemId, + }, + data: { + stockBalance: currentStock.stockBalance - item.qty, + }, + }); + } else { + await prisma.itemVariant.update({ + where: { + id: item.itemVariantId, + }, + data: { + stockBalance: currentStock.stockBalance - item.qty, + }, + }); + } + + await prisma.stockLedger.create({ + data: { + qty: item.qty, + balance: currentStock.stockBalance - item.qty, + itemId: item.itemId, + itemVariantId: item.itemVariantId ?? '', + remarks: validatedData.remarks, + refId: validatedData.refId, + }, + }); + } +} diff --git a/libs/items/src/services/stocks/reserveInventory/types.ts b/libs/items/src/services/stocks/reserveInventory/types.ts new file mode 100644 index 00000000..36bf89bf --- /dev/null +++ b/libs/items/src/services/stocks/reserveInventory/types.ts @@ -0,0 +1,10 @@ +import { Item } from '@prisma/client'; + +import { reserveInventorySchemaType } from './zod'; + +export type reserveInventoryRequest = { + data: reserveInventorySchemaType; +}; +export type reserveInventoryResponse = { + data: Item; +}; diff --git a/libs/items/src/services/stocks/reserveInventory/zod.ts b/libs/items/src/services/stocks/reserveInventory/zod.ts new file mode 100644 index 00000000..65b2c9d2 --- /dev/null +++ b/libs/items/src/services/stocks/reserveInventory/zod.ts @@ -0,0 +1,15 @@ +import { z } from 'zod'; + +export const reserveInventorySchema = z.object({ + refId: z.string().optional(), + items: z.array( + z.object({ + itemId: z.string(), + itemVariantId: z.string().nullish(), + qty: z.coerce.number(), + }), + ), + remarks: z.string(), +}); + +export type reserveInventorySchemaType = z.infer; From 785fec167ccbbdcd81f2fa2622e86462dada483e Mon Sep 17 00:00:00 2001 From: Fathima Sayeeda Date: Wed, 13 Aug 2025 20:58:51 +0530 Subject: [PATCH 05/32] fix: ReserveInventory function in Order Transaction --- .../services/orders/create-order/service.ts | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/libs/orders/src/services/orders/create-order/service.ts b/libs/orders/src/services/orders/create-order/service.ts index e1df89c1..c8410183 100644 --- a/libs/orders/src/services/orders/create-order/service.ts +++ b/libs/orders/src/services/orders/create-order/service.ts @@ -2,6 +2,7 @@ import type { Payment } from '@prisma/client'; import { sendSMS, SMSSenderID, SMSTemplate } from '@vestido-ecommerce/fast2sms'; import { clearCartOnOrderCreation } from '@vestido-ecommerce/items'; +import { reserveInventory } from '@vestido-ecommerce/items'; import { getPrismaClient } from '@vestido-ecommerce/models'; import { VestidoError } from '@vestido-ecommerce/utils'; @@ -34,9 +35,8 @@ export async function createOrder(_data: CreateOrderSchemaType) { couponCode, }); - // Transaction: returns the created order + payment - const { newOrder, newPayment } = await prisma.$transaction(async (tx) => { - const newOrder = await tx.order.create({ + const { newOrder, newPayment } = await prisma.$transaction(async (prisma) => { + const newOrder = await prisma.order.create({ data: { ...data, createdAt: new Date(), @@ -72,7 +72,7 @@ export async function createOrder(_data: CreateOrderSchemaType) { let newPayment: Payment | null = null; if (paymentType === 'CASH_ON_DELIVERY') { - newPayment = await tx.payment.create({ + newPayment = await prisma.payment.create({ data: { order: { connect: { id: newOrder.id } }, paymentGateway: 'CASH_ON_DELIVERY', @@ -84,7 +84,7 @@ export async function createOrder(_data: CreateOrderSchemaType) { }, }); } else if (paymentType === 'REPLACEMENT_ORDER') { - newPayment = await tx.payment.create({ + newPayment = await prisma.payment.create({ data: { order: { connect: { id: newOrder.id } }, paymentGateway: 'REPLACEMENT_ORDER', @@ -97,10 +97,19 @@ export async function createOrder(_data: CreateOrderSchemaType) { }); } + await reserveInventory(prisma, { + refId: newOrder.id, + remarks: 'Order Reservation', + items: itemsWithTax.map((item) => ({ + itemId: item.itemId, + itemVariantId: item.variantId ?? null, + qty: item.qty, + })), + }); + return { newOrder, newPayment }; }); - // ==== Side effects (outside transaction) ==== if (paymentType === 'CASH_ON_DELIVERY') { const totalItems = itemsWithTax .reduce((sum, item) => sum + item.qty, 0) From c173767380b46ff96615b6c65f0908ad9a266257 Mon Sep 17 00:00:00 2001 From: Fathima Sayeeda Date: Fri, 15 Aug 2025 10:40:15 +0530 Subject: [PATCH 06/32] fix: Alter Service for StockBalances of Multiple Items --- .../stocks/getStockBalance/service.ts | 68 ++++++++++++------- .../services/stocks/getStockBalance/types.ts | 10 ++- 2 files changed, 52 insertions(+), 26 deletions(-) diff --git a/libs/items/src/services/stocks/getStockBalance/service.ts b/libs/items/src/services/stocks/getStockBalance/service.ts index f1ab6515..48578268 100644 --- a/libs/items/src/services/stocks/getStockBalance/service.ts +++ b/libs/items/src/services/stocks/getStockBalance/service.ts @@ -1,29 +1,49 @@ -import { getPrismaClient } from '@vestido-ecommerce/models'; +import type { PrismaTransactionalClient } from '@vestido-ecommerce/models'; -import { getStockBalanceSchema, getStockBalanceSchemaType } from './zod'; +import { StockBalanceRow } from './types'; +import { getStockBalanceSchemaType } from './zod'; -export async function getStockBalance(data: getStockBalanceSchemaType) { - const prisma = getPrismaClient(); +export async function getStockBalances( + prisma: PrismaTransactionalClient, + items: getStockBalanceSchemaType[], +) { + const itemIdsOnly = items + .filter((i) => i.itemVariantId === null) + .map((i) => i.itemId); + const variantIdsOnly = items + .filter((i) => i.itemVariantId !== null) + .map((i) => i.itemVariantId!); - const validatedData = getStockBalanceSchema.parse(data); + const rows: StockBalanceRow[] = await prisma.$queryRawUnsafe( + ` + ( + SELECT "id" AS "itemId", + NULL AS "itemVariantId", + "stockBalance" AS balance + FROM "Item" + WHERE "id" = ANY($1::uuid[]) + ) + UNION ALL + ( + SELECT "itemId", + "id" AS "itemVariantId", + "stockBalance" AS balance + FROM "ItemVariant" + WHERE "id" = ANY($2::uuid[]) + ) + FOR UPDATE + `, + itemIdsOnly, + variantIdsOnly, + ); - let latestStockBalanceDetails; - - if (validatedData.itemVariantId === null) { - latestStockBalanceDetails = await prisma.stockLedger.findFirst({ - where: { itemId: validatedData.itemId }, - orderBy: { createdAt: 'desc' }, - select: { itemId: true, itemVariantId: true, balance: true }, - }); - } else { - latestStockBalanceDetails = await prisma.stockLedger.findFirst({ - where: { - itemId: validatedData.itemId, - itemVariantId: validatedData.itemVariantId, - }, - orderBy: { createdAt: 'desc' }, - select: { itemId: true, itemVariantId: true, balance: true }, - }); - } - return latestStockBalanceDetails; + return rows.map((row) => ({ + latestStockBalanceDetails: row, + stockStatus: + row.balance <= 0 + ? 'OUT_OF_STOCK' + : row.balance < 20 + ? 'LIMITED_STOCK' + : 'AVAILABLE', + })); } diff --git a/libs/items/src/services/stocks/getStockBalance/types.ts b/libs/items/src/services/stocks/getStockBalance/types.ts index 004470cd..bbabbf8c 100644 --- a/libs/items/src/services/stocks/getStockBalance/types.ts +++ b/libs/items/src/services/stocks/getStockBalance/types.ts @@ -1,6 +1,6 @@ import { VestidoResponse } from '@vestido-ecommerce/utils'; -import { getStockBalance } from './service'; +import { getStockBalances } from './service'; import { getStockBalanceSchemaType } from './zod'; export type getStockBalanceRequest = { @@ -8,8 +8,14 @@ export type getStockBalanceRequest = { }; export type getStockBalanceResponse = { - data: Awaited>; + data: Awaited>; }; export type getStockBalanceSWRResponse = VestidoResponse; + +export type StockBalanceRow = { + itemId: string; + itemVariantId: string | null; + balance: number; +}; From 87e6c35dedc60879fb412eea88bc0384cd0ae228 Mon Sep 17 00:00:00 2001 From: Fathima Sayeeda Date: Fri, 15 Aug 2025 10:41:30 +0530 Subject: [PATCH 07/32] fix: Alter ReseerveInventory to take StockBalances of Multiple Items --- .../stocks/reserveInventory/service.ts | 55 ++++++++----------- 1 file changed, 22 insertions(+), 33 deletions(-) diff --git a/libs/items/src/services/stocks/reserveInventory/service.ts b/libs/items/src/services/stocks/reserveInventory/service.ts index 4e9d0770..7ac49d9a 100644 --- a/libs/items/src/services/stocks/reserveInventory/service.ts +++ b/libs/items/src/services/stocks/reserveInventory/service.ts @@ -1,77 +1,66 @@ import type { PrismaTransactionalClient } from '@vestido-ecommerce/models'; import { VestidoError } from '@vestido-ecommerce/utils'; +import { StockBalanceRow } from '../getStockBalance/types'; import { reserveInventorySchema, reserveInventorySchemaType } from './zod'; -type StockRow = { stockBalance: number }; - export async function reserveInventory( prisma: PrismaTransactionalClient, data: reserveInventorySchemaType, + stockBalances: StockBalanceRow[], ) { const validatedData = reserveInventorySchema.parse(data); - for (const item of validatedData.items) { - let currentStock: StockRow | undefined; - - if (item.itemVariantId === null) { - [currentStock] = await prisma.$queryRaw` - SELECT "stockBalance" - FROM "Item" - WHERE "id" = ${item.itemId} - FOR UPDATE; - `; - } else { - [currentStock] = await prisma.$queryRaw` - SELECT "stockBalance" - FROM "ItemVariant" - WHERE "id" = ${item.itemVariantId} - FOR UPDATE; - `; - } + for (const inventoryItem of validatedData.items) { + const stockRow = stockBalances.find( + (s) => + s.itemId === inventoryItem.itemId && + s.itemVariantId === inventoryItem.itemVariantId, + ); - if (!currentStock) { + if (!stockRow) { throw new VestidoError({ name: 'Item Not Found', - message: `Item ${item.itemId} not found`, + message: `Item ${inventoryItem.itemId} not found`, httpStatus: 400, }); } - if (currentStock.stockBalance < item.qty) { + if (stockRow.balance < inventoryItem.qty) { throw new VestidoError({ name: 'Not Enough Stock', - message: `Not enough stock to reserve for item ${item.itemId}`, + message: `Not enough stock to reserve for item ${inventoryItem.itemId}`, httpStatus: 400, }); } + const newBalance = stockRow.balance - inventoryItem.qty; - if (item.itemVariantId === null) { + if (inventoryItem.itemVariantId === null) { await prisma.item.update({ where: { - id: item.itemId, + id: inventoryItem.itemId, }, data: { - stockBalance: currentStock.stockBalance - item.qty, + stockBalance: newBalance, }, }); } else { await prisma.itemVariant.update({ where: { - id: item.itemVariantId, + id: inventoryItem.itemVariantId, }, data: { - stockBalance: currentStock.stockBalance - item.qty, + stockBalance: newBalance, }, }); } await prisma.stockLedger.create({ data: { - qty: item.qty, - balance: currentStock.stockBalance - item.qty, - itemId: item.itemId, - itemVariantId: item.itemVariantId ?? '', + qty: inventoryItem.qty, + balance: newBalance, + itemId: inventoryItem.itemId, + itemVariantId: inventoryItem.itemVariantId ?? '', remarks: validatedData.remarks, refId: validatedData.refId, }, From f8b218954cc3a4d7aa294ffdea38fe122139352b Mon Sep 17 00:00:00 2001 From: Fathima Sayeeda Date: Fri, 15 Aug 2025 10:42:06 +0530 Subject: [PATCH 08/32] feat: ReleaseInventory Service --- libs/items/src/services/index.ts | 3 +- libs/items/src/services/stocks/index.ts | 1 + .../services/stocks/releaseInventory/index.ts | 3 + .../stocks/releaseInventory/service.ts | 61 +++++++++++++++++++ .../services/stocks/releaseInventory/types.ts | 10 +++ .../services/stocks/releaseInventory/zod.ts | 15 +++++ 6 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 libs/items/src/services/stocks/releaseInventory/index.ts create mode 100644 libs/items/src/services/stocks/releaseInventory/service.ts create mode 100644 libs/items/src/services/stocks/releaseInventory/types.ts create mode 100644 libs/items/src/services/stocks/releaseInventory/zod.ts diff --git a/libs/items/src/services/index.ts b/libs/items/src/services/index.ts index 28de229a..3dec5a0c 100644 --- a/libs/items/src/services/index.ts +++ b/libs/items/src/services/index.ts @@ -9,8 +9,7 @@ export * from './cart/get-cart'; export * from './cart/remove-from-cart'; export * from './categories'; export * from './items'; -export * from './stocks/reconcileInventory'; -export * from './stocks/reserveInventory'; +export * from './stocks'; export * from './variants'; export * from './wishlist/add-to-wishlist'; export * from './wishlist/get-wishlist'; diff --git a/libs/items/src/services/stocks/index.ts b/libs/items/src/services/stocks/index.ts index 699eff27..9a3197d6 100644 --- a/libs/items/src/services/stocks/index.ts +++ b/libs/items/src/services/stocks/index.ts @@ -1,3 +1,4 @@ export * from './getStockBalance'; export * from './reconcileInventory'; +export * from './releaseInventory'; export * from './reserveInventory'; diff --git a/libs/items/src/services/stocks/releaseInventory/index.ts b/libs/items/src/services/stocks/releaseInventory/index.ts new file mode 100644 index 00000000..8a9411ad --- /dev/null +++ b/libs/items/src/services/stocks/releaseInventory/index.ts @@ -0,0 +1,3 @@ +export * from './service'; +export * from './types'; +export * from './zod'; diff --git a/libs/items/src/services/stocks/releaseInventory/service.ts b/libs/items/src/services/stocks/releaseInventory/service.ts new file mode 100644 index 00000000..1b10acf5 --- /dev/null +++ b/libs/items/src/services/stocks/releaseInventory/service.ts @@ -0,0 +1,61 @@ +import type { PrismaTransactionalClient } from '@vestido-ecommerce/models'; +import { VestidoError } from '@vestido-ecommerce/utils'; + +import { StockBalanceRow } from '../getStockBalance'; +import { releaseInventorySchema, releaseInventorySchemaType } from './zod'; + +export async function releaseInventory( + prisma: PrismaTransactionalClient, + data: releaseInventorySchemaType, + stockBalances: StockBalanceRow[], +) { + const validatedData = releaseInventorySchema.parse(data); + + for (const inventoryItem of validatedData.items) { + const stockRow = stockBalances.find( + (s) => + s.itemId === inventoryItem.itemId && + s.itemVariantId === inventoryItem.itemVariantId, + ); + if (!stockRow) { + throw new VestidoError({ + name: 'Item Not Found', + message: `Item ${inventoryItem.itemId} not found`, + httpStatus: 400, + }); + } + + const newBalance = stockRow.balance + inventoryItem.qty; + + if (inventoryItem.itemVariantId === null) { + await prisma.item.update({ + where: { + id: inventoryItem.itemId, + }, + data: { + stockBalance: newBalance, + }, + }); + } else { + await prisma.itemVariant.update({ + where: { + id: inventoryItem.itemVariantId, + }, + data: { + stockBalance: newBalance, + }, + }); + } + + await prisma.stockLedger.create({ + data: { + qty: inventoryItem.qty, + balance: newBalance, + itemId: inventoryItem.itemId, + itemVariantId: inventoryItem.itemVariantId ?? '', + remarks: validatedData.remarks, + refId: validatedData.refId, + }, + }); + } +} diff --git a/libs/items/src/services/stocks/releaseInventory/types.ts b/libs/items/src/services/stocks/releaseInventory/types.ts new file mode 100644 index 00000000..758dba78 --- /dev/null +++ b/libs/items/src/services/stocks/releaseInventory/types.ts @@ -0,0 +1,10 @@ +import { Item } from '@prisma/client'; + +import { releaseInventorySchemaType } from './zod'; + +export type releaseInventoryRequest = { + data: releaseInventorySchemaType; +}; +export type releaseInventoryResponse = { + data: Item; +}; diff --git a/libs/items/src/services/stocks/releaseInventory/zod.ts b/libs/items/src/services/stocks/releaseInventory/zod.ts new file mode 100644 index 00000000..b3e47ce3 --- /dev/null +++ b/libs/items/src/services/stocks/releaseInventory/zod.ts @@ -0,0 +1,15 @@ +import { z } from 'zod'; + +export const releaseInventorySchema = z.object({ + refId: z.string().optional(), + items: z.array( + z.object({ + itemId: z.string(), + itemVariantId: z.string().nullish(), + qty: z.coerce.number(), + }), + ), + remarks: z.string(), +}); + +export type releaseInventorySchemaType = z.infer; From 72fa1163b790f027038e8e13dffbbcaf1fecda79 Mon Sep 17 00:00:00 2001 From: Fathima Sayeeda Date: Fri, 15 Aug 2025 10:42:44 +0530 Subject: [PATCH 09/32] fix: ReserveInventory in CreateOrder Service --- .../services/orders/create-order/service.ts | 43 ++++++++++++------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/libs/orders/src/services/orders/create-order/service.ts b/libs/orders/src/services/orders/create-order/service.ts index c8410183..dada8860 100644 --- a/libs/orders/src/services/orders/create-order/service.ts +++ b/libs/orders/src/services/orders/create-order/service.ts @@ -2,7 +2,7 @@ import type { Payment } from '@prisma/client'; import { sendSMS, SMSSenderID, SMSTemplate } from '@vestido-ecommerce/fast2sms'; import { clearCartOnOrderCreation } from '@vestido-ecommerce/items'; -import { reserveInventory } from '@vestido-ecommerce/items'; +import { getStockBalances, reserveInventory } from '@vestido-ecommerce/items'; import { getPrismaClient } from '@vestido-ecommerce/models'; import { VestidoError } from '@vestido-ecommerce/utils'; @@ -36,7 +36,7 @@ export async function createOrder(_data: CreateOrderSchemaType) { }); const { newOrder, newPayment } = await prisma.$transaction(async (prisma) => { - const newOrder = await prisma.order.create({ + const createdOrder = await prisma.order.create({ data: { ...data, createdAt: new Date(), @@ -74,7 +74,7 @@ export async function createOrder(_data: CreateOrderSchemaType) { if (paymentType === 'CASH_ON_DELIVERY') { newPayment = await prisma.payment.create({ data: { - order: { connect: { id: newOrder.id } }, + order: { connect: { id: createdOrder.id } }, paymentGateway: 'CASH_ON_DELIVERY', paymentGatewayRef: 'Null', moreDetails: 'Null', @@ -86,7 +86,7 @@ export async function createOrder(_data: CreateOrderSchemaType) { } else if (paymentType === 'REPLACEMENT_ORDER') { newPayment = await prisma.payment.create({ data: { - order: { connect: { id: newOrder.id } }, + order: { connect: { id: createdOrder.id } }, paymentGateway: 'REPLACEMENT_ORDER', paymentGatewayRef: 'Null', moreDetails: 'Null', @@ -96,18 +96,31 @@ export async function createOrder(_data: CreateOrderSchemaType) { }, }); } + const balances = ( + await getStockBalances( + prisma, + itemsWithTax.map((item) => ({ + itemId: item.itemId, + itemVariantId: item.variantId ?? undefined, + })), + ) + ).map((row) => row.latestStockBalanceDetails); + + await reserveInventory( + prisma, + { + refId: createdOrder.id, + remarks: 'Order Reservation', + items: itemsWithTax.map((item) => ({ + itemId: item.itemId, + itemVariantId: item.variantId ?? null, + qty: item.qty, + })), + }, + balances, + ); - await reserveInventory(prisma, { - refId: newOrder.id, - remarks: 'Order Reservation', - items: itemsWithTax.map((item) => ({ - itemId: item.itemId, - itemVariantId: item.variantId ?? null, - qty: item.qty, - })), - }); - - return { newOrder, newPayment }; + return { newOrder: createdOrder, newPayment }; }); if (paymentType === 'CASH_ON_DELIVERY') { From 6b5648c6d40bf12f35b51e3e4de29560b6fb1c03 Mon Sep 17 00:00:00 2001 From: Fathima Sayeeda Date: Fri, 15 Aug 2025 10:43:04 +0530 Subject: [PATCH 10/32] fix: ReleaseInventory in CancelOrder Service --- libs/orders/src/services/orders/cancel-order/service.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/libs/orders/src/services/orders/cancel-order/service.ts b/libs/orders/src/services/orders/cancel-order/service.ts index 1d9dd162..64ee4e71 100644 --- a/libs/orders/src/services/orders/cancel-order/service.ts +++ b/libs/orders/src/services/orders/cancel-order/service.ts @@ -81,8 +81,6 @@ export async function cancelOrder( refundResponse = await refundRazorpay(refundData); - console.log('Refund Response: ', refundResponse); - if (!refundResponse || refundResponse.status === 'failed') { throw new VestidoError({ name: 'RazorpayRefundFailed', From a69208c922960e7142f4d247b8bd57381b7ea582 Mon Sep 17 00:00:00 2001 From: Fathima Sayeeda Date: Fri, 29 Aug 2025 15:58:43 +0530 Subject: [PATCH 11/32] feat: ReleaseInventory on Order Cancellation --- .../services/orders/cancel-order/service.ts | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/libs/orders/src/services/orders/cancel-order/service.ts b/libs/orders/src/services/orders/cancel-order/service.ts index 64ee4e71..336b8308 100644 --- a/libs/orders/src/services/orders/cancel-order/service.ts +++ b/libs/orders/src/services/orders/cancel-order/service.ts @@ -1,7 +1,9 @@ +import { getStockBalances, releaseInventory } from '@vestido-ecommerce/items'; import { getPrismaClient } from '@vestido-ecommerce/models'; import { refundRazorpay } from '@vestido-ecommerce/razorpay'; import { VestidoError } from '@vestido-ecommerce/utils'; +import { getOrder } from '../get-order'; import { CancelOrderSchema, CancelOrderSchemaType } from './zod'; export async function cancelOrder( @@ -11,15 +13,7 @@ export async function cancelOrder( const { orderId, reason, remarks } = CancelOrderSchema.parse(data); try { - const order = await prisma.order.findFirst({ - where: { - id: orderId, - }, - include: { - payments: true, - }, - }); - + const order = await getOrder(orderId); if (!order) { throw new VestidoError({ name: 'OrderNotFoundError', @@ -131,7 +125,30 @@ export async function cancelOrder( }, }); } - + //release the stock + const balances = ( + await getStockBalances( + prismaTransaction, + order.orderItems.map((item) => ({ + itemId: item.itemId, + itemVariantId: item.variantId ?? undefined, + })), + ) + ).map((row) => row.latestStockBalanceDetails); + + await releaseInventory( + prismaTransaction, + { + refId: order.id, + remarks: 'Stock Release on Order Cancellation', + items: order.orderItems.map((item) => ({ + itemId: item.itemId, + itemVariantId: item.variantId ?? null, + qty: item.qty, + })), + }, + balances, + ); // Update the order status await prismaTransaction.order.update({ where: { id: orderId }, From c74395cadbf13d769af7162c2a2d648af6b5aaf5 Mon Sep 17 00:00:00 2001 From: Fathima Sayeeda Date: Fri, 29 Aug 2025 15:59:05 +0530 Subject: [PATCH 12/32] feat: ReleaseInventory on Payment Failure --- .../src/services/razorpay-webhook/service.ts | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/third-party/razorpay/src/services/razorpay-webhook/service.ts b/third-party/razorpay/src/services/razorpay-webhook/service.ts index 58355b6c..a7ea90b2 100644 --- a/third-party/razorpay/src/services/razorpay-webhook/service.ts +++ b/third-party/razorpay/src/services/razorpay-webhook/service.ts @@ -1,3 +1,4 @@ +import { getStockBalances, releaseInventory } from '@vestido-ecommerce/items'; import { getPrismaClient } from '@vestido-ecommerce/models'; import { VestidoError } from '@vestido-ecommerce/utils'; @@ -123,6 +124,22 @@ export async function handleRazorpayWebhook(data: RazorpayWebhookSchemaType) { }); } + const order = await transaction.order.findUnique({ + where: { id: pRow.orderId }, + include: { + orderItems: true, + }, + }); + + if (!order) { + throw new VestidoError({ + name: 'NotFoundError', + message: 'Order not found for releasing inventory', + httpStatus: 404, + context: { orderId: pRow.orderId }, + }); + } + // Update all related payment entries to 'FAILED' await transaction.payment.updateMany({ where: { @@ -132,6 +149,31 @@ export async function handleRazorpayWebhook(data: RazorpayWebhookSchemaType) { }); console.log('Payments updated to FAILED.'); + //release the stock + const balances = ( + await getStockBalances( + transaction, + order.orderItems.map((item) => ({ + itemId: item.itemId, + itemVariantId: item.variantId ?? undefined, + })), + ) + ).map((row) => row.latestStockBalanceDetails); + + await releaseInventory( + transaction, + { + refId: order.id, + remarks: 'Stock Release on Order Cancellation', + items: order.orderItems.map((item) => ({ + itemId: item.itemId, + itemVariantId: item.variantId ?? null, + qty: item.qty, + })), + }, + balances, + ); + // Update each related order to 'CANCELLED' await transaction.order.updateMany({ where: { id: pRow.id }, From 71f6b08cc4e77b23c582a1cdd6850670a58b2138 Mon Sep 17 00:00:00 2001 From: Fathima Sayeeda Date: Mon, 8 Sep 2025 11:19:03 +0530 Subject: [PATCH 13/32] feat(wip): BE Stock Updates --- .../src/services/items/get-item/service.ts | 1 + .../stocks/getStockBalance/service.ts | 93 ++++++++++++------- .../stocks/reconcileInventory/types.ts | 9 +- .../services/stocks/reconcileInventory/zod.ts | 2 +- libs/items/src/swr/index.ts | 1 + libs/items/src/swr/stocks/index.ts | 1 + .../swr/stocks/reconcileInventory/index.ts | 2 + .../reconcileInventory/reconcile-inventory.ts | 18 ++++ .../swr/stocks/reconcileInventory/service.ts | 29 ++++++ .../app/api/items/[item_id]/route.ts | 14 ++- .../app/api/items/[item_id]/variants/route.ts | 29 +++++- 11 files changed, 159 insertions(+), 40 deletions(-) create mode 100644 libs/items/src/swr/stocks/reconcileInventory/index.ts create mode 100644 libs/items/src/swr/stocks/reconcileInventory/reconcile-inventory.ts create mode 100644 libs/items/src/swr/stocks/reconcileInventory/service.ts diff --git a/libs/items/src/services/items/get-item/service.ts b/libs/items/src/services/items/get-item/service.ts index a8009f69..5fcfad6f 100644 --- a/libs/items/src/services/items/get-item/service.ts +++ b/libs/items/src/services/items/get-item/service.ts @@ -46,5 +46,6 @@ export async function getItemDetails( await populateImageURLs(item?.images as ImageSchemaType[]); + // TODO: Insert Stock Balance at the root / individual variants return item; } diff --git a/libs/items/src/services/stocks/getStockBalance/service.ts b/libs/items/src/services/stocks/getStockBalance/service.ts index 48578268..b28cf0d3 100644 --- a/libs/items/src/services/stocks/getStockBalance/service.ts +++ b/libs/items/src/services/stocks/getStockBalance/service.ts @@ -1,49 +1,78 @@ import type { PrismaTransactionalClient } from '@vestido-ecommerce/models'; +import { PrismaClient } from '@vestido-ecommerce/models'; import { StockBalanceRow } from './types'; import { getStockBalanceSchemaType } from './zod'; +type StockBalanceResult = { + latestStockBalanceDetails: StockBalanceRow; + stockStatus: 'OUT_OF_STOCK' | 'LIMITED_STOCK' | 'AVAILABLE'; +}; + export async function getStockBalances( - prisma: PrismaTransactionalClient, + prisma: PrismaClient | PrismaTransactionalClient, items: getStockBalanceSchemaType[], -) { +): Promise> { const itemIdsOnly = items - .filter((i) => i.itemVariantId === null) + .filter((i) => !i.itemVariantId) .map((i) => i.itemId); const variantIdsOnly = items - .filter((i) => i.itemVariantId !== null) + .filter((i) => !!i.itemVariantId) .map((i) => i.itemVariantId!); - const rows: StockBalanceRow[] = await prisma.$queryRawUnsafe( - ` - ( - SELECT "id" AS "itemId", + console.info({ + itemIdsOnly, + variantIdsOnly, + }); + + const rows: StockBalanceRow[] = []; + if (itemIdsOnly.length > 0) { + rows.push( + ...((await prisma.$queryRawUnsafe( + ` + SELECT + "id" AS "itemId", NULL AS "itemVariantId", "stockBalance" AS balance FROM "Item" WHERE "id" = ANY($1::uuid[]) - ) - UNION ALL - ( - SELECT "itemId", - "id" AS "itemVariantId", - "stockBalance" AS balance - FROM "ItemVariant" - WHERE "id" = ANY($2::uuid[]) - ) - FOR UPDATE - `, - itemIdsOnly, - variantIdsOnly, - ); - - return rows.map((row) => ({ - latestStockBalanceDetails: row, - stockStatus: - row.balance <= 0 - ? 'OUT_OF_STOCK' - : row.balance < 20 - ? 'LIMITED_STOCK' - : 'AVAILABLE', - })); + FOR UPDATE + `, + itemIdsOnly, + )) as StockBalanceRow[]), + ); + } + + if (variantIdsOnly.length > 0) { + rows.push( + ...((await prisma.$queryRawUnsafe( + ` + SELECT "itemId", + "id" AS "itemVariantId", + "stockBalance" AS balance + FROM "ItemVariant" + WHERE "id" = ANY($1::uuid[]) + FOR UPDATE + `, + variantIdsOnly, + )) as StockBalanceRow[]), + ); + } + + const result: Record = {}; + + for (const row of rows) { + const key = row.itemVariantId ?? row.itemId; + result[key] = { + latestStockBalanceDetails: row, + stockStatus: + row.balance <= 0 + ? 'OUT_OF_STOCK' + : row.balance < 20 + ? 'LIMITED_STOCK' + : 'AVAILABLE', + }; + } + + return result; } diff --git a/libs/items/src/services/stocks/reconcileInventory/types.ts b/libs/items/src/services/stocks/reconcileInventory/types.ts index 1b0db554..5713b0ec 100644 --- a/libs/items/src/services/stocks/reconcileInventory/types.ts +++ b/libs/items/src/services/stocks/reconcileInventory/types.ts @@ -1,10 +1,13 @@ import { Item } from '@prisma/client'; +import { VestidoResponse } from '@vestido-ecommerce/utils'; + import { stockUpdateSchemaType } from './zod'; -export type stockUpdateRequest = { - data: stockUpdateSchemaType; -}; +export type stockUpdateRequest = stockUpdateSchemaType; + export type stockUpdateResponse = { data: Item; }; + +export type stockUpdateSWRResponse = VestidoResponse; diff --git a/libs/items/src/services/stocks/reconcileInventory/zod.ts b/libs/items/src/services/stocks/reconcileInventory/zod.ts index 1e06af04..3c00b8b0 100644 --- a/libs/items/src/services/stocks/reconcileInventory/zod.ts +++ b/libs/items/src/services/stocks/reconcileInventory/zod.ts @@ -5,7 +5,7 @@ export const stockUpdateSchema = z.object({ itemId: z.string(), itemVariantId: z.string().optional(), qty: z.coerce.number(), - remarks: z.string(), + remarks: z.string().nullish(), }); export type stockUpdateSchemaType = z.infer; diff --git a/libs/items/src/swr/index.ts b/libs/items/src/swr/index.ts index 5b6299fa..1c9239c3 100644 --- a/libs/items/src/swr/index.ts +++ b/libs/items/src/swr/index.ts @@ -2,5 +2,6 @@ export * from './attributes'; export * from './cart'; export * from './category'; export * from './item'; +export * from './stocks'; export * from './variants'; export * from './wishlist'; diff --git a/libs/items/src/swr/stocks/index.ts b/libs/items/src/swr/stocks/index.ts index 1b494318..d0d5f5fb 100644 --- a/libs/items/src/swr/stocks/index.ts +++ b/libs/items/src/swr/stocks/index.ts @@ -1,3 +1,4 @@ 'use client'; export * from './getStockBalance'; +export * from './reconcileInventory'; diff --git a/libs/items/src/swr/stocks/reconcileInventory/index.ts b/libs/items/src/swr/stocks/reconcileInventory/index.ts new file mode 100644 index 00000000..e40a196f --- /dev/null +++ b/libs/items/src/swr/stocks/reconcileInventory/index.ts @@ -0,0 +1,2 @@ +export * from './reconcile-inventory'; +export * from './service'; diff --git a/libs/items/src/swr/stocks/reconcileInventory/reconcile-inventory.ts b/libs/items/src/swr/stocks/reconcileInventory/reconcile-inventory.ts new file mode 100644 index 00000000..4a0da8d4 --- /dev/null +++ b/libs/items/src/swr/stocks/reconcileInventory/reconcile-inventory.ts @@ -0,0 +1,18 @@ +import useSWRMutation from 'swr/mutation' +import { useAuth } from '@vestido-ecommerce/auth/client' + +import { + stockUpdateRequest, + stockUpdateResponse, +} from 'libs/items/src/services/stocks' +import { StockSWRKeys } from '../keys' +import { reconcileInventory } from './service' + +export function useReconcileInventory() { + const { authHeaders } = useAuth() + + return useSWRMutation( + StockSWRKeys.RECONCILE, + (_key, { arg }) => reconcileInventory(arg, authHeaders) + ) +} diff --git a/libs/items/src/swr/stocks/reconcileInventory/service.ts b/libs/items/src/swr/stocks/reconcileInventory/service.ts new file mode 100644 index 00000000..820688f8 --- /dev/null +++ b/libs/items/src/swr/stocks/reconcileInventory/service.ts @@ -0,0 +1,29 @@ +import { handleVestidoErrorResponse } from '@vestido-ecommerce/utils'; + +import { + stockUpdateRequest, + stockUpdateResponse, +} from 'libs/items/src/services/stocks'; + +export async function reconcileInventory( + args: stockUpdateRequest, + headers?: Record, +): Promise { + const url = `/api/stocks/update`; + + const r = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(headers ?? {}), + }, + body: JSON.stringify(args), + }); + + if (!r.ok) { + await handleVestidoErrorResponse(r); + } + + const data = await r.json(); + return data as stockUpdateResponse; +} diff --git a/vestido/vestido-dashboard/app/api/items/[item_id]/route.ts b/vestido/vestido-dashboard/app/api/items/[item_id]/route.ts index eaad0414..b7ba44b4 100644 --- a/vestido/vestido-dashboard/app/api/items/[item_id]/route.ts +++ b/vestido/vestido-dashboard/app/api/items/[item_id]/route.ts @@ -2,6 +2,7 @@ import { authMiddleware, roleMiddleware } from '@vestido-ecommerce/auth'; import { deleteItem, getItemDetails, + reconcileInventory, updateItem, } from '@vestido-ecommerce/items'; import { apiRouteHandler } from '@vestido-ecommerce/utils'; @@ -11,7 +12,8 @@ export const GET = apiRouteHandler( roleMiddleware('ADMIN'), async ({ params }) => { const item = await getItemDetails(params.item_id); - return item; + + return { item }; }, ); @@ -20,7 +22,15 @@ export const PUT = apiRouteHandler( roleMiddleware('ADMIN'), async ({ request, params: { item_id } }) => { const body = await request.json(); - return await updateItem(item_id, body); + const updated_item = await updateItem(item_id, body); + + // TODO: Rethinink about this approach + const updated_stock_balance = await reconcileInventory({ + ...body, + itemId: item_id, + }); + + return { updated_item, updated_stock_balance }; }, ); diff --git a/vestido/vestido-dashboard/app/api/items/[item_id]/variants/route.ts b/vestido/vestido-dashboard/app/api/items/[item_id]/variants/route.ts index 65923912..ab92a8e8 100644 --- a/vestido/vestido-dashboard/app/api/items/[item_id]/variants/route.ts +++ b/vestido/vestido-dashboard/app/api/items/[item_id]/variants/route.ts @@ -1,13 +1,25 @@ import { authMiddleware, roleMiddleware } from '@vestido-ecommerce/auth'; -import { createVariant, listVariants } from '@vestido-ecommerce/items'; +import { + createVariant, + getStockBalances, + listVariants, + reconcileInventory, +} from '@vestido-ecommerce/items'; +import { getPrismaClient } from '@vestido-ecommerce/models'; import { apiRouteHandler } from '@vestido-ecommerce/utils'; export const GET = apiRouteHandler( authMiddleware, roleMiddleware('ADMIN'), async ({ params }) => { + const prisma = getPrismaClient(); const variants = await listVariants(params.item_id); - return variants; + + // TODO: Revisit + const stockBalance = await getStockBalances(prisma, [ + { itemId: params.item_id, itemVariantId: params.variant_id }, + ]); + return { variants, stockBalance }; }, ); @@ -20,3 +32,16 @@ export const POST = apiRouteHandler( return newVariant; }, ); + +export const PUT = apiRouteHandler( + authMiddleware, + roleMiddleware('ADMIN'), + async ({ request, params }) => { + const body = await request.json(); + return await reconcileInventory({ + ...body, + itemId: params.item_id, + itemVariantId: params.variant_id, + }); + }, +); From 65b44887d3cf2914fcd94a2d78827ba60d40588b Mon Sep 17 00:00:00 2001 From: Fahim Ali Zain Date: Mon, 8 Sep 2025 18:10:52 +0530 Subject: [PATCH 14/32] feat: Update Stock Services --- .gitignore | 3 +- .nvmrc | 2 +- .../src/services/items/get-item/service.ts | 1 - .../index.ts | 0 .../service.ts | 27 ++++--- .../types.ts | 4 +- .../services/stocks/get-stock-balance/zod.ts | 10 +++ .../services/stocks/get-stock-status/index.ts | 1 + .../stocks/get-stock-status/service.ts | 10 +++ .../services/stocks/getStockBalance/zod.ts | 8 --- libs/items/src/services/stocks/index.ts | 8 +-- .../index.ts | 0 .../stocks/reconcile-stock/service.ts | 70 +++++++++++++++++++ .../services/stocks/reconcile-stock/types.ts | 13 ++++ .../zod.ts | 4 +- .../stocks/reconcileInventory/service.ts | 46 ------------ .../stocks/reconcileInventory/types.ts | 13 ---- .../index.ts | 0 .../service.ts | 13 ++-- .../services/stocks/release-stock/types.ts | 10 +++ .../zod.ts | 4 +- .../services/stocks/releaseInventory/types.ts | 10 --- .../index.ts | 0 .../service.ts | 13 ++-- .../services/stocks/reserve-stock/types.ts | 10 +++ .../zod.ts | 4 +- .../services/stocks/reserveInventory/types.ts | 10 --- libs/items/src/swr/stocks/index.ts | 2 +- .../swr/stocks/reconcileInventory/index.ts | 2 - .../reconcileInventory/reconcile-inventory.ts | 18 ----- .../src/swr/stocks/reconcileStock/index.ts | 2 + .../stocks/reconcileStock/reconcile-stock.ts | 18 +++++ .../service.ts | 16 ++--- .../migrations/20250908123517_/migration.sql | 8 +++ libs/models/prisma/schema.prisma | 12 ++-- .../services/orders/cancel-order/service.ts | 10 +-- .../services/orders/create-order/service.ts | 10 +-- .../src/services/razorpay-webhook/service.ts | 10 +-- .../app/api/items/[item_id]/route.ts | 14 +--- .../app/api/items/[item_id]/stock/route.ts | 18 +++++ .../app/api/items/[item_id]/variants/route.ts | 4 +- 41 files changed, 247 insertions(+), 191 deletions(-) rename libs/items/src/services/stocks/{getStockBalance => get-stock-balance}/index.ts (100%) rename libs/items/src/services/stocks/{getStockBalance => get-stock-balance}/service.ts (78%) rename libs/items/src/services/stocks/{getStockBalance => get-stock-balance}/types.ts (82%) create mode 100644 libs/items/src/services/stocks/get-stock-balance/zod.ts create mode 100644 libs/items/src/services/stocks/get-stock-status/index.ts create mode 100644 libs/items/src/services/stocks/get-stock-status/service.ts delete mode 100644 libs/items/src/services/stocks/getStockBalance/zod.ts rename libs/items/src/services/stocks/{reconcileInventory => reconcile-stock}/index.ts (100%) create mode 100644 libs/items/src/services/stocks/reconcile-stock/service.ts create mode 100644 libs/items/src/services/stocks/reconcile-stock/types.ts rename libs/items/src/services/stocks/{reconcileInventory => reconcile-stock}/zod.ts (59%) delete mode 100644 libs/items/src/services/stocks/reconcileInventory/service.ts delete mode 100644 libs/items/src/services/stocks/reconcileInventory/types.ts rename libs/items/src/services/stocks/{releaseInventory => release-stock}/index.ts (100%) rename libs/items/src/services/stocks/{releaseInventory => release-stock}/service.ts (77%) create mode 100644 libs/items/src/services/stocks/release-stock/types.ts rename libs/items/src/services/stocks/{releaseInventory => release-stock}/zod.ts (63%) delete mode 100644 libs/items/src/services/stocks/releaseInventory/types.ts rename libs/items/src/services/stocks/{reserveInventory => reserve-stock}/index.ts (100%) rename libs/items/src/services/stocks/{reserveInventory => reserve-stock}/service.ts (80%) create mode 100644 libs/items/src/services/stocks/reserve-stock/types.ts rename libs/items/src/services/stocks/{reserveInventory => reserve-stock}/zod.ts (63%) delete mode 100644 libs/items/src/services/stocks/reserveInventory/types.ts delete mode 100644 libs/items/src/swr/stocks/reconcileInventory/index.ts delete mode 100644 libs/items/src/swr/stocks/reconcileInventory/reconcile-inventory.ts create mode 100644 libs/items/src/swr/stocks/reconcileStock/index.ts create mode 100644 libs/items/src/swr/stocks/reconcileStock/reconcile-stock.ts rename libs/items/src/swr/stocks/{reconcileInventory => reconcileStock}/service.ts (60%) create mode 100644 libs/models/prisma/migrations/20250908123517_/migration.sql create mode 100644 vestido/vestido-dashboard/app/api/items/[item_id]/stock/route.ts diff --git a/.gitignore b/.gitignore index bc873b63..6516c292 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ # Environment Files .env +.env.* # compiled output dist @@ -45,4 +46,4 @@ Thumbs.db .nx/cache # Next.js -.next \ No newline at end of file +.next diff --git a/.nvmrc b/.nvmrc index 0a47c855..209e3ef4 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -lts/iron \ No newline at end of file +20 diff --git a/libs/items/src/services/items/get-item/service.ts b/libs/items/src/services/items/get-item/service.ts index 5fcfad6f..a8009f69 100644 --- a/libs/items/src/services/items/get-item/service.ts +++ b/libs/items/src/services/items/get-item/service.ts @@ -46,6 +46,5 @@ export async function getItemDetails( await populateImageURLs(item?.images as ImageSchemaType[]); - // TODO: Insert Stock Balance at the root / individual variants return item; } diff --git a/libs/items/src/services/stocks/getStockBalance/index.ts b/libs/items/src/services/stocks/get-stock-balance/index.ts similarity index 100% rename from libs/items/src/services/stocks/getStockBalance/index.ts rename to libs/items/src/services/stocks/get-stock-balance/index.ts diff --git a/libs/items/src/services/stocks/getStockBalance/service.ts b/libs/items/src/services/stocks/get-stock-balance/service.ts similarity index 78% rename from libs/items/src/services/stocks/getStockBalance/service.ts rename to libs/items/src/services/stocks/get-stock-balance/service.ts index b28cf0d3..d7c08acb 100644 --- a/libs/items/src/services/stocks/getStockBalance/service.ts +++ b/libs/items/src/services/stocks/get-stock-balance/service.ts @@ -1,17 +1,21 @@ import type { PrismaTransactionalClient } from '@vestido-ecommerce/models'; import { PrismaClient } from '@vestido-ecommerce/models'; +import { getStockStatus } from '../get-stock-status'; import { StockBalanceRow } from './types'; -import { getStockBalanceSchemaType } from './zod'; +import { GetStockBalanceInputSchemaType } from './zod'; type StockBalanceResult = { - latestStockBalanceDetails: StockBalanceRow; + itemId: string; + itemVariantId: string | null; + stockBalance: number; stockStatus: 'OUT_OF_STOCK' | 'LIMITED_STOCK' | 'AVAILABLE'; + _stockBalanceRow: StockBalanceRow; }; export async function getStockBalances( prisma: PrismaClient | PrismaTransactionalClient, - items: getStockBalanceSchemaType[], + items: GetStockBalanceInputSchemaType[], ): Promise> { const itemIdsOnly = items .filter((i) => !i.itemVariantId) @@ -20,11 +24,6 @@ export async function getStockBalances( .filter((i) => !!i.itemVariantId) .map((i) => i.itemVariantId!); - console.info({ - itemIdsOnly, - variantIdsOnly, - }); - const rows: StockBalanceRow[] = []; if (itemIdsOnly.length > 0) { rows.push( @@ -64,13 +63,11 @@ export async function getStockBalances( for (const row of rows) { const key = row.itemVariantId ?? row.itemId; result[key] = { - latestStockBalanceDetails: row, - stockStatus: - row.balance <= 0 - ? 'OUT_OF_STOCK' - : row.balance < 20 - ? 'LIMITED_STOCK' - : 'AVAILABLE', + itemId: row.itemId, + itemVariantId: row.itemVariantId, + stockBalance: row.balance, + stockStatus: getStockStatus(row.balance), + _stockBalanceRow: row, }; } diff --git a/libs/items/src/services/stocks/getStockBalance/types.ts b/libs/items/src/services/stocks/get-stock-balance/types.ts similarity index 82% rename from libs/items/src/services/stocks/getStockBalance/types.ts rename to libs/items/src/services/stocks/get-stock-balance/types.ts index bbabbf8c..8fe5f75e 100644 --- a/libs/items/src/services/stocks/getStockBalance/types.ts +++ b/libs/items/src/services/stocks/get-stock-balance/types.ts @@ -1,10 +1,10 @@ import { VestidoResponse } from '@vestido-ecommerce/utils'; import { getStockBalances } from './service'; -import { getStockBalanceSchemaType } from './zod'; +import { GetStockBalanceInputSchemaType } from './zod'; export type getStockBalanceRequest = { - data: getStockBalanceSchemaType; + data: GetStockBalanceInputSchemaType; }; export type getStockBalanceResponse = { diff --git a/libs/items/src/services/stocks/get-stock-balance/zod.ts b/libs/items/src/services/stocks/get-stock-balance/zod.ts new file mode 100644 index 00000000..46f6c55e --- /dev/null +++ b/libs/items/src/services/stocks/get-stock-balance/zod.ts @@ -0,0 +1,10 @@ +import { z } from 'zod'; + +export const GetStockBalanceInputSchema = z.object({ + itemId: z.string(), + itemVariantId: z.string().optional(), +}); + +export type GetStockBalanceInputSchemaType = z.infer< + typeof GetStockBalanceInputSchema +>; diff --git a/libs/items/src/services/stocks/get-stock-status/index.ts b/libs/items/src/services/stocks/get-stock-status/index.ts new file mode 100644 index 00000000..f78beabc --- /dev/null +++ b/libs/items/src/services/stocks/get-stock-status/index.ts @@ -0,0 +1 @@ +export * from './service'; diff --git a/libs/items/src/services/stocks/get-stock-status/service.ts b/libs/items/src/services/stocks/get-stock-status/service.ts new file mode 100644 index 00000000..c1e9a5b2 --- /dev/null +++ b/libs/items/src/services/stocks/get-stock-status/service.ts @@ -0,0 +1,10 @@ +import { StockStatus } from '@prisma/client'; + +export function getStockStatus(stockBalance: number): StockStatus { + if (stockBalance > 20) { + return 'AVAILABLE'; + } else if (stockBalance > 0) { + return 'LIMITED_STOCK'; + } + return 'OUT_OF_STOCK'; +} diff --git a/libs/items/src/services/stocks/getStockBalance/zod.ts b/libs/items/src/services/stocks/getStockBalance/zod.ts deleted file mode 100644 index 64d2d20b..00000000 --- a/libs/items/src/services/stocks/getStockBalance/zod.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { z } from 'zod'; - -export const getStockBalanceSchema = z.object({ - itemId: z.string(), - itemVariantId: z.string().optional(), -}); - -export type getStockBalanceSchemaType = z.infer; diff --git a/libs/items/src/services/stocks/index.ts b/libs/items/src/services/stocks/index.ts index 9a3197d6..fbf6cc42 100644 --- a/libs/items/src/services/stocks/index.ts +++ b/libs/items/src/services/stocks/index.ts @@ -1,4 +1,4 @@ -export * from './getStockBalance'; -export * from './reconcileInventory'; -export * from './releaseInventory'; -export * from './reserveInventory'; +export * from './get-stock-balance'; +export * from './reconcile-stock'; +export * from './release-stock'; +export * from './reserve-stock'; diff --git a/libs/items/src/services/stocks/reconcileInventory/index.ts b/libs/items/src/services/stocks/reconcile-stock/index.ts similarity index 100% rename from libs/items/src/services/stocks/reconcileInventory/index.ts rename to libs/items/src/services/stocks/reconcile-stock/index.ts diff --git a/libs/items/src/services/stocks/reconcile-stock/service.ts b/libs/items/src/services/stocks/reconcile-stock/service.ts new file mode 100644 index 00000000..042cd1cf --- /dev/null +++ b/libs/items/src/services/stocks/reconcile-stock/service.ts @@ -0,0 +1,70 @@ +import { getPrismaClient } from '@vestido-ecommerce/models'; + +import { getStockBalances } from '../get-stock-balance'; +import { getStockStatus } from '../get-stock-status'; +import { ReconcileStockSchema } from './zod'; +import { ReconcileStockSchemaType } from './zod'; + +export async function reconcileStock(data: ReconcileStockSchemaType) { + const prisma = getPrismaClient(); + const validatedData = ReconcileStockSchema.parse(data); + + await prisma.$transaction(async (prisma) => { + // Acquire Lock on Stock Balances + const balances = await getStockBalances(prisma, [ + { + itemId: validatedData.itemId, + itemVariantId: validatedData.itemVariantId + ? validatedData.itemVariantId + : undefined, + }, + ]); + const currentBalance = + balances[validatedData.itemVariantId ?? validatedData.itemId] + .stockBalance; + + if (!validatedData.itemVariantId) { + await prisma.item.update({ + where: { + id: validatedData.itemId, + }, + data: { + stockBalance: validatedData.qty, + stockStatus: getStockStatus(validatedData.qty), + }, + }); + } else { + await prisma.itemVariant.update({ + where: { + id: validatedData.itemVariantId, + }, + data: { + stockBalance: validatedData.qty, + stockStatus: getStockStatus(validatedData.qty), + }, + }); + } + + await prisma.stockLedger.create({ + data: { + qty: validatedData.qty - currentBalance, + balance: validatedData.qty, + itemId: validatedData.itemId, + itemVariantId: validatedData.itemVariantId + ? validatedData.itemVariantId + : null, + remarks: validatedData.remarks, + refId: validatedData.refId, + }, + }); + }); + + return { + itemId: validatedData.itemId, + itemVariantId: validatedData.itemVariantId + ? validatedData.itemVariantId + : undefined, + stockBalance: validatedData.qty, + stockStatus: getStockStatus(validatedData.qty), + }; +} diff --git a/libs/items/src/services/stocks/reconcile-stock/types.ts b/libs/items/src/services/stocks/reconcile-stock/types.ts new file mode 100644 index 00000000..da191665 --- /dev/null +++ b/libs/items/src/services/stocks/reconcile-stock/types.ts @@ -0,0 +1,13 @@ +import { Item } from '@prisma/client'; + +import { VestidoResponse } from '@vestido-ecommerce/utils'; + +import { ReconcileStockSchemaType } from './zod'; + +export type ReconcileStockRequest = ReconcileStockSchemaType; + +export type ReconcileStockResponse = { + data: Item; +}; + +export type ReconcileStockSWRResponse = VestidoResponse; diff --git a/libs/items/src/services/stocks/reconcileInventory/zod.ts b/libs/items/src/services/stocks/reconcile-stock/zod.ts similarity index 59% rename from libs/items/src/services/stocks/reconcileInventory/zod.ts rename to libs/items/src/services/stocks/reconcile-stock/zod.ts index 3c00b8b0..a02528b8 100644 --- a/libs/items/src/services/stocks/reconcileInventory/zod.ts +++ b/libs/items/src/services/stocks/reconcile-stock/zod.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -export const stockUpdateSchema = z.object({ +export const ReconcileStockSchema = z.object({ refId: z.string().optional(), itemId: z.string(), itemVariantId: z.string().optional(), @@ -8,4 +8,4 @@ export const stockUpdateSchema = z.object({ remarks: z.string().nullish(), }); -export type stockUpdateSchemaType = z.infer; +export type ReconcileStockSchemaType = z.infer; diff --git a/libs/items/src/services/stocks/reconcileInventory/service.ts b/libs/items/src/services/stocks/reconcileInventory/service.ts deleted file mode 100644 index 9c960978..00000000 --- a/libs/items/src/services/stocks/reconcileInventory/service.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { getPrismaClient } from '@vestido-ecommerce/models'; - -import { stockUpdateSchema } from './zod'; -import { stockUpdateSchemaType } from './zod'; - -export async function reconcileInventory(data: stockUpdateSchemaType) { - const prisma = getPrismaClient(); - const validatedData = stockUpdateSchema.parse(data); - - await prisma.$transaction(async (prisma) => { - if (validatedData.itemVariantId === null) { - await prisma.item.update({ - where: { - id: validatedData.itemId, - }, - data: { - stockBalance: validatedData.qty, - }, - }); - } else { - await prisma.itemVariant.update({ - where: { - id: validatedData.itemVariantId, - }, - data: { - stockBalance: validatedData.qty, - }, - }); - } - - await prisma.stockLedger.create({ - data: { - qty: validatedData.qty, - balance: validatedData.qty, - itemId: validatedData.itemId, - itemVariantId: validatedData.itemVariantId - ? validatedData.itemVariantId - : ' ', - remarks: validatedData.remarks, - refId: validatedData.refId, - }, - }); - }); - - return; -} diff --git a/libs/items/src/services/stocks/reconcileInventory/types.ts b/libs/items/src/services/stocks/reconcileInventory/types.ts deleted file mode 100644 index 5713b0ec..00000000 --- a/libs/items/src/services/stocks/reconcileInventory/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Item } from '@prisma/client'; - -import { VestidoResponse } from '@vestido-ecommerce/utils'; - -import { stockUpdateSchemaType } from './zod'; - -export type stockUpdateRequest = stockUpdateSchemaType; - -export type stockUpdateResponse = { - data: Item; -}; - -export type stockUpdateSWRResponse = VestidoResponse; diff --git a/libs/items/src/services/stocks/releaseInventory/index.ts b/libs/items/src/services/stocks/release-stock/index.ts similarity index 100% rename from libs/items/src/services/stocks/releaseInventory/index.ts rename to libs/items/src/services/stocks/release-stock/index.ts diff --git a/libs/items/src/services/stocks/releaseInventory/service.ts b/libs/items/src/services/stocks/release-stock/service.ts similarity index 77% rename from libs/items/src/services/stocks/releaseInventory/service.ts rename to libs/items/src/services/stocks/release-stock/service.ts index 1b10acf5..96201bc2 100644 --- a/libs/items/src/services/stocks/releaseInventory/service.ts +++ b/libs/items/src/services/stocks/release-stock/service.ts @@ -1,15 +1,16 @@ import type { PrismaTransactionalClient } from '@vestido-ecommerce/models'; import { VestidoError } from '@vestido-ecommerce/utils'; -import { StockBalanceRow } from '../getStockBalance'; -import { releaseInventorySchema, releaseInventorySchemaType } from './zod'; +import { StockBalanceRow } from '../get-stock-balance'; +import { getStockStatus } from '../get-stock-status'; +import { ReleaseStockSchema, ReleaseStockSchemaType } from './zod'; -export async function releaseInventory( +export async function releaseStock( prisma: PrismaTransactionalClient, - data: releaseInventorySchemaType, + data: ReleaseStockSchemaType, stockBalances: StockBalanceRow[], ) { - const validatedData = releaseInventorySchema.parse(data); + const validatedData = ReleaseStockSchema.parse(data); for (const inventoryItem of validatedData.items) { const stockRow = stockBalances.find( @@ -34,6 +35,7 @@ export async function releaseInventory( }, data: { stockBalance: newBalance, + stockStatus: getStockStatus(newBalance), }, }); } else { @@ -43,6 +45,7 @@ export async function releaseInventory( }, data: { stockBalance: newBalance, + stockStatus: getStockStatus(newBalance), }, }); } diff --git a/libs/items/src/services/stocks/release-stock/types.ts b/libs/items/src/services/stocks/release-stock/types.ts new file mode 100644 index 00000000..fa0be3b3 --- /dev/null +++ b/libs/items/src/services/stocks/release-stock/types.ts @@ -0,0 +1,10 @@ +import { Item } from '@prisma/client'; + +import { ReleaseStockSchemaType } from './zod'; + +export type releaseStockRequest = { + data: ReleaseStockSchemaType; +}; +export type releaseStockResponse = { + data: Item; +}; diff --git a/libs/items/src/services/stocks/releaseInventory/zod.ts b/libs/items/src/services/stocks/release-stock/zod.ts similarity index 63% rename from libs/items/src/services/stocks/releaseInventory/zod.ts rename to libs/items/src/services/stocks/release-stock/zod.ts index b3e47ce3..18d36daf 100644 --- a/libs/items/src/services/stocks/releaseInventory/zod.ts +++ b/libs/items/src/services/stocks/release-stock/zod.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -export const releaseInventorySchema = z.object({ +export const ReleaseStockSchema = z.object({ refId: z.string().optional(), items: z.array( z.object({ @@ -12,4 +12,4 @@ export const releaseInventorySchema = z.object({ remarks: z.string(), }); -export type releaseInventorySchemaType = z.infer; +export type ReleaseStockSchemaType = z.infer; diff --git a/libs/items/src/services/stocks/releaseInventory/types.ts b/libs/items/src/services/stocks/releaseInventory/types.ts deleted file mode 100644 index 758dba78..00000000 --- a/libs/items/src/services/stocks/releaseInventory/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Item } from '@prisma/client'; - -import { releaseInventorySchemaType } from './zod'; - -export type releaseInventoryRequest = { - data: releaseInventorySchemaType; -}; -export type releaseInventoryResponse = { - data: Item; -}; diff --git a/libs/items/src/services/stocks/reserveInventory/index.ts b/libs/items/src/services/stocks/reserve-stock/index.ts similarity index 100% rename from libs/items/src/services/stocks/reserveInventory/index.ts rename to libs/items/src/services/stocks/reserve-stock/index.ts diff --git a/libs/items/src/services/stocks/reserveInventory/service.ts b/libs/items/src/services/stocks/reserve-stock/service.ts similarity index 80% rename from libs/items/src/services/stocks/reserveInventory/service.ts rename to libs/items/src/services/stocks/reserve-stock/service.ts index 7ac49d9a..c5dcfa66 100644 --- a/libs/items/src/services/stocks/reserveInventory/service.ts +++ b/libs/items/src/services/stocks/reserve-stock/service.ts @@ -1,15 +1,16 @@ import type { PrismaTransactionalClient } from '@vestido-ecommerce/models'; import { VestidoError } from '@vestido-ecommerce/utils'; -import { StockBalanceRow } from '../getStockBalance/types'; -import { reserveInventorySchema, reserveInventorySchemaType } from './zod'; +import { StockBalanceRow } from '../get-stock-balance/types'; +import { getStockStatus } from '../get-stock-status'; +import { ReserveStockSchema, ReserveStockSchemaType } from './zod'; -export async function reserveInventory( +export async function reserveStock( prisma: PrismaTransactionalClient, - data: reserveInventorySchemaType, + data: ReserveStockSchemaType, stockBalances: StockBalanceRow[], ) { - const validatedData = reserveInventorySchema.parse(data); + const validatedData = ReserveStockSchema.parse(data); for (const inventoryItem of validatedData.items) { const stockRow = stockBalances.find( @@ -42,6 +43,7 @@ export async function reserveInventory( }, data: { stockBalance: newBalance, + stockStatus: getStockStatus(newBalance), }, }); } else { @@ -51,6 +53,7 @@ export async function reserveInventory( }, data: { stockBalance: newBalance, + stockStatus: getStockStatus(newBalance), }, }); } diff --git a/libs/items/src/services/stocks/reserve-stock/types.ts b/libs/items/src/services/stocks/reserve-stock/types.ts new file mode 100644 index 00000000..b814b791 --- /dev/null +++ b/libs/items/src/services/stocks/reserve-stock/types.ts @@ -0,0 +1,10 @@ +import { Item } from '@prisma/client'; + +import { ReserveStockSchemaType } from './zod'; + +export type ReserveInventoryRequest = { + data: ReserveStockSchemaType; +}; +export type ReserveInventoryResponse = { + data: Item; +}; diff --git a/libs/items/src/services/stocks/reserveInventory/zod.ts b/libs/items/src/services/stocks/reserve-stock/zod.ts similarity index 63% rename from libs/items/src/services/stocks/reserveInventory/zod.ts rename to libs/items/src/services/stocks/reserve-stock/zod.ts index 65b2c9d2..4332ea7f 100644 --- a/libs/items/src/services/stocks/reserveInventory/zod.ts +++ b/libs/items/src/services/stocks/reserve-stock/zod.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -export const reserveInventorySchema = z.object({ +export const ReserveStockSchema = z.object({ refId: z.string().optional(), items: z.array( z.object({ @@ -12,4 +12,4 @@ export const reserveInventorySchema = z.object({ remarks: z.string(), }); -export type reserveInventorySchemaType = z.infer; +export type ReserveStockSchemaType = z.infer; diff --git a/libs/items/src/services/stocks/reserveInventory/types.ts b/libs/items/src/services/stocks/reserveInventory/types.ts deleted file mode 100644 index 36bf89bf..00000000 --- a/libs/items/src/services/stocks/reserveInventory/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Item } from '@prisma/client'; - -import { reserveInventorySchemaType } from './zod'; - -export type reserveInventoryRequest = { - data: reserveInventorySchemaType; -}; -export type reserveInventoryResponse = { - data: Item; -}; diff --git a/libs/items/src/swr/stocks/index.ts b/libs/items/src/swr/stocks/index.ts index d0d5f5fb..75e0a6ec 100644 --- a/libs/items/src/swr/stocks/index.ts +++ b/libs/items/src/swr/stocks/index.ts @@ -1,4 +1,4 @@ 'use client'; export * from './getStockBalance'; -export * from './reconcileInventory'; +export * from './reconcileStock'; diff --git a/libs/items/src/swr/stocks/reconcileInventory/index.ts b/libs/items/src/swr/stocks/reconcileInventory/index.ts deleted file mode 100644 index e40a196f..00000000 --- a/libs/items/src/swr/stocks/reconcileInventory/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './reconcile-inventory'; -export * from './service'; diff --git a/libs/items/src/swr/stocks/reconcileInventory/reconcile-inventory.ts b/libs/items/src/swr/stocks/reconcileInventory/reconcile-inventory.ts deleted file mode 100644 index 4a0da8d4..00000000 --- a/libs/items/src/swr/stocks/reconcileInventory/reconcile-inventory.ts +++ /dev/null @@ -1,18 +0,0 @@ -import useSWRMutation from 'swr/mutation' -import { useAuth } from '@vestido-ecommerce/auth/client' - -import { - stockUpdateRequest, - stockUpdateResponse, -} from 'libs/items/src/services/stocks' -import { StockSWRKeys } from '../keys' -import { reconcileInventory } from './service' - -export function useReconcileInventory() { - const { authHeaders } = useAuth() - - return useSWRMutation( - StockSWRKeys.RECONCILE, - (_key, { arg }) => reconcileInventory(arg, authHeaders) - ) -} diff --git a/libs/items/src/swr/stocks/reconcileStock/index.ts b/libs/items/src/swr/stocks/reconcileStock/index.ts new file mode 100644 index 00000000..fc8c2dc1 --- /dev/null +++ b/libs/items/src/swr/stocks/reconcileStock/index.ts @@ -0,0 +1,2 @@ +export * from './reconcile-stock'; +export * from './service'; diff --git a/libs/items/src/swr/stocks/reconcileStock/reconcile-stock.ts b/libs/items/src/swr/stocks/reconcileStock/reconcile-stock.ts new file mode 100644 index 00000000..433a933e --- /dev/null +++ b/libs/items/src/swr/stocks/reconcileStock/reconcile-stock.ts @@ -0,0 +1,18 @@ +import useSWRMutation from 'swr/mutation' +import { useAuth } from '@vestido-ecommerce/auth/client' + +import { + ReconcileStockRequest, + ReconcileStockResponse, +} from 'libs/items/src/services/stocks' +import { StockSWRKeys } from '../keys' +import { reconcileStock } from './service' + +export function useReconcileStock() { + const { authHeaders } = useAuth() + + return useSWRMutation( + StockSWRKeys.RECONCILE, + (_key, { arg }) => reconcileStock(arg, authHeaders) + ) +} diff --git a/libs/items/src/swr/stocks/reconcileInventory/service.ts b/libs/items/src/swr/stocks/reconcileStock/service.ts similarity index 60% rename from libs/items/src/swr/stocks/reconcileInventory/service.ts rename to libs/items/src/swr/stocks/reconcileStock/service.ts index 820688f8..5f88abd7 100644 --- a/libs/items/src/swr/stocks/reconcileInventory/service.ts +++ b/libs/items/src/swr/stocks/reconcileStock/service.ts @@ -1,18 +1,18 @@ import { handleVestidoErrorResponse } from '@vestido-ecommerce/utils'; import { - stockUpdateRequest, - stockUpdateResponse, + ReconcileStockRequest, + ReconcileStockResponse, } from 'libs/items/src/services/stocks'; -export async function reconcileInventory( - args: stockUpdateRequest, +export async function reconcileStock( + args: ReconcileStockRequest, headers?: Record, -): Promise { - const url = `/api/stocks/update`; +): Promise { + const url = `/api/items/${args.itemId}/stock`; const r = await fetch(url, { - method: 'POST', + method: 'POST', headers: { 'Content-Type': 'application/json', ...(headers ?? {}), @@ -25,5 +25,5 @@ export async function reconcileInventory( } const data = await r.json(); - return data as stockUpdateResponse; + return data as ReconcileStockResponse; } diff --git a/libs/models/prisma/migrations/20250908123517_/migration.sql b/libs/models/prisma/migrations/20250908123517_/migration.sql new file mode 100644 index 00000000..115deccb --- /dev/null +++ b/libs/models/prisma/migrations/20250908123517_/migration.sql @@ -0,0 +1,8 @@ +-- DropForeignKey +ALTER TABLE "StockLedger" DROP CONSTRAINT "StockLedger_itemVariantId_fkey"; + +-- AlterTable +ALTER TABLE "StockLedger" ALTER COLUMN "itemVariantId" DROP NOT NULL; + +-- AddForeignKey +ALTER TABLE "StockLedger" ADD CONSTRAINT "StockLedger_itemVariantId_fkey" FOREIGN KEY ("itemVariantId") REFERENCES "ItemVariant"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/libs/models/prisma/schema.prisma b/libs/models/prisma/schema.prisma index 9aeda258..55276b15 100644 --- a/libs/models/prisma/schema.prisma +++ b/libs/models/prisma/schema.prisma @@ -529,14 +529,14 @@ model Tax { } model StockLedger { - id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid - createdAt DateTime @default(now()) @db.Timestamptz(3) + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + createdAt DateTime @default(now()) @db.Timestamptz(3) qty Int balance Int - item Item @relation(fields: [itemId], references: [id]) - itemId String @db.Uuid - itemVariant ItemVariant @relation(fields: [itemVariantId], references: [id]) - itemVariantId String @db.Uuid + item Item @relation(fields: [itemId], references: [id]) + itemId String @db.Uuid + itemVariant ItemVariant? @relation(fields: [itemVariantId], references: [id]) + itemVariantId String? @db.Uuid remarks String? refId String? } diff --git a/libs/orders/src/services/orders/cancel-order/service.ts b/libs/orders/src/services/orders/cancel-order/service.ts index 336b8308..c335daa3 100644 --- a/libs/orders/src/services/orders/cancel-order/service.ts +++ b/libs/orders/src/services/orders/cancel-order/service.ts @@ -1,4 +1,4 @@ -import { getStockBalances, releaseInventory } from '@vestido-ecommerce/items'; +import { getStockBalances, releaseStock } from '@vestido-ecommerce/items'; import { getPrismaClient } from '@vestido-ecommerce/models'; import { refundRazorpay } from '@vestido-ecommerce/razorpay'; import { VestidoError } from '@vestido-ecommerce/utils'; @@ -126,17 +126,17 @@ export async function cancelOrder( }); } //release the stock - const balances = ( + const balances = Object.values( await getStockBalances( prismaTransaction, order.orderItems.map((item) => ({ itemId: item.itemId, itemVariantId: item.variantId ?? undefined, })), - ) - ).map((row) => row.latestStockBalanceDetails); + ), + ).map((row) => row._stockBalanceRow); - await releaseInventory( + await releaseStock( prismaTransaction, { refId: order.id, diff --git a/libs/orders/src/services/orders/create-order/service.ts b/libs/orders/src/services/orders/create-order/service.ts index dada8860..501cc6c4 100644 --- a/libs/orders/src/services/orders/create-order/service.ts +++ b/libs/orders/src/services/orders/create-order/service.ts @@ -2,7 +2,7 @@ import type { Payment } from '@prisma/client'; import { sendSMS, SMSSenderID, SMSTemplate } from '@vestido-ecommerce/fast2sms'; import { clearCartOnOrderCreation } from '@vestido-ecommerce/items'; -import { getStockBalances, reserveInventory } from '@vestido-ecommerce/items'; +import { getStockBalances, reserveStock } from '@vestido-ecommerce/items'; import { getPrismaClient } from '@vestido-ecommerce/models'; import { VestidoError } from '@vestido-ecommerce/utils'; @@ -96,17 +96,17 @@ export async function createOrder(_data: CreateOrderSchemaType) { }, }); } - const balances = ( + const balances = Object.values( await getStockBalances( prisma, itemsWithTax.map((item) => ({ itemId: item.itemId, itemVariantId: item.variantId ?? undefined, })), - ) - ).map((row) => row.latestStockBalanceDetails); + ), + ).map((row) => row._stockBalanceRow); - await reserveInventory( + await reserveStock( prisma, { refId: createdOrder.id, diff --git a/third-party/razorpay/src/services/razorpay-webhook/service.ts b/third-party/razorpay/src/services/razorpay-webhook/service.ts index a7ea90b2..24fc71e6 100644 --- a/third-party/razorpay/src/services/razorpay-webhook/service.ts +++ b/third-party/razorpay/src/services/razorpay-webhook/service.ts @@ -1,4 +1,4 @@ -import { getStockBalances, releaseInventory } from '@vestido-ecommerce/items'; +import { getStockBalances, releaseStock } from '@vestido-ecommerce/items'; import { getPrismaClient } from '@vestido-ecommerce/models'; import { VestidoError } from '@vestido-ecommerce/utils'; @@ -150,17 +150,17 @@ export async function handleRazorpayWebhook(data: RazorpayWebhookSchemaType) { console.log('Payments updated to FAILED.'); //release the stock - const balances = ( + const balances = Object.values( await getStockBalances( transaction, order.orderItems.map((item) => ({ itemId: item.itemId, itemVariantId: item.variantId ?? undefined, })), - ) - ).map((row) => row.latestStockBalanceDetails); + ), + ).map((row) => row._stockBalanceRow); - await releaseInventory( + await releaseStock( transaction, { refId: order.id, diff --git a/vestido/vestido-dashboard/app/api/items/[item_id]/route.ts b/vestido/vestido-dashboard/app/api/items/[item_id]/route.ts index b7ba44b4..eaad0414 100644 --- a/vestido/vestido-dashboard/app/api/items/[item_id]/route.ts +++ b/vestido/vestido-dashboard/app/api/items/[item_id]/route.ts @@ -2,7 +2,6 @@ import { authMiddleware, roleMiddleware } from '@vestido-ecommerce/auth'; import { deleteItem, getItemDetails, - reconcileInventory, updateItem, } from '@vestido-ecommerce/items'; import { apiRouteHandler } from '@vestido-ecommerce/utils'; @@ -12,8 +11,7 @@ export const GET = apiRouteHandler( roleMiddleware('ADMIN'), async ({ params }) => { const item = await getItemDetails(params.item_id); - - return { item }; + return item; }, ); @@ -22,15 +20,7 @@ export const PUT = apiRouteHandler( roleMiddleware('ADMIN'), async ({ request, params: { item_id } }) => { const body = await request.json(); - const updated_item = await updateItem(item_id, body); - - // TODO: Rethinink about this approach - const updated_stock_balance = await reconcileInventory({ - ...body, - itemId: item_id, - }); - - return { updated_item, updated_stock_balance }; + return await updateItem(item_id, body); }, ); diff --git a/vestido/vestido-dashboard/app/api/items/[item_id]/stock/route.ts b/vestido/vestido-dashboard/app/api/items/[item_id]/stock/route.ts new file mode 100644 index 00000000..56a8315a --- /dev/null +++ b/vestido/vestido-dashboard/app/api/items/[item_id]/stock/route.ts @@ -0,0 +1,18 @@ +import { authMiddleware, roleMiddleware } from '@vestido-ecommerce/auth'; +import { reconcileStock } from '@vestido-ecommerce/items'; +import { apiRouteHandler } from '@vestido-ecommerce/utils'; + +export const PUT = apiRouteHandler( + authMiddleware, + roleMiddleware('ADMIN'), + async ({ request, params: { item_id } }) => { + const body = await request.json(); + + const data = await reconcileStock({ + ...body, + itemId: item_id, + }); + + return data; + }, +); diff --git a/vestido/vestido-dashboard/app/api/items/[item_id]/variants/route.ts b/vestido/vestido-dashboard/app/api/items/[item_id]/variants/route.ts index ab92a8e8..0cf46cca 100644 --- a/vestido/vestido-dashboard/app/api/items/[item_id]/variants/route.ts +++ b/vestido/vestido-dashboard/app/api/items/[item_id]/variants/route.ts @@ -3,7 +3,7 @@ import { createVariant, getStockBalances, listVariants, - reconcileInventory, + reconcileStock, } from '@vestido-ecommerce/items'; import { getPrismaClient } from '@vestido-ecommerce/models'; import { apiRouteHandler } from '@vestido-ecommerce/utils'; @@ -38,7 +38,7 @@ export const PUT = apiRouteHandler( roleMiddleware('ADMIN'), async ({ request, params }) => { const body = await request.json(); - return await reconcileInventory({ + return await reconcileStock({ ...body, itemId: params.item_id, itemVariantId: params.variant_id, From 13dba30fd782ab9080e3fc71b04e1f32c8ec8aa1 Mon Sep 17 00:00:00 2001 From: Fahim Ali Zain Date: Mon, 8 Sep 2025 19:20:31 +0530 Subject: [PATCH 15/32] fix: Variant Reconciliation --- .../stocks/get-stock-balance/service.ts | 18 ++++++++++++++++-- .../stocks/get-stock-balance/types.ts | 1 + .../services/stocks/reconcile-stock/zod.ts | 2 +- .../src/services/stocks/release-stock/zod.ts | 2 +- .../src/services/stocks/reserve-stock/zod.ts | 2 +- .../variants/[variant_id]/stock/route.ts | 19 +++++++++++++++++++ 6 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 vestido/vestido-dashboard/app/api/items/[item_id]/variants/[variant_id]/stock/route.ts diff --git a/libs/items/src/services/stocks/get-stock-balance/service.ts b/libs/items/src/services/stocks/get-stock-balance/service.ts index d7c08acb..a7ba1a40 100644 --- a/libs/items/src/services/stocks/get-stock-balance/service.ts +++ b/libs/items/src/services/stocks/get-stock-balance/service.ts @@ -1,5 +1,6 @@ import type { PrismaTransactionalClient } from '@vestido-ecommerce/models'; import { PrismaClient } from '@vestido-ecommerce/models'; +import { VestidoError } from '@vestido-ecommerce/utils'; import { getStockStatus } from '../get-stock-status'; import { StockBalanceRow } from './types'; @@ -31,8 +32,9 @@ export async function getStockBalances( ` SELECT "id" AS "itemId", - NULL AS "itemVariantId", - "stockBalance" AS balance + NULL AS "itemVariantId", + "stockBalance" AS balance, + "hasVariants" FROM "Item" WHERE "id" = ANY($1::uuid[]) FOR UPDATE @@ -40,6 +42,18 @@ export async function getStockBalances( itemIdsOnly, )) as StockBalanceRow[]), ); + + const templateRows = rows.filter((r) => r.hasVariants); + if (templateRows.length > 0) { + throw new VestidoError({ + message: `Some items have variants. Please specify itemVariantId instead of itemId.`, + name: 'TemplateItemCannotHaveStockBalance', + httpStatus: 400, + context: { + itemIds: templateRows.map((r) => r.itemId), + }, + }); + } } if (variantIdsOnly.length > 0) { diff --git a/libs/items/src/services/stocks/get-stock-balance/types.ts b/libs/items/src/services/stocks/get-stock-balance/types.ts index 8fe5f75e..8dfd96a3 100644 --- a/libs/items/src/services/stocks/get-stock-balance/types.ts +++ b/libs/items/src/services/stocks/get-stock-balance/types.ts @@ -18,4 +18,5 @@ export type StockBalanceRow = { itemId: string; itemVariantId: string | null; balance: number; + hasVariants?: boolean; }; diff --git a/libs/items/src/services/stocks/reconcile-stock/zod.ts b/libs/items/src/services/stocks/reconcile-stock/zod.ts index a02528b8..468e6ba6 100644 --- a/libs/items/src/services/stocks/reconcile-stock/zod.ts +++ b/libs/items/src/services/stocks/reconcile-stock/zod.ts @@ -4,7 +4,7 @@ export const ReconcileStockSchema = z.object({ refId: z.string().optional(), itemId: z.string(), itemVariantId: z.string().optional(), - qty: z.coerce.number(), + qty: z.coerce.number().min(0, 'Quantity must be at least 0'), remarks: z.string().nullish(), }); diff --git a/libs/items/src/services/stocks/release-stock/zod.ts b/libs/items/src/services/stocks/release-stock/zod.ts index 18d36daf..5f4a8dbb 100644 --- a/libs/items/src/services/stocks/release-stock/zod.ts +++ b/libs/items/src/services/stocks/release-stock/zod.ts @@ -6,7 +6,7 @@ export const ReleaseStockSchema = z.object({ z.object({ itemId: z.string(), itemVariantId: z.string().nullish(), - qty: z.coerce.number(), + qty: z.coerce.number().min(1, { message: 'Quantity must be at least 1' }), }), ), remarks: z.string(), diff --git a/libs/items/src/services/stocks/reserve-stock/zod.ts b/libs/items/src/services/stocks/reserve-stock/zod.ts index 4332ea7f..b668df19 100644 --- a/libs/items/src/services/stocks/reserve-stock/zod.ts +++ b/libs/items/src/services/stocks/reserve-stock/zod.ts @@ -6,7 +6,7 @@ export const ReserveStockSchema = z.object({ z.object({ itemId: z.string(), itemVariantId: z.string().nullish(), - qty: z.coerce.number(), + qty: z.coerce.number().min(1, { message: 'Quantity must be at least 1' }), }), ), remarks: z.string(), diff --git a/vestido/vestido-dashboard/app/api/items/[item_id]/variants/[variant_id]/stock/route.ts b/vestido/vestido-dashboard/app/api/items/[item_id]/variants/[variant_id]/stock/route.ts new file mode 100644 index 00000000..30665ba3 --- /dev/null +++ b/vestido/vestido-dashboard/app/api/items/[item_id]/variants/[variant_id]/stock/route.ts @@ -0,0 +1,19 @@ +import { authMiddleware, roleMiddleware } from '@vestido-ecommerce/auth'; +import { reconcileStock } from '@vestido-ecommerce/items'; +import { apiRouteHandler } from '@vestido-ecommerce/utils'; + +export const PUT = apiRouteHandler( + authMiddleware, + roleMiddleware('ADMIN'), + async ({ request, params: { item_id, variant_id } }) => { + const body = await request.json(); + + const data = await reconcileStock({ + ...body, + itemId: item_id, + itemVariantId: variant_id, + }); + + return data; + }, +); From 683880d6bb49085a22d7088fb9cf7683cee82fae Mon Sep 17 00:00:00 2001 From: Fahim Ali Zain Date: Mon, 8 Sep 2025 19:23:56 +0530 Subject: [PATCH 16/32] fix: SWR Reconciliation --- libs/items/src/swr/stocks/reconcileStock/service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/items/src/swr/stocks/reconcileStock/service.ts b/libs/items/src/swr/stocks/reconcileStock/service.ts index 5f88abd7..6fb7a703 100644 --- a/libs/items/src/swr/stocks/reconcileStock/service.ts +++ b/libs/items/src/swr/stocks/reconcileStock/service.ts @@ -9,7 +9,7 @@ export async function reconcileStock( args: ReconcileStockRequest, headers?: Record, ): Promise { - const url = `/api/items/${args.itemId}/stock`; + const url = `/api/items/${args.itemId}${args.itemVariantId ? '/variants/' + args.itemVariantId : ''}/stock`; const r = await fetch(url, { method: 'POST', From 6ad26c8562e87ce3d15fcfcba4eebd22c1657d1b Mon Sep 17 00:00:00 2001 From: Fathima Sayeeda Date: Tue, 16 Sep 2025 17:39:31 +0530 Subject: [PATCH 17/32] feat: WIP: Update Stock for Template Items --- .../stocks/reconcile-stock/service.ts | 7 +- .../services/stocks/reconcile-stock/zod.ts | 4 +- .../src/swr/stocks/reconcileStock/service.ts | 2 +- .../modules/products/ProductForm.tsx | 53 +++++- .../modules/products/ProductSizeForm.tsx | 28 ++- .../modules/products/ProductsTable.tsx | 3 + .../products/update-bulk-stock-dialog.tsx | 180 ++++++++++++++++++ .../modules/products/update-stock-dialog.tsx | 134 +++++++++++++ 8 files changed, 386 insertions(+), 25 deletions(-) create mode 100644 vestido/vestido-dashboard/modules/products/update-bulk-stock-dialog.tsx create mode 100644 vestido/vestido-dashboard/modules/products/update-stock-dialog.tsx diff --git a/libs/items/src/services/stocks/reconcile-stock/service.ts b/libs/items/src/services/stocks/reconcile-stock/service.ts index 042cd1cf..50ae0c41 100644 --- a/libs/items/src/services/stocks/reconcile-stock/service.ts +++ b/libs/items/src/services/stocks/reconcile-stock/service.ts @@ -23,6 +23,9 @@ export async function reconcileStock(data: ReconcileStockSchemaType) { balances[validatedData.itemVariantId ?? validatedData.itemId] .stockBalance; + console.log( + `Updating Stock for ${validatedData.itemVariantId} with ${validatedData.qty} with status: ${getStockStatus(validatedData.qty)}`, + ); if (!validatedData.itemVariantId) { await prisma.item.update({ where: { @@ -50,9 +53,7 @@ export async function reconcileStock(data: ReconcileStockSchemaType) { qty: validatedData.qty - currentBalance, balance: validatedData.qty, itemId: validatedData.itemId, - itemVariantId: validatedData.itemVariantId - ? validatedData.itemVariantId - : null, + itemVariantId: validatedData.itemVariantId ?? '', remarks: validatedData.remarks, refId: validatedData.refId, }, diff --git a/libs/items/src/services/stocks/reconcile-stock/zod.ts b/libs/items/src/services/stocks/reconcile-stock/zod.ts index 468e6ba6..3d629640 100644 --- a/libs/items/src/services/stocks/reconcile-stock/zod.ts +++ b/libs/items/src/services/stocks/reconcile-stock/zod.ts @@ -1,9 +1,9 @@ import { z } from 'zod'; export const ReconcileStockSchema = z.object({ - refId: z.string().optional(), + refId: z.string().nullish(), itemId: z.string(), - itemVariantId: z.string().optional(), + itemVariantId: z.string().nullish(), qty: z.coerce.number().min(0, 'Quantity must be at least 0'), remarks: z.string().nullish(), }); diff --git a/libs/items/src/swr/stocks/reconcileStock/service.ts b/libs/items/src/swr/stocks/reconcileStock/service.ts index 6fb7a703..faec5a90 100644 --- a/libs/items/src/swr/stocks/reconcileStock/service.ts +++ b/libs/items/src/swr/stocks/reconcileStock/service.ts @@ -12,7 +12,7 @@ export async function reconcileStock( const url = `/api/items/${args.itemId}${args.itemVariantId ? '/variants/' + args.itemVariantId : ''}/stock`; const r = await fetch(url, { - method: 'POST', + method: 'PUT', headers: { 'Content-Type': 'application/json', ...(headers ?? {}), diff --git a/vestido/vestido-dashboard/modules/products/ProductForm.tsx b/vestido/vestido-dashboard/modules/products/ProductForm.tsx index 05862bc3..46e5052f 100644 --- a/vestido/vestido-dashboard/modules/products/ProductForm.tsx +++ b/vestido/vestido-dashboard/modules/products/ProductForm.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import { useRouter } from 'next/router'; import { LuChevronLeft } from 'react-icons/lu'; @@ -26,6 +26,8 @@ import { SwitchElement } from '../../forms/switch-element'; import { TextAreaElement } from '../../forms/textarea-element'; import ProductFormTaxItem from './product-form-tax-item'; import ProductSizeForm from './ProductSizeForm'; +import { BulkUpdateStockDialog } from './update-bulk-stock-dialog'; +import { UpdateStockDialog } from './update-stock-dialog'; import { useProductForm } from './use-product-form'; type ProductFormProps = { @@ -39,11 +41,15 @@ const ProductForm: React.FC = ({ itemId, isNew }) => { const { data: { data: item } = { data: null }, isLoading } = useItem( isNew ? null : itemId, ); + const variants = item?.variants; + + console.log('vaaariants', variants); const sizeCharts = useVestidoSizeChart(); const sizeChartIds = sizeCharts ? Object.keys(sizeCharts) : []; const { form, handleSubmit } = useProductForm(isNew, itemId, item); + const [stock, setStock] = useState(item?.stockBalance ?? 0); const hasVariants = form.watch('hasVariants'); const { isSubmitting } = form.formState; @@ -89,6 +95,29 @@ const ProductForm: React.FC = ({ itemId, isNew }) => { )} + + {!hasVariants && ( +
+ {' '} + + {item && ( + + + + )} +
+ )} +
= ({ itemId, isNew }) => { {hasVariants && ( <>
-
Size Availability
+
+
Size Availability
+ {item && ( + ({ + itemVariantId: v.id, + name: + v.attributeValues.find((x) => x.attribute.name === 'Size') + ?.attributeValue.value ?? '', + currentStock: v.stockBalance, + }))} + onUpdated={(newValues) => { + console.log('Updated stock values:', newValues); + // Optionally refresh product data here + }} + > + + + )} +
)} diff --git a/vestido/vestido-dashboard/modules/products/ProductSizeForm.tsx b/vestido/vestido-dashboard/modules/products/ProductSizeForm.tsx index da45bfed..2010731e 100644 --- a/vestido/vestido-dashboard/modules/products/ProductSizeForm.tsx +++ b/vestido/vestido-dashboard/modules/products/ProductSizeForm.tsx @@ -8,7 +8,6 @@ import { Card } from '@vestido-ecommerce/shadcn-ui/card'; import { Label } from '@vestido-ecommerce/shadcn-ui/label'; import { InputElement } from '../../forms/input-element'; -import { SelectElement } from '../../forms/select-element'; import { SwitchElement } from '../../forms/switch-element'; import { CreateProductForm, ItemVariantWithSize } from './zod'; @@ -54,7 +53,7 @@ const ProductSizeForm: FC<{ className?: string }> = ({ className }) => { itemAttributeValueId: size.id, enabled: false, sku: null, - stockStatus: 'AVAILABLE', + stockStatus: 'OUT_OF_STOCK', } satisfies ItemVariantWithSize; }) .sort( @@ -94,21 +93,16 @@ const ProductSizeForm: FC<{ className?: string }> = ({ className }) => {
- +
+ {' '} + {/* {form.watch(`variants.${index}.stockStatus`)} + */} + {size.stockStatus === 'OUT_OF_STOCK' + ? 'Out of Stock' + : size.stockStatus === 'AVAILABLE' + ? 'Available' + : 'Limited Stock'} +
= ({ data, categoryId }) => { Description Has Variants Variants Count + Stock + Delete @@ -105,6 +107,7 @@ const ProductsTable: React.FC = ({ data, categoryId }) => { > {item.hasVariants ? `${item.variants.length}` : 'No variant'} + {item.stockBalance} e.stopPropagation()}> diff --git a/vestido/vestido-dashboard/modules/products/update-bulk-stock-dialog.tsx b/vestido/vestido-dashboard/modules/products/update-bulk-stock-dialog.tsx new file mode 100644 index 00000000..aa1620af --- /dev/null +++ b/vestido/vestido-dashboard/modules/products/update-bulk-stock-dialog.tsx @@ -0,0 +1,180 @@ +import { ReactNode, useEffect, useState } from 'react'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { useFieldArray, useForm } from 'react-hook-form'; +import { z } from 'zod'; + +import { useReconcileStock } from '@vestido-ecommerce/items/client'; +import { Button } from '@vestido-ecommerce/shadcn-ui/button'; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@vestido-ecommerce/shadcn-ui/dialog'; +import { Form } from '@vestido-ecommerce/shadcn-ui/form'; +import { Textarea } from '@vestido-ecommerce/shadcn-ui/textarea'; +import { useToast } from '@vestido-ecommerce/shadcn-ui/use-toast'; + +import { InputElement } from '../../forms/input-element'; + +// ✅ Schema for multiple variants +const BulkUpdateStockSchema = z.object({ + variants: z.array( + z.object({ + itemVariantId: z.string(), + name: z.string(), // size / color label + currentStock: z.number(), + qty: z.coerce.number().int().nonnegative(), + remarks: z.string().optional(), + }), + ), +}); + +type BulkUpdateStockForm = z.infer; + +export function BulkUpdateStockDialog({ + itemId, + variants, // array of { id, name, currentStock } + children, + onUpdated, +}: { + itemId: string; + variants: { itemVariantId: string; name: string; currentStock: number }[]; + children: ReactNode; + onUpdated?: (newValues: { itemVariantId: string; qty: number }[]) => void; +}) { + const { toast } = useToast(); + const [open, setOpen] = useState(false); + const { trigger, isMutating } = useReconcileStock(); + + console.log('variants in bulk dialog', variants); + + const form = useForm({ + resolver: zodResolver(BulkUpdateStockSchema), + defaultValues: { + variants: variants.map((v) => ({ + itemVariantId: v.itemVariantId, + name: v.name, + currentStock: v.currentStock, + qty: v.currentStock, + remarks: '', + })), + }, + }); + + const { fields } = useFieldArray({ + control: form.control, + name: 'variants', + }); + + useEffect(() => { + if (open) { + // Reset to fresh values when dialog opens + form.reset({ + variants: variants.map((v) => ({ + itemVariantId: v.itemVariantId, + name: v.name, + currentStock: v.currentStock, + qty: v.currentStock, + remarks: '', + })), + }); + } + }, [open, variants, form]); + + const handleBulkSubmit = async (data: BulkUpdateStockForm) => { + try { + await Promise.all( + data.variants.map((variant) => + trigger({ + itemId, + itemVariantId: variant.itemVariantId, + qty: variant.qty, + remarks: variant.remarks ?? '', + }), + ), + ); + + toast({ title: 'All variant stocks updated successfully!' }); + setOpen(false); + + if (onUpdated) { + onUpdated( + data.variants.map((v) => ({ + itemVariantId: v.itemVariantId, + qty: v.qty, + })), + ); + } + } catch (err) { + console.error(err); + toast({ + title: 'Error updating stock', + description: 'Please try again.', + variant: 'destructive', + }); + } + }; + + return ( + + {children} + + + Update Stock for Variants + + +
+ +
+ {fields.map((field, index) => ( +
+ {/* Variant Name */} +
{field.name}
+ + {/* Current Stock */} + + + {/* New Stock */} + + + {/* Remarks */} +