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/client.ts b/libs/items/src/client.ts index be76b1c0..e2d6f7b4 100644 --- a/libs/items/src/client.ts +++ b/libs/items/src/client.ts @@ -1,5 +1,4 @@ export { - type ItemDetailsResponse, type ItemVariantWithAttributes, type ItemVariantWithSize, } from './services'; diff --git a/libs/items/src/services/attributes/add-attribute/services.ts b/libs/items/src/services/attributes/add-attribute/services.ts index 6e349b76..6df85b0e 100644 --- a/libs/items/src/services/attributes/add-attribute/services.ts +++ b/libs/items/src/services/attributes/add-attribute/services.ts @@ -1,8 +1,9 @@ import { getPrismaClient } from '@vestido-ecommerce/models'; -import { CreateAttributeSchema, CreateAttributeSchemaType } from './zod'; +import { CreateAttributeArgs } from './types'; +import { CreateAttributeSchema } from './zod'; -export async function createAttribute(body: CreateAttributeSchemaType) { +export async function createAttribute(body: CreateAttributeArgs) { const prisma = getPrismaClient(); const validatedData = CreateAttributeSchema.parse(body); diff --git a/libs/items/src/services/attributes/add-attribute/types.ts b/libs/items/src/services/attributes/add-attribute/types.ts index f1fcb129..badb8ed5 100644 --- a/libs/items/src/services/attributes/add-attribute/types.ts +++ b/libs/items/src/services/attributes/add-attribute/types.ts @@ -1,11 +1,10 @@ -import { ItemAttribute, ItemAttributeValue } from '@prisma/client'; - +import { createAttribute } from './services'; import { CreateAttributeSchemaType } from './zod'; -export type CreateAttributeRequest = CreateAttributeSchemaType; +export type CreateAttributeArgs = CreateAttributeSchemaType; + +export type CreateAttributeResult = Awaited>; -export type CreateAttributeResponse = { - data: ItemAttribute & { - values: ItemAttributeValue[]; - }; -}; +// export type CreateAttributeResult = ItemAttribute & { +// values: ItemAttributeValue[]; +// }; diff --git a/libs/items/src/services/attributes/list-attributes/service.ts b/libs/items/src/services/attributes/list-attributes/service.ts index e342227b..57ef8031 100644 --- a/libs/items/src/services/attributes/list-attributes/service.ts +++ b/libs/items/src/services/attributes/list-attributes/service.ts @@ -1,9 +1,9 @@ import { getPrismaClient } from '@vestido-ecommerce/models'; -import { ListAttributesRequest } from './types'; +import { ListAttributesArgs } from './types'; import { ListAttributeRequestSchema } from './zod'; -export async function listAttribute(_args: ListAttributesRequest) { +export async function listAttribute(_args: ListAttributesArgs) { const prisma = getPrismaClient(); const args = ListAttributeRequestSchema.parse(_args ?? {}); diff --git a/libs/items/src/services/attributes/list-attributes/types.ts b/libs/items/src/services/attributes/list-attributes/types.ts index 6265a9d9..70d85764 100644 --- a/libs/items/src/services/attributes/list-attributes/types.ts +++ b/libs/items/src/services/attributes/list-attributes/types.ts @@ -1,5 +1,5 @@ import { type listAttribute } from './service'; import { ListAttributeRequestSchemaType } from './zod'; -export type ListAttributesRequest = ListAttributeRequestSchemaType; -export type ListAttributesResponse = Awaited>; +export type ListAttributeArgs = ListAttributeRequestSchemaType; +export type ListAttributesResult = Awaited>; diff --git a/libs/items/src/services/attributes/update-attribute/types.ts b/libs/items/src/services/attributes/update-attribute/types.ts index 171fba3e..3fc8d5dc 100644 --- a/libs/items/src/services/attributes/update-attribute/types.ts +++ b/libs/items/src/services/attributes/update-attribute/types.ts @@ -1,11 +1,6 @@ -import { ItemAttribute, ItemAttributeValue } from '@prisma/client'; - +import { updateAttribute } from './services'; import { UpdateAttributeSchemaType } from './zod'; -export type UpdateAttributeRequest = UpdateAttributeSchemaType; +export type UpdateAttributeArgs = UpdateAttributeSchemaType; -export type UpdateAttributeResponse = { - data: ItemAttribute & { - values: ItemAttributeValue[]; - }; -}; +export type UpdateAttributeResult = Awaited>; diff --git a/libs/items/src/services/cart/add-to-cart/services.ts b/libs/items/src/services/cart/add-to-cart/services.ts index 00e7e71a..182e8915 100644 --- a/libs/items/src/services/cart/add-to-cart/services.ts +++ b/libs/items/src/services/cart/add-to-cart/services.ts @@ -1,8 +1,9 @@ import { getPrismaClient } from '@vestido-ecommerce/models'; -import { AddToCartSchema, AddToCartSchemaType } from './zod'; +import { AddToCartArgs } from './types'; +import { AddToCartSchema } from './zod'; -export async function addToCart(body: AddToCartSchemaType) { +export async function addToCart(body: AddToCartArgs) { const prisma = getPrismaClient(); const validatedData = AddToCartSchema.parse(body); diff --git a/libs/items/src/services/cart/add-to-cart/types.ts b/libs/items/src/services/cart/add-to-cart/types.ts index 67437180..65fa9125 100644 --- a/libs/items/src/services/cart/add-to-cart/types.ts +++ b/libs/items/src/services/cart/add-to-cart/types.ts @@ -1,9 +1,6 @@ -import { CartItem } from '@prisma/client'; - +import { addToCart } from './services'; import { AddToCartSchemaType } from './zod'; -export type AddToCartRequest = AddToCartSchemaType; +export type AddToCartArgs = AddToCartSchemaType; -export type AddToCartResponse = { - data: CartItem; -}; +export type AddToCartResult = Awaited>; diff --git a/libs/items/src/services/cart/get-cart/index.ts b/libs/items/src/services/cart/get-cart/index.ts index 3112a3c0..45237b7d 100644 --- a/libs/items/src/services/cart/get-cart/index.ts +++ b/libs/items/src/services/cart/get-cart/index.ts @@ -1,2 +1,2 @@ -export * from './services'; +export * from './service'; export * from './types'; diff --git a/libs/items/src/services/cart/get-cart/services.ts b/libs/items/src/services/cart/get-cart/service.ts similarity index 100% rename from libs/items/src/services/cart/get-cart/services.ts rename to libs/items/src/services/cart/get-cart/service.ts diff --git a/libs/items/src/services/cart/get-cart/types.ts b/libs/items/src/services/cart/get-cart/types.ts index ba5ea865..245326be 100644 --- a/libs/items/src/services/cart/get-cart/types.ts +++ b/libs/items/src/services/cart/get-cart/types.ts @@ -1,9 +1,3 @@ -import { CartItem, Item } from '@prisma/client'; +import { listCartItems } from './service'; -export type CartItemResponse = { - data: Array< - CartItem & { - item: Item; - } - >; -}; +export type CartItemResult = Awaited>; diff --git a/libs/items/src/services/cart/remove-from-cart/services.ts b/libs/items/src/services/cart/remove-from-cart/services.ts index d2f294d4..906fe697 100644 --- a/libs/items/src/services/cart/remove-from-cart/services.ts +++ b/libs/items/src/services/cart/remove-from-cart/services.ts @@ -1,8 +1,9 @@ import { getPrismaClient } from '@vestido-ecommerce/models'; +import { RemoveFromCartArgs } from './types'; import { RemoveFromCartSchema } from './zod'; -export async function removeFromCart(body: unknown) { +export async function removeFromCart(body: RemoveFromCartArgs) { const prisma = getPrismaClient(); const validatedData = RemoveFromCartSchema.parse(body); diff --git a/libs/items/src/services/cart/remove-from-cart/types.ts b/libs/items/src/services/cart/remove-from-cart/types.ts index cb601b61..ae28c38a 100644 --- a/libs/items/src/services/cart/remove-from-cart/types.ts +++ b/libs/items/src/services/cart/remove-from-cart/types.ts @@ -1,9 +1,6 @@ -import { CartItem } from '@prisma/client'; - +import { removeFromCart } from './services'; import { RemoveFromCartSchemaType } from './zod'; -export type RemoveFromCartRequest = RemoveFromCartSchemaType; +export type RemoveFromCartArgs = RemoveFromCartSchemaType; -export type RemoveFromCartResponse = { - data: CartItem; -}; +export type RemoveFromCartResult = Awaited>; diff --git a/libs/items/src/services/categories/create-category/service.ts b/libs/items/src/services/categories/create-category/service.ts index 7b2f5518..4d477fd8 100644 --- a/libs/items/src/services/categories/create-category/service.ts +++ b/libs/items/src/services/categories/create-category/service.ts @@ -4,9 +4,10 @@ import { getPrismaClient } from '@vestido-ecommerce/models'; import { VestidoError } from '@vestido-ecommerce/utils'; import { validateSlug } from '../../slug'; -import { CreateCategorySchema, CreateCategorySchemaType } from './zod'; +import { CreateCategoryArgs } from './types'; +import { CreateCategorySchema } from './zod'; -export async function createCategory(body: CreateCategorySchemaType) { +export async function createCategory(body: CreateCategoryArgs) { const prisma = getPrismaClient(); const validatedData = CreateCategorySchema.parse(body); diff --git a/libs/items/src/services/categories/create-category/types.ts b/libs/items/src/services/categories/create-category/types.ts index 7746431a..bd57d2e1 100644 --- a/libs/items/src/services/categories/create-category/types.ts +++ b/libs/items/src/services/categories/create-category/types.ts @@ -1,11 +1,6 @@ -import { Category } from '@prisma/client'; - +import { createCategory } from './service'; import { CreateCategorySchemaType } from './zod'; -export type CreateCategoryRequest = { - data: CreateCategorySchemaType; -}; +export type CreateCategoryArgs = CreateCategorySchemaType; -export type CreateCategoryResponse = { - data: Category; -}; +export type CreateCategoryResult = Awaited>; diff --git a/libs/items/src/services/categories/get-category/types.ts b/libs/items/src/services/categories/get-category/types.ts index 25b8d6f6..0a6c09bb 100644 --- a/libs/items/src/services/categories/get-category/types.ts +++ b/libs/items/src/services/categories/get-category/types.ts @@ -1,5 +1,3 @@ -import { Category } from '@prisma/client'; +import { categoryDetails } from './service'; -export type CategoryDetailsResponse = { - data: Category; -}; +export type CategoryDetailsResult = Awaited>; diff --git a/libs/items/src/services/categories/list-category/service.ts b/libs/items/src/services/categories/list-category/service.ts index 3554de03..9519e662 100644 --- a/libs/items/src/services/categories/list-category/service.ts +++ b/libs/items/src/services/categories/list-category/service.ts @@ -1,9 +1,9 @@ import { getPrismaClient } from '@vestido-ecommerce/models'; -import { ListCategoryRequest } from './types'; +import { ListCategoryArgs } from './types'; import { ListCategoryRequestSchema } from './zod'; -export async function listCategories(_args: ListCategoryRequest) { +export async function listCategories(_args: ListCategoryArgs) { const prisma = getPrismaClient(); const args = ListCategoryRequestSchema.parse(_args); diff --git a/libs/items/src/services/categories/list-category/types.ts b/libs/items/src/services/categories/list-category/types.ts index 3ce1a2cb..44d3d9d4 100644 --- a/libs/items/src/services/categories/list-category/types.ts +++ b/libs/items/src/services/categories/list-category/types.ts @@ -1,5 +1,5 @@ import { type listCategories } from './service'; import { ListCategoryRequestSchemaType } from './zod'; -export type ListCategoryRequest = ListCategoryRequestSchemaType; -export type ListCategoriesResponse = Awaited>; +export type ListCategoryArgs = ListCategoryRequestSchemaType; +export type ListCategoryResult = Awaited>; diff --git a/libs/items/src/services/categories/update-category/service.ts b/libs/items/src/services/categories/update-category/service.ts index 4c17ba9f..4cb849cb 100644 --- a/libs/items/src/services/categories/update-category/service.ts +++ b/libs/items/src/services/categories/update-category/service.ts @@ -4,12 +4,12 @@ import { getPrismaClient } from '@vestido-ecommerce/models'; import { VestidoError } from '@vestido-ecommerce/utils'; import { validateSlug } from '../../slug'; -import { UpdateCategoryRequest } from './types'; +import { UpdateCategoryArgs } from './types'; import { UpdateCategorySchema } from './zod'; export async function updateCategory( categoryId: string, - data: UpdateCategoryRequest, + data: UpdateCategoryArgs, ) { const prisma = getPrismaClient(); diff --git a/libs/items/src/services/categories/update-category/types.ts b/libs/items/src/services/categories/update-category/types.ts index 94e0cbf2..6150cc12 100644 --- a/libs/items/src/services/categories/update-category/types.ts +++ b/libs/items/src/services/categories/update-category/types.ts @@ -1,11 +1,6 @@ -import { Category } from '@prisma/client'; - +import { updateCategory } from './service'; import { UpdateCategorySchemaType } from './zod'; -export type UpdateCategoryRequest = { - data: UpdateCategorySchemaType; -}; +export type UpdateCategoryArgs = UpdateCategorySchemaType; -export type UpdateCategoryResponse = { - data: Category; -}; +export type UpdateCategoryResult = Awaited>; diff --git a/libs/items/src/services/index.ts b/libs/items/src/services/index.ts index 1aa5f981..3dec5a0c 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'; export * from './variants'; export * from './wishlist/add-to-wishlist'; export * from './wishlist/get-wishlist'; diff --git a/libs/items/src/services/items/create-item/types.ts b/libs/items/src/services/items/create-item/types.ts index 8e9170e6..d86d7b3c 100644 --- a/libs/items/src/services/items/create-item/types.ts +++ b/libs/items/src/services/items/create-item/types.ts @@ -1,10 +1,6 @@ -import { Item } from '@prisma/client'; - import { ItemUpsertSchemaType } from '../zod'; +import { createItem } from './service'; + +export type CreateItemArgs = ItemUpsertSchemaType; -export type CreateItemRequest = { - data: ItemUpsertSchemaType; -}; -export type CreateItemResponse = { - data: Item; -}; +export type CreateItemResult = Awaited>; diff --git a/libs/items/src/services/items/get-item/types.ts b/libs/items/src/services/items/get-item/types.ts index f8e719a0..97d94b01 100644 --- a/libs/items/src/services/items/get-item/types.ts +++ b/libs/items/src/services/items/get-item/types.ts @@ -1,9 +1,5 @@ import { getItemDetails } from './service'; -export type ItemDetails = NonNullable< +export type ItemDetailsResult = NonNullable< Awaited> >; - -export type ItemDetailsResponse = { - data: ItemDetails | null; -}; diff --git a/libs/items/src/services/items/list-item/types.ts b/libs/items/src/services/items/list-item/types.ts index 48dfcb63..9fadb7a8 100644 --- a/libs/items/src/services/items/list-item/types.ts +++ b/libs/items/src/services/items/list-item/types.ts @@ -1,4 +1,4 @@ import { type listItem } from './service'; import { ListItemRequestSchemaType } from './zod'; -export type ListItemRequest = ListItemRequestSchemaType; -export type ListItemResponse = Awaited>; +export type ListItemArgs = ListItemRequestSchemaType; +export type ListItemResult = Awaited>; diff --git a/libs/items/src/services/items/update-item/types.ts b/libs/items/src/services/items/update-item/types.ts index 61e90bba..73dcd044 100644 --- a/libs/items/src/services/items/update-item/types.ts +++ b/libs/items/src/services/items/update-item/types.ts @@ -1,10 +1,6 @@ -import { Item } from '@prisma/client'; - import { ItemUpsertSchemaType } from '../zod'; +import { updateItem } from './service'; + +export type UpdateItemArgs = ItemUpsertSchemaType; -export type UpdateItemRequest = { - data: ItemUpsertSchemaType; -}; -export type UpdateItemResponse = { - data: Item; -}; +export type UpdateItemResult = Awaited>; diff --git a/libs/items/src/services/items/zod.ts b/libs/items/src/services/items/zod.ts index c8341528..67b2dee6 100644 --- a/libs/items/src/services/items/zod.ts +++ b/libs/items/src/services/items/zod.ts @@ -11,6 +11,7 @@ export const ItemVariantWithSizeSchema = z.object({ stockStatus: z .nativeEnum(StockStatus) .default('AVAILABLE' satisfies StockStatus), + stockBalance: z.number().nullish(), }); export type ItemVariantWithSize = z.infer; diff --git a/libs/items/src/services/stocks/get-stock-balance/index.ts b/libs/items/src/services/stocks/get-stock-balance/index.ts new file mode 100644 index 00000000..45237b7d --- /dev/null +++ b/libs/items/src/services/stocks/get-stock-balance/index.ts @@ -0,0 +1,2 @@ +export * from './service'; +export * from './types'; diff --git a/libs/items/src/services/stocks/get-stock-balance/service.ts b/libs/items/src/services/stocks/get-stock-balance/service.ts new file mode 100644 index 00000000..a7ba1a40 --- /dev/null +++ b/libs/items/src/services/stocks/get-stock-balance/service.ts @@ -0,0 +1,89 @@ +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'; +import { GetStockBalanceInputSchemaType } from './zod'; + +type StockBalanceResult = { + itemId: string; + itemVariantId: string | null; + stockBalance: number; + stockStatus: 'OUT_OF_STOCK' | 'LIMITED_STOCK' | 'AVAILABLE'; + _stockBalanceRow: StockBalanceRow; +}; + +export async function getStockBalances( + prisma: PrismaClient | PrismaTransactionalClient, + items: GetStockBalanceInputSchemaType[], +): Promise> { + const itemIdsOnly = items + .filter((i) => !i.itemVariantId) + .map((i) => i.itemId); + const variantIdsOnly = items + .filter((i) => !!i.itemVariantId) + .map((i) => i.itemVariantId!); + + const rows: StockBalanceRow[] = []; + if (itemIdsOnly.length > 0) { + rows.push( + ...((await prisma.$queryRawUnsafe( + ` + SELECT + "id" AS "itemId", + NULL AS "itemVariantId", + "stockBalance" AS balance, + "hasVariants" + FROM "Item" + WHERE "id" = ANY($1::uuid[]) + FOR UPDATE + `, + 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) { + 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] = { + itemId: row.itemId, + itemVariantId: row.itemVariantId, + stockBalance: row.balance, + stockStatus: getStockStatus(row.balance), + _stockBalanceRow: row, + }; + } + + return result; +} diff --git a/libs/items/src/services/stocks/get-stock-balance/types.ts b/libs/items/src/services/stocks/get-stock-balance/types.ts new file mode 100644 index 00000000..4d8b9729 --- /dev/null +++ b/libs/items/src/services/stocks/get-stock-balance/types.ts @@ -0,0 +1,15 @@ +import { getStockBalances } from './service'; +import { GetStockBalanceInputSchemaType } from './zod'; + +export type GetStockBalanceArgs = GetStockBalanceInputSchemaType; + +export type GetStockBalanceResult = Awaited< + ReturnType +>; + +export type StockBalanceRow = { + itemId: string; + itemVariantId: string | null; + balance: number; + hasVariants?: boolean; +}; 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/index.ts b/libs/items/src/services/stocks/index.ts new file mode 100644 index 00000000..fbf6cc42 --- /dev/null +++ b/libs/items/src/services/stocks/index.ts @@ -0,0 +1,4 @@ +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/reconcile-stock/index.ts b/libs/items/src/services/stocks/reconcile-stock/index.ts new file mode 100644 index 00000000..8a9411ad --- /dev/null +++ b/libs/items/src/services/stocks/reconcile-stock/index.ts @@ -0,0 +1,3 @@ +export * from './service'; +export * from './types'; +export * from './zod'; 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..ba9ff8d1 --- /dev/null +++ b/libs/items/src/services/stocks/reconcile-stock/service.ts @@ -0,0 +1,68 @@ +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 ?? '', + 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..bff510f3 --- /dev/null +++ b/libs/items/src/services/stocks/reconcile-stock/types.ts @@ -0,0 +1,6 @@ +import { Item } from '@prisma/client'; + +import { ReconcileStockSchemaType } from './zod'; + +export type ReconcileStockArgs = ReconcileStockSchemaType; +export type ReconcileStockResult = Item; diff --git a/libs/items/src/services/stocks/reconcile-stock/zod.ts b/libs/items/src/services/stocks/reconcile-stock/zod.ts new file mode 100644 index 00000000..3d629640 --- /dev/null +++ b/libs/items/src/services/stocks/reconcile-stock/zod.ts @@ -0,0 +1,11 @@ +import { z } from 'zod'; + +export const ReconcileStockSchema = z.object({ + refId: z.string().nullish(), + itemId: z.string(), + itemVariantId: z.string().nullish(), + qty: z.coerce.number().min(0, 'Quantity must be at least 0'), + remarks: z.string().nullish(), +}); + +export type ReconcileStockSchemaType = z.infer; diff --git a/libs/items/src/services/stocks/release-stock/index.ts b/libs/items/src/services/stocks/release-stock/index.ts new file mode 100644 index 00000000..8a9411ad --- /dev/null +++ b/libs/items/src/services/stocks/release-stock/index.ts @@ -0,0 +1,3 @@ +export * from './service'; +export * from './types'; +export * from './zod'; diff --git a/libs/items/src/services/stocks/release-stock/service.ts b/libs/items/src/services/stocks/release-stock/service.ts new file mode 100644 index 00000000..96201bc2 --- /dev/null +++ b/libs/items/src/services/stocks/release-stock/service.ts @@ -0,0 +1,64 @@ +import type { PrismaTransactionalClient } from '@vestido-ecommerce/models'; +import { VestidoError } from '@vestido-ecommerce/utils'; + +import { StockBalanceRow } from '../get-stock-balance'; +import { getStockStatus } from '../get-stock-status'; +import { ReleaseStockSchema, ReleaseStockSchemaType } from './zod'; + +export async function releaseStock( + prisma: PrismaTransactionalClient, + data: ReleaseStockSchemaType, + stockBalances: StockBalanceRow[], +) { + const validatedData = ReleaseStockSchema.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, + stockStatus: getStockStatus(newBalance), + }, + }); + } else { + await prisma.itemVariant.update({ + where: { + id: inventoryItem.itemVariantId, + }, + data: { + stockBalance: newBalance, + stockStatus: getStockStatus(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/release-stock/types.ts b/libs/items/src/services/stocks/release-stock/types.ts new file mode 100644 index 00000000..7174c741 --- /dev/null +++ b/libs/items/src/services/stocks/release-stock/types.ts @@ -0,0 +1,6 @@ +import { Item } from '@prisma/client'; + +import { ReleaseStockSchemaType } from './zod'; + +export type ReleaseStockArgs = ReleaseStockSchemaType; +export type ReleaseStockResult = Item; diff --git a/libs/items/src/services/stocks/release-stock/zod.ts b/libs/items/src/services/stocks/release-stock/zod.ts new file mode 100644 index 00000000..5f4a8dbb --- /dev/null +++ b/libs/items/src/services/stocks/release-stock/zod.ts @@ -0,0 +1,15 @@ +import { z } from 'zod'; + +export const ReleaseStockSchema = z.object({ + refId: z.string().optional(), + items: z.array( + z.object({ + itemId: z.string(), + itemVariantId: z.string().nullish(), + qty: z.coerce.number().min(1, { message: 'Quantity must be at least 1' }), + }), + ), + remarks: z.string(), +}); + +export type ReleaseStockSchemaType = z.infer; diff --git a/libs/items/src/services/stocks/reserve-stock/index.ts b/libs/items/src/services/stocks/reserve-stock/index.ts new file mode 100644 index 00000000..8a9411ad --- /dev/null +++ b/libs/items/src/services/stocks/reserve-stock/index.ts @@ -0,0 +1,3 @@ +export * from './service'; +export * from './types'; +export * from './zod'; diff --git a/libs/items/src/services/stocks/reserve-stock/service.ts b/libs/items/src/services/stocks/reserve-stock/service.ts new file mode 100644 index 00000000..c5dcfa66 --- /dev/null +++ b/libs/items/src/services/stocks/reserve-stock/service.ts @@ -0,0 +1,72 @@ +import type { PrismaTransactionalClient } from '@vestido-ecommerce/models'; +import { VestidoError } from '@vestido-ecommerce/utils'; + +import { StockBalanceRow } from '../get-stock-balance/types'; +import { getStockStatus } from '../get-stock-status'; +import { ReserveStockSchema, ReserveStockSchemaType } from './zod'; + +export async function reserveStock( + prisma: PrismaTransactionalClient, + data: ReserveStockSchemaType, + stockBalances: StockBalanceRow[], +) { + const validatedData = ReserveStockSchema.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, + }); + } + + if (stockRow.balance < inventoryItem.qty) { + throw new VestidoError({ + name: 'Not Enough Stock', + message: `Not enough stock to reserve for item ${inventoryItem.itemId}`, + httpStatus: 400, + }); + } + const newBalance = stockRow.balance - inventoryItem.qty; + + if (inventoryItem.itemVariantId === null) { + await prisma.item.update({ + where: { + id: inventoryItem.itemId, + }, + data: { + stockBalance: newBalance, + stockStatus: getStockStatus(newBalance), + }, + }); + } else { + await prisma.itemVariant.update({ + where: { + id: inventoryItem.itemVariantId, + }, + data: { + stockBalance: newBalance, + stockStatus: getStockStatus(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/reserve-stock/types.ts b/libs/items/src/services/stocks/reserve-stock/types.ts new file mode 100644 index 00000000..6de53c1f --- /dev/null +++ b/libs/items/src/services/stocks/reserve-stock/types.ts @@ -0,0 +1,6 @@ +import { Item } from '@prisma/client'; + +import { ReserveStockSchemaType } from './zod'; + +export type ReserveInventoryArgs = ReserveStockSchemaType; +export type ReserveInventoryResult = Item; diff --git a/libs/items/src/services/stocks/reserve-stock/zod.ts b/libs/items/src/services/stocks/reserve-stock/zod.ts new file mode 100644 index 00000000..b668df19 --- /dev/null +++ b/libs/items/src/services/stocks/reserve-stock/zod.ts @@ -0,0 +1,15 @@ +import { z } from 'zod'; + +export const ReserveStockSchema = z.object({ + refId: z.string().optional(), + items: z.array( + z.object({ + itemId: z.string(), + itemVariantId: z.string().nullish(), + qty: z.coerce.number().min(1, { message: 'Quantity must be at least 1' }), + }), + ), + remarks: z.string(), +}); + +export type ReserveStockSchemaType = z.infer; diff --git a/libs/items/src/services/variants/create-variant/service.ts b/libs/items/src/services/variants/create-variant/service.ts index 903f3b60..abc1171f 100644 --- a/libs/items/src/services/variants/create-variant/service.ts +++ b/libs/items/src/services/variants/create-variant/service.ts @@ -5,9 +5,10 @@ import { validateSlug } from '../../slug'; import { generateVariantTitle } from '../generate_variant_title'; import { variantDetails } from '../get-variant'; import { validateAttributes } from '../validate_attributes'; -import { CreateVariantSchema, CreateVariantSchemaType } from './zod'; +import { CreateVariantArgs } from './types'; +import { CreateVariantSchema } from './zod'; -export async function createVariant(data: CreateVariantSchemaType) { +export async function createVariant(data: CreateVariantArgs) { const prisma = getPrismaClient(); // validate zod here diff --git a/libs/items/src/services/variants/create-variant/types.ts b/libs/items/src/services/variants/create-variant/types.ts index bab043ff..f5f3aafc 100644 --- a/libs/items/src/services/variants/create-variant/types.ts +++ b/libs/items/src/services/variants/create-variant/types.ts @@ -1,9 +1,6 @@ -import { ItemVariantWithAttributes } from '../types'; +import { createVariant } from './service'; import { CreateVariantSchemaType } from './zod'; -export type CreateVariantRequest = { - data: CreateVariantSchemaType; -}; -export type CreateVariantResponse = { - data: ItemVariantWithAttributes; -}; +export type CreateVariantArgs = CreateVariantSchemaType; +// export type CreateVariantResponse = ItemVariantWithAttributes; +export type CreateVariantResult = Awaited>; diff --git a/libs/items/src/services/variants/get-variant/types.ts b/libs/items/src/services/variants/get-variant/types.ts index 0a41066a..051653bd 100644 --- a/libs/items/src/services/variants/get-variant/types.ts +++ b/libs/items/src/services/variants/get-variant/types.ts @@ -1,5 +1,3 @@ -import { ItemVariantWithAttributes } from '../types'; +import { variantDetails } from './service'; -export type VariantDetailsResponse = { - data: ItemVariantWithAttributes; -}; +export type VariantDetailsResult = Awaited>; diff --git a/libs/items/src/services/variants/list-variants/types.ts b/libs/items/src/services/variants/list-variants/types.ts index 1fb567ad..7fa011df 100644 --- a/libs/items/src/services/variants/list-variants/types.ts +++ b/libs/items/src/services/variants/list-variants/types.ts @@ -1,5 +1,3 @@ -import { ItemVariantWithAttributes } from '../types'; +import { listVariants } from './service'; -export type ListVariantsResponse = { - data: Array; -}; +export type VariantListResult = Awaited>; diff --git a/libs/items/src/services/variants/update-variant/service.ts b/libs/items/src/services/variants/update-variant/service.ts index a4ccbe38..c9b534a1 100644 --- a/libs/items/src/services/variants/update-variant/service.ts +++ b/libs/items/src/services/variants/update-variant/service.ts @@ -5,11 +5,12 @@ import { validateSlug } from '../../slug'; import { generateVariantTitle } from '../generate_variant_title'; import { variantDetails } from '../get-variant'; import { validateAttributes } from '../validate_attributes'; -import { UpdateVariantSchema, UpdateVariantSchemaType } from './zod'; +import { UpdateVariantArgs } from './types'; +import { UpdateVariantSchema } from './zod'; export async function updateVariant( variantId: string, - data: UpdateVariantSchemaType, + data: UpdateVariantArgs, ) { const prisma = getPrismaClient(); diff --git a/libs/items/src/services/variants/update-variant/types.ts b/libs/items/src/services/variants/update-variant/types.ts index 1e4bd241..272b1289 100644 --- a/libs/items/src/services/variants/update-variant/types.ts +++ b/libs/items/src/services/variants/update-variant/types.ts @@ -1,11 +1,6 @@ -import { ItemVariant, VariantAttributeValue } from '@prisma/client'; - +import { updateVariant } from './service'; import { UpdateVariantSchemaType } from './zod'; -export type UpdateVariantRequest = UpdateVariantSchemaType; +export type UpdateVariantArgs = UpdateVariantSchemaType; -export type UpdateVariantResponse = { - data: ItemVariant & { - attributeValues: VariantAttributeValue[]; - }; -}; +export type UpdateVariantResult = Awaited>; diff --git a/libs/items/src/services/wishlist/add-to-wishlist/service.ts b/libs/items/src/services/wishlist/add-to-wishlist/service.ts index d7c316e2..aa09e282 100644 --- a/libs/items/src/services/wishlist/add-to-wishlist/service.ts +++ b/libs/items/src/services/wishlist/add-to-wishlist/service.ts @@ -1,8 +1,9 @@ import { getPrismaClient } from '@vestido-ecommerce/models'; -import { AddToWishlistSchema, AddToWishlistSchemaType } from './zod'; +import { AddToWishlistArgs } from './types'; +import { AddToWishlistSchema } from './zod'; -export async function addToWishlist(data: AddToWishlistSchemaType) { +export async function addToWishlist(data: AddToWishlistArgs) { const prisma = getPrismaClient(); const validatedData = AddToWishlistSchema.parse(data); diff --git a/libs/items/src/services/wishlist/add-to-wishlist/types.ts b/libs/items/src/services/wishlist/add-to-wishlist/types.ts index 6a3676ea..38284059 100644 --- a/libs/items/src/services/wishlist/add-to-wishlist/types.ts +++ b/libs/items/src/services/wishlist/add-to-wishlist/types.ts @@ -1,9 +1,6 @@ -import { WishlistItem } from '@prisma/client'; - +import { addToWishlist } from './service'; import { AddToWishlistSchemaType } from './zod'; -export type AddToWishlistRequest = AddToWishlistSchemaType; +export type AddToWishlistArgs = AddToWishlistSchemaType; -export type AddToWishlistResponse = { - data: WishlistItem; -}; +export type AddToWishlistResult = Awaited>; diff --git a/libs/items/src/services/wishlist/get-wishlist/types.ts b/libs/items/src/services/wishlist/get-wishlist/types.ts index f6f203eb..0a06b486 100644 --- a/libs/items/src/services/wishlist/get-wishlist/types.ts +++ b/libs/items/src/services/wishlist/get-wishlist/types.ts @@ -1,9 +1,3 @@ -import { Item, WishlistItem } from '@prisma/client'; +import { listWishlistItems } from './service'; -export type WishlistItemResponse = { - data: Array< - WishlistItem & { - item: Item; - } - >; -}; +export type WishlistItemResult = Awaited>; diff --git a/libs/items/src/services/wishlist/remove-from-wishlist/service.ts b/libs/items/src/services/wishlist/remove-from-wishlist/service.ts index f339d734..867ae7ed 100644 --- a/libs/items/src/services/wishlist/remove-from-wishlist/service.ts +++ b/libs/items/src/services/wishlist/remove-from-wishlist/service.ts @@ -1,8 +1,9 @@ import { getPrismaClient } from '@vestido-ecommerce/models'; -import { RemoveFromWishlistSchema, RemoveFromWishlistSchemaType } from './zod'; +import { RemoveFromWishlistArgs } from './types'; +import { RemoveFromWishlistSchema } from './zod'; -export async function removeFromWishlist(body: RemoveFromWishlistSchemaType) { +export async function removeFromWishlist(body: RemoveFromWishlistArgs) { const prisma = getPrismaClient(); const validatedData = RemoveFromWishlistSchema.parse(body); diff --git a/libs/items/src/services/wishlist/remove-from-wishlist/types.ts b/libs/items/src/services/wishlist/remove-from-wishlist/types.ts index cafdcfda..0f953273 100644 --- a/libs/items/src/services/wishlist/remove-from-wishlist/types.ts +++ b/libs/items/src/services/wishlist/remove-from-wishlist/types.ts @@ -1,9 +1,8 @@ -import { WishlistItem } from '@prisma/client'; - +import { removeFromWishlist } from './service'; import { RemoveFromWishlistSchemaType } from './zod'; -export type RemoveFromWishlistRequest = RemoveFromWishlistSchemaType; +export type RemoveFromWishlistArgs = RemoveFromWishlistSchemaType; -export type RemoveFromWishlistResponse = { - data: WishlistItem; -}; +export type RemoveFromWishlistResult = Awaited< + ReturnType +>; diff --git a/libs/items/src/swr/attributes/list-attributes/list-attributes.ts b/libs/items/src/swr/attributes/list-attributes/list-attributes.ts index 8be49dcd..59736852 100644 --- a/libs/items/src/swr/attributes/list-attributes/list-attributes.ts +++ b/libs/items/src/swr/attributes/list-attributes/list-attributes.ts @@ -2,12 +2,11 @@ import useSWRImmutable from 'swr/immutable'; import { useAuth } from '@vestido-ecommerce/auth/client'; -import { ListAttributesRequest } from '../../../services/attributes/list-attributes/types'; import { AttributeListSWRKeys } from '../keys'; import { getAttributesList } from './service'; -import { AttributeListResponse } from './types'; +import { ListAttributeRequest, ListAttributeResponse } from './types'; -export function useAttributes(args?: ListAttributesRequest) { +export function useAttributes(args?: ListAttributeRequest) { const { authHeaders } = useAuth(); const key = [ AttributeListSWRKeys.ATTRIBUTE, @@ -15,7 +14,7 @@ export function useAttributes(args?: ListAttributesRequest) { JSON.stringify(args ?? {}), ]; - return useSWRImmutable( + return useSWRImmutable( key, () => getAttributesList( diff --git a/libs/items/src/swr/attributes/list-attributes/service.ts b/libs/items/src/swr/attributes/list-attributes/service.ts index 9c66cd90..a634f206 100644 --- a/libs/items/src/swr/attributes/list-attributes/service.ts +++ b/libs/items/src/swr/attributes/list-attributes/service.ts @@ -1,12 +1,11 @@ import { handleVestidoErrorResponse } from '@vestido-ecommerce/utils'; -import { ListAttributesRequest } from '../../../services/attributes/list-attributes/types'; -import { AttributeListResponse } from './types'; +import { ListAttributeRequest, ListAttributeResponse } from './types'; export async function getAttributesList( - args: ListAttributesRequest, + args: ListAttributeRequest, headers?: Record, -): Promise { +): Promise { let url = '/api/attributes'; if (args.q) { const encodedQuery = encodeURIComponent(args.q); @@ -22,5 +21,5 @@ export async function getAttributesList( } const data = await r.json(); - return data as AttributeListResponse; + return data as ListAttributeResponse; } diff --git a/libs/items/src/swr/attributes/list-attributes/types.ts b/libs/items/src/swr/attributes/list-attributes/types.ts index c5cca146..fd331e26 100644 --- a/libs/items/src/swr/attributes/list-attributes/types.ts +++ b/libs/items/src/swr/attributes/list-attributes/types.ts @@ -1,5 +1,10 @@ import { VestidoResponse } from '@vestido-ecommerce/auth/client'; -import { type ListAttributesResponse as _ListAttributesResponse } from '../../../services/attributes/list-attributes/types'; +import { + ListAttributeArgs, + ListAttributesResult, +} from '../../../services/attributes/list-attributes/types'; -export type AttributeListResponse = VestidoResponse<_ListAttributesResponse>; +export type ListAttributeRequest = ListAttributeArgs; + +export type ListAttributeResponse = VestidoResponse; diff --git a/libs/items/src/swr/attributes/upsert-attribute/service.ts b/libs/items/src/swr/attributes/upsert-attribute/service.ts index 0ca7e9d9..f4e3a55a 100644 --- a/libs/items/src/swr/attributes/upsert-attribute/service.ts +++ b/libs/items/src/swr/attributes/upsert-attribute/service.ts @@ -1,11 +1,11 @@ import { handleVestidoErrorResponse } from '@vestido-ecommerce/utils'; -import { attributeUpsertRequest, attributeUpsertResponse } from './types'; +import { UpsertAttributeRequest, UpsertAttributeResponse } from './types'; export async function upsertAttribute( - args: attributeUpsertRequest, + args: UpsertAttributeRequest, headers?: Record, -): Promise { +): Promise { let url = '/api/attributes'; let method = 'POST'; const attributeId = args.id; @@ -29,5 +29,5 @@ export async function upsertAttribute( } const data = await r.json(); - return data as attributeUpsertResponse; + return data as UpsertAttributeResponse; } diff --git a/libs/items/src/swr/attributes/upsert-attribute/types.ts b/libs/items/src/swr/attributes/upsert-attribute/types.ts index 8c0539ff..c87dc8bf 100644 --- a/libs/items/src/swr/attributes/upsert-attribute/types.ts +++ b/libs/items/src/swr/attributes/upsert-attribute/types.ts @@ -1,9 +1,17 @@ -import { ItemAttribute, ItemAttributeValue } from '@prisma/client'; +import { VestidoResponse } from '@vestido-ecommerce/utils'; -export type attributeUpsertRequest = Partial; +import { + CreateAttributeArgs, + CreateAttributeResult, + UpdateAttributeArgs, + UpdateAttributeResult, +} from '../../../services'; -export type attributeUpsertResponse = { - data: ItemAttribute & { - values: ItemAttributeValue[]; - }; -}; +export type UpsertAttributeRequest = ( + | CreateAttributeArgs + | UpdateAttributeArgs +) & { id?: string }; + +export type UpsertAttributeResponse = VestidoResponse< + CreateAttributeResult | UpdateAttributeResult +>; diff --git a/libs/items/src/swr/attributes/upsert-attribute/upsert-attribute.ts b/libs/items/src/swr/attributes/upsert-attribute/upsert-attribute.ts index 5d075149..f2db8e92 100644 --- a/libs/items/src/swr/attributes/upsert-attribute/upsert-attribute.ts +++ b/libs/items/src/swr/attributes/upsert-attribute/upsert-attribute.ts @@ -5,17 +5,17 @@ import { useClearCacheOnSuccess } from '@vestido-ecommerce/utils'; import { AttributeUpsertSWRKeys } from '../keys'; import { upsertAttribute } from './service'; -import { attributeUpsertRequest, attributeUpsertResponse } from './types'; +import { UpsertAttributeRequest, UpsertAttributeResponse } from './types'; export const useAttributeUpsert = () => { const { authHeaders } = useAuth(); const key = [AttributeUpsertSWRKeys.ATTRIBUTE, AttributeUpsertSWRKeys.UPSERT]; return useSWRMutation< - attributeUpsertResponse, + UpsertAttributeResponse, Error, string[] | null, - attributeUpsertRequest + UpsertAttributeRequest >(key, (_, { arg }) => upsertAttribute({ ...arg }, authHeaders), { ...useClearCacheOnSuccess(AttributeUpsertSWRKeys.ATTRIBUTE), }); diff --git a/libs/items/src/swr/cart/add-cart/add-cart.ts b/libs/items/src/swr/cart/add-cart/add-cart.ts index 266adcba..23418558 100644 --- a/libs/items/src/swr/cart/add-cart/add-cart.ts +++ b/libs/items/src/swr/cart/add-cart/add-cart.ts @@ -5,17 +5,17 @@ import { useClearCacheOnSuccess } from '@vestido-ecommerce/utils'; import { CartSWRKeys } from '../keys'; import { addCartItem } from './service'; -import { AddCartRequest, AddCartResponse } from './types'; +import { AddToCartRequest, AddToCartResponse } from './types'; export const useAddToCart = () => { const { isAuthenticated, authHeaders } = useAuth(); const key = isAuthenticated ? [CartSWRKeys.CART] : null; return useSWRMutation< - AddCartResponse, + AddToCartResponse, Error, string[] | null, - AddCartRequest + AddToCartRequest >(key, (_, { arg }) => addCartItem({ ...arg }, authHeaders), { ...useClearCacheOnSuccess(CartSWRKeys.CART, false), }); diff --git a/libs/items/src/swr/cart/add-cart/service.ts b/libs/items/src/swr/cart/add-cart/service.ts index 50671fd7..f7abf85e 100644 --- a/libs/items/src/swr/cart/add-cart/service.ts +++ b/libs/items/src/swr/cart/add-cart/service.ts @@ -1,11 +1,11 @@ import { handleVestidoErrorResponse } from '@vestido-ecommerce/utils'; -import { AddCartRequest, AddCartResponse } from './types'; +import { AddToCartRequest, AddToCartResponse } from './types'; export async function addCartItem( - args: AddCartRequest, + args: AddToCartRequest, authHeaders: Record, -): Promise { +): Promise { const r = await fetch('/api/cart', { method: 'POST', headers: { @@ -20,5 +20,5 @@ export async function addCartItem( } const data = await r.json(); - return data as AddCartResponse; + return data as AddToCartResponse; } diff --git a/libs/items/src/swr/cart/add-cart/types.ts b/libs/items/src/swr/cart/add-cart/types.ts index b0bac2c0..4deed74f 100644 --- a/libs/items/src/swr/cart/add-cart/types.ts +++ b/libs/items/src/swr/cart/add-cart/types.ts @@ -1,10 +1,11 @@ -import { CartItem } from '@prisma/client'; +import { VestidoResponse } from '@vestido-ecommerce/utils'; -export type AddCartRequest = Omit< - CartItem, - 'id' | 'customerId' | 'createdAt' | 'updatedAt' ->; +import { AddToCartArgs, AddToCartResult } from '../../../services'; -export type AddCartResponse = { - data: CartItem; -}; +// export type AddCartRequest = Omit< +// CartItem, +// 'id' | 'customerId' | 'createdAt' | 'updatedAt' +// >; + +export type AddToCartRequest = AddToCartArgs; +export type AddToCartResponse = VestidoResponse; diff --git a/libs/items/src/swr/cart/get-cart/get-cart.ts b/libs/items/src/swr/cart/get-cart/get-cart.ts index 066fc5bf..7d5d8875 100644 --- a/libs/items/src/swr/cart/get-cart/get-cart.ts +++ b/libs/items/src/swr/cart/get-cart/get-cart.ts @@ -2,15 +2,15 @@ import useSWRImmutable from 'swr/immutable'; import { useAuth } from '@vestido-ecommerce/auth/client'; -import { CartItemResponse } from '../../../services/cart/get-cart'; import { CartSWRKeys } from '../keys'; import { getCartItems } from './service'; +import { GetCartResponse } from './types'; export function useCart() { const { isAuthenticated, authHeaders } = useAuth(); const key = isAuthenticated ? [CartSWRKeys.CART] : null; - return useSWRImmutable(key, () => + return useSWRImmutable(key, () => getCartItems(authHeaders), ); } diff --git a/libs/items/src/swr/cart/get-cart/service.ts b/libs/items/src/swr/cart/get-cart/service.ts index 59a498a6..a00c1626 100644 --- a/libs/items/src/swr/cart/get-cart/service.ts +++ b/libs/items/src/swr/cart/get-cart/service.ts @@ -1,10 +1,10 @@ import { handleVestidoErrorResponse } from '@vestido-ecommerce/utils'; -import { CartItemResponse } from '../../../services/cart/get-cart/types'; +import { GetCartResponse } from './types'; export async function getCartItems( authHeaders: Record, -): Promise { +): Promise { const r = await fetch('/api/cart', { headers: { ...authHeaders, @@ -16,5 +16,5 @@ export async function getCartItems( } const data = await r.json(); - return data as CartItemResponse; + return data as GetCartResponse; } diff --git a/libs/items/src/swr/cart/get-cart/types.ts b/libs/items/src/swr/cart/get-cart/types.ts new file mode 100644 index 00000000..32f199c3 --- /dev/null +++ b/libs/items/src/swr/cart/get-cart/types.ts @@ -0,0 +1,5 @@ +import { VestidoResponse } from '@vestido-ecommerce/utils'; + +import { CartItemResult } from '../../../services'; + +export type GetCartResponse = VestidoResponse; diff --git a/libs/items/src/swr/cart/remove-cart/remove-cart.ts b/libs/items/src/swr/cart/remove-cart/remove-cart.ts index c880b562..a11ffcdf 100644 --- a/libs/items/src/swr/cart/remove-cart/remove-cart.ts +++ b/libs/items/src/swr/cart/remove-cart/remove-cart.ts @@ -3,10 +3,9 @@ import useSWRMutation from 'swr/mutation'; import { useAuth } from '@vestido-ecommerce/auth/client'; import { useClearCacheOnSuccess } from '@vestido-ecommerce/utils'; -import { RemoveFromCartResponse } from '../../../services/cart/remove-from-cart/types'; import { CartSWRKeys } from '../keys'; import { removeCartItem } from './service'; -import { RemoveFromCartSWRRequest } from './types'; +import { RemoveFromCartRequest, RemoveFromCartResponse } from './types'; export const useRemoveFromCart = () => { const { isAuthenticated, authHeaders } = useAuth(); @@ -16,7 +15,7 @@ export const useRemoveFromCart = () => { RemoveFromCartResponse, Error, string[] | null, - RemoveFromCartSWRRequest + RemoveFromCartRequest >(key, (_, { arg }) => removeCartItem({ ...arg }, authHeaders), { ...useClearCacheOnSuccess(CartSWRKeys.CART, false), }); diff --git a/libs/items/src/swr/cart/remove-cart/service.ts b/libs/items/src/swr/cart/remove-cart/service.ts index 5f054eb3..932e1666 100644 --- a/libs/items/src/swr/cart/remove-cart/service.ts +++ b/libs/items/src/swr/cart/remove-cart/service.ts @@ -1,10 +1,9 @@ import { handleVestidoErrorResponse } from '@vestido-ecommerce/utils'; -import { RemoveFromCartResponse } from '../../../services/cart/remove-from-cart/types'; -import { RemoveFromCartSWRRequest } from './types'; +import { RemoveFromCartRequest, RemoveFromCartResponse } from './types'; export async function removeCartItem( - args: RemoveFromCartSWRRequest, + args: RemoveFromCartRequest, authHeaders: Record, ): Promise { const params = new URLSearchParams(); diff --git a/libs/items/src/swr/cart/remove-cart/types.ts b/libs/items/src/swr/cart/remove-cart/types.ts index 9d67d15f..4f5f3571 100644 --- a/libs/items/src/swr/cart/remove-cart/types.ts +++ b/libs/items/src/swr/cart/remove-cart/types.ts @@ -1,6 +1,10 @@ -import { RemoveFromCartRequest } from '../../../services/cart/remove-from-cart/types'; +import { VestidoResponse } from '@vestido-ecommerce/utils'; -export type RemoveFromCartSWRRequest = Omit< - RemoveFromCartRequest, - 'customerId' ->; +import { + RemoveFromCartArgs, + RemoveFromCartResult, +} from '../../../services/cart/remove-from-cart/types'; + +export type RemoveFromCartRequest = Omit; + +export type RemoveFromCartResponse = VestidoResponse; diff --git a/libs/items/src/swr/category/get-category/service.ts b/libs/items/src/swr/category/get-category/service.ts index be4b3322..1c759d98 100644 --- a/libs/items/src/swr/category/get-category/service.ts +++ b/libs/items/src/swr/category/get-category/service.ts @@ -1,6 +1,6 @@ import { handleVestidoErrorResponse } from '@vestido-ecommerce/utils'; -import { CategoryDetailsResponse } from '../../../services/categories/get-category/types'; +import { CategoryDetailsResponse } from './types'; export async function getCategoryDetails( categoryId: string, diff --git a/libs/items/src/swr/category/get-category/types.ts b/libs/items/src/swr/category/get-category/types.ts new file mode 100644 index 00000000..79847466 --- /dev/null +++ b/libs/items/src/swr/category/get-category/types.ts @@ -0,0 +1,5 @@ +import { VestidoResponse } from '@vestido-ecommerce/utils'; + +import { CategoryDetailsResult } from '../../../services'; + +export type CategoryDetailsResponse = VestidoResponse; diff --git a/libs/items/src/swr/category/list-category/list-category.ts b/libs/items/src/swr/category/list-category/list-category.ts index a4de68e1..7760a4b6 100644 --- a/libs/items/src/swr/category/list-category/list-category.ts +++ b/libs/items/src/swr/category/list-category/list-category.ts @@ -2,10 +2,9 @@ import useSWRImmutable from 'swr/immutable'; import { useAuth } from '@vestido-ecommerce/auth/client'; -import { ListCategoryRequest } from '../../../services/categories/list-category/types'; import { CategorySWRKeys } from '../keys'; import { getCategoriesList } from './service'; -import { ListCategoryResponse } from './types'; +import { ListCategoryRequest, ListCategoryResponse } from './types'; export function useCategories(args?: ListCategoryRequest) { const { authHeaders } = useAuth(); diff --git a/libs/items/src/swr/category/list-category/service.ts b/libs/items/src/swr/category/list-category/service.ts index e4d99de8..e5eb138f 100644 --- a/libs/items/src/swr/category/list-category/service.ts +++ b/libs/items/src/swr/category/list-category/service.ts @@ -1,7 +1,6 @@ import { handleVestidoErrorResponse } from '@vestido-ecommerce/utils'; -import { ListCategoryRequest } from '../../../services/categories/list-category/types'; -import { ListCategoryResponse } from './types'; +import { ListCategoryRequest, ListCategoryResponse } from './types'; export async function getCategoriesList( args: ListCategoryRequest, diff --git a/libs/items/src/swr/category/list-category/types.ts b/libs/items/src/swr/category/list-category/types.ts index 27325a37..c139b59b 100644 --- a/libs/items/src/swr/category/list-category/types.ts +++ b/libs/items/src/swr/category/list-category/types.ts @@ -1,5 +1,8 @@ import { VestidoResponse } from '@vestido-ecommerce/auth/client'; -import { type ListCategoriesResponse as _ListCategoriesResponse } from '../../../services/categories/list-category/types'; - -export type ListCategoryResponse = VestidoResponse<_ListCategoriesResponse>; +import { + ListCategoryArgs, + ListCategoryResult, +} from '../../../services/categories/list-category/types'; +export type ListCategoryRequest = ListCategoryArgs; +export type ListCategoryResponse = VestidoResponse; diff --git a/libs/items/src/swr/category/upsert-category/service.ts b/libs/items/src/swr/category/upsert-category/service.ts index d9cff41f..d52a1b8a 100644 --- a/libs/items/src/swr/category/upsert-category/service.ts +++ b/libs/items/src/swr/category/upsert-category/service.ts @@ -1,11 +1,11 @@ import { handleVestidoErrorResponse } from '@vestido-ecommerce/utils'; -import { categoryUpsertRequest, categoryUpsertResponse } from './types'; +import { CategoryUpsertRequest, CategoryUpsertResponse } from './types'; export async function upsertCategory( - args: categoryUpsertRequest, + args: CategoryUpsertRequest, headers?: Record, -): Promise { +): Promise { let url = '/api/categories'; let method = 'POST'; const categoryId = args.id; @@ -29,5 +29,5 @@ export async function upsertCategory( } const data = await r.json(); - return data as categoryUpsertResponse; + return data as CategoryUpsertResponse; } diff --git a/libs/items/src/swr/category/upsert-category/types.ts b/libs/items/src/swr/category/upsert-category/types.ts index 65c6d4bf..1e002b2c 100644 --- a/libs/items/src/swr/category/upsert-category/types.ts +++ b/libs/items/src/swr/category/upsert-category/types.ts @@ -1,7 +1,17 @@ -import { Category } from '@prisma/client'; +import { VestidoResponse } from '@vestido-ecommerce/utils'; -export type categoryUpsertRequest = Partial; +import { + CreateCategoryArgs, + CreateCategoryResult, + UpdateCategoryArgs, + UpdateCategoryResult, +} from '../../../services'; -export type categoryUpsertResponse = { - data: Category; -}; +export type CategoryUpsertRequest = ( + | CreateCategoryArgs + | UpdateCategoryArgs +) & { id?: string }; + +export type CategoryUpsertResponse = VestidoResponse< + CreateCategoryResult | UpdateCategoryResult +>; diff --git a/libs/items/src/swr/category/upsert-category/upsert-category.ts b/libs/items/src/swr/category/upsert-category/upsert-category.ts index d5a9ae38..47af2a96 100644 --- a/libs/items/src/swr/category/upsert-category/upsert-category.ts +++ b/libs/items/src/swr/category/upsert-category/upsert-category.ts @@ -5,7 +5,7 @@ import { useClearCacheOnSuccess } from '@vestido-ecommerce/utils'; import { CategorySWRKeys } from '../keys'; import { upsertCategory } from './service'; -import { categoryUpsertRequest, categoryUpsertResponse } from './types'; +import { CategoryUpsertRequest, categoryUpsertResponse } from './types'; export const useCategoryUpsert = () => { const { authHeaders } = useAuth(); @@ -15,7 +15,7 @@ export const useCategoryUpsert = () => { categoryUpsertResponse, Error, string[] | null, - categoryUpsertRequest + CategoryUpsertRequest >(key, (_, { arg }) => upsertCategory({ ...arg }, authHeaders), { ...useClearCacheOnSuccess(CategorySWRKeys.CATEGORY), }); 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/item/get-item/get-item.ts b/libs/items/src/swr/item/get-item/get-item.ts index 5541bda8..30af0e83 100644 --- a/libs/items/src/swr/item/get-item/get-item.ts +++ b/libs/items/src/swr/item/get-item/get-item.ts @@ -2,9 +2,9 @@ import useSWRImmutable from 'swr/immutable'; import { useAuth } from '@vestido-ecommerce/auth/client'; -import { ItemDetailsResponse } from '../../../services/items/get-item/types'; import { ItemDetailsSWRKeys } from '../keys'; import { getItemDetails } from './service'; +import { ItemDetailsResponse } from './types'; export function useItem(itemId?: string | null) { const { authHeaders } = useAuth(); diff --git a/libs/items/src/swr/item/get-item/service.ts b/libs/items/src/swr/item/get-item/service.ts index 527fb0b9..03981558 100644 --- a/libs/items/src/swr/item/get-item/service.ts +++ b/libs/items/src/swr/item/get-item/service.ts @@ -1,6 +1,6 @@ import { handleVestidoErrorResponse } from '@vestido-ecommerce/utils'; -import { ItemDetailsResponse } from '../../../services/items/get-item/types'; +import { ItemDetailsResponse } from './types'; export async function getItemDetails( itemId: string, diff --git a/libs/items/src/swr/item/get-item/types.ts b/libs/items/src/swr/item/get-item/types.ts new file mode 100644 index 00000000..2f64428a --- /dev/null +++ b/libs/items/src/swr/item/get-item/types.ts @@ -0,0 +1,5 @@ +import { VestidoResponse } from '@vestido-ecommerce/utils'; + +import { ItemDetailsResult } from '../../../services'; + +export type ItemDetailsResponse = VestidoResponse; diff --git a/libs/items/src/swr/item/list-item/list-item.ts b/libs/items/src/swr/item/list-item/list-item.ts index 4affa45b..78af81de 100644 --- a/libs/items/src/swr/item/list-item/list-item.ts +++ b/libs/items/src/swr/item/list-item/list-item.ts @@ -2,10 +2,9 @@ import useSWRImmutable from 'swr/immutable'; import { useAuth } from '@vestido-ecommerce/auth/client'; -import { ListItemRequest } from '../../../services/items/list-item/types'; import { ListItemSWRKeys } from '../keys'; import { getItemList } from './service'; -import { ListItemResponse } from './types'; +import { ListItemRequest, ListItemResponse } from './types'; export function useItems(args?: ListItemRequest) { const { authHeaders } = useAuth(); diff --git a/libs/items/src/swr/item/list-item/service.ts b/libs/items/src/swr/item/list-item/service.ts index caf67bcf..9a96736f 100644 --- a/libs/items/src/swr/item/list-item/service.ts +++ b/libs/items/src/swr/item/list-item/service.ts @@ -1,7 +1,6 @@ import { handleVestidoErrorResponse } from '@vestido-ecommerce/utils'; -import { ListItemRequest } from '../../../services/items/list-item/types'; -import { ListItemResponse } from './types'; +import { ListItemRequest, ListItemResponse } from './types'; export async function getItemList( args?: ListItemRequest, diff --git a/libs/items/src/swr/item/list-item/types.ts b/libs/items/src/swr/item/list-item/types.ts index f149f7ab..4efdb696 100644 --- a/libs/items/src/swr/item/list-item/types.ts +++ b/libs/items/src/swr/item/list-item/types.ts @@ -1,2 +1,7 @@ -// import { SortParam } from '../../types'; -export { type ListItemResponse } from '../../../services/items/list-item/types'; +import { VestidoResponse } from '@vestido-ecommerce/utils'; + +import { ListItemArgs, ListItemResult } from '../../../services'; + +export type ListItemRequest = ListItemArgs; + +export type ListItemResponse = VestidoResponse; diff --git a/libs/items/src/swr/item/upsert-item/types.ts b/libs/items/src/swr/item/upsert-item/types.ts index 95d25cc0..a9971594 100644 --- a/libs/items/src/swr/item/upsert-item/types.ts +++ b/libs/items/src/swr/item/upsert-item/types.ts @@ -1,7 +1,13 @@ -import { ItemDetails, ItemUpsertSchemaType } from '../../../services'; +import { VestidoResponse } from '@vestido-ecommerce/utils'; + +import { + CreateItemResult, + ItemUpsertSchemaType, + UpdateItemResult, +} from '../../../services'; export type ItemUpsertRequest = ItemUpsertSchemaType; -export type ItemUpsertResponse = { - data: ItemDetails; -}; +export type ItemUpsertResponse = + | VestidoResponse + | VestidoResponse; 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..2173238d --- /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 { StockSWRKeys } from '../keys'; +import { getStockBalance } from './service'; +import { GetStockBalanceRequest, GetStockBalanceResponse } from './types'; + +export function useStockBalance(args: GetStockBalanceRequest) { + const { authHeaders } = useAuth(); + const key = [StockSWRKeys.STOCKBALANCE, StockSWRKeys.GET, args]; + 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..ed7daff2 --- /dev/null +++ b/libs/items/src/swr/stocks/getStockBalance/service.ts @@ -0,0 +1,21 @@ +import { handleVestidoErrorResponse } from '@vestido-ecommerce/utils'; + +import { GetStockBalanceRequest, GetStockBalanceResponse } from './types'; + +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/getStockBalance/types.ts b/libs/items/src/swr/stocks/getStockBalance/types.ts new file mode 100644 index 00000000..a2399d7b --- /dev/null +++ b/libs/items/src/swr/stocks/getStockBalance/types.ts @@ -0,0 +1,6 @@ +import { VestidoResponse } from '@vestido-ecommerce/utils'; + +import { GetStockBalanceArgs, GetStockBalanceResult } from '../../../services'; + +export type GetStockBalanceRequest = GetStockBalanceArgs; +export type GetStockBalanceResponse = VestidoResponse; diff --git a/libs/items/src/swr/stocks/index.ts b/libs/items/src/swr/stocks/index.ts new file mode 100644 index 00000000..75e0a6ec --- /dev/null +++ b/libs/items/src/swr/stocks/index.ts @@ -0,0 +1,4 @@ +'use client'; + +export * from './getStockBalance'; +export * from './reconcileStock'; 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', +} 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..91cd4307 --- /dev/null +++ b/libs/items/src/swr/stocks/reconcileStock/reconcile-stock.ts @@ -0,0 +1,23 @@ +import useSWRMutation from 'swr/mutation'; + +import { useAuth } from '@vestido-ecommerce/auth/client'; +import { useClearCacheOnSuccess } from '@vestido-ecommerce/utils'; + +import { StockSWRKeys } from '../keys'; +import { reconcileStock } from './service'; +import { ReconcileStockRequest, ReconcileStockResponse } from './types'; + +export function useReconcileStock() { + const { authHeaders } = useAuth(); + + return useSWRMutation< + ReconcileStockResponse, + Error, + typeof StockSWRKeys.RECONCILE, + ReconcileStockRequest + >( + StockSWRKeys.RECONCILE, + (_key, { arg }) => reconcileStock(arg, authHeaders), + { ...useClearCacheOnSuccess(StockSWRKeys.STOCKBALANCE) }, + ); +} diff --git a/libs/items/src/swr/stocks/reconcileStock/service.ts b/libs/items/src/swr/stocks/reconcileStock/service.ts new file mode 100644 index 00000000..fa7d6db6 --- /dev/null +++ b/libs/items/src/swr/stocks/reconcileStock/service.ts @@ -0,0 +1,26 @@ +import { handleVestidoErrorResponse } from '@vestido-ecommerce/utils'; + +import { ReconcileStockRequest, ReconcileStockResponse } from './types'; + +export async function reconcileStock( + args: ReconcileStockRequest, + headers?: Record, +): Promise { + const url = `/api/items/${args.itemId}${args.itemVariantId ? '/variants/' + args.itemVariantId : ''}/stock`; + + const r = await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + ...(headers ?? {}), + }, + body: JSON.stringify(args), + }); + + if (!r.ok) { + await handleVestidoErrorResponse(r); + } + + const data = await r.json(); + return data as ReconcileStockResponse; +} diff --git a/libs/items/src/swr/stocks/reconcileStock/types.ts b/libs/items/src/swr/stocks/reconcileStock/types.ts new file mode 100644 index 00000000..16a4336c --- /dev/null +++ b/libs/items/src/swr/stocks/reconcileStock/types.ts @@ -0,0 +1,6 @@ +import { VestidoResponse } from '@vestido-ecommerce/utils'; + +import { ReconcileStockArgs, ReconcileStockResult } from '../../../services'; + +export type ReconcileStockRequest = ReconcileStockArgs; +export type ReconcileStockResponse = VestidoResponse; diff --git a/libs/items/src/swr/variants/get-variant/get-variant.ts b/libs/items/src/swr/variants/get-variant/get-variant.ts index 8737c59e..e6fb4f95 100644 --- a/libs/items/src/swr/variants/get-variant/get-variant.ts +++ b/libs/items/src/swr/variants/get-variant/get-variant.ts @@ -2,9 +2,9 @@ import useSWRImmutable from 'swr/immutable'; import { useAuth } from '@vestido-ecommerce/auth/client'; -import { VariantDetailsResponse } from '../../../services/variants/get-variant/types'; import { VariantDetailsSWRKeys } from '../keys'; import { getVariantDetails } from './service'; +import { VariantDetailsResponse } from './types'; export function useVariant(itemId: string, variantId?: string | null) { const { authHeaders } = useAuth(); diff --git a/libs/items/src/swr/variants/get-variant/service.ts b/libs/items/src/swr/variants/get-variant/service.ts index e213dd6f..5e1f9729 100644 --- a/libs/items/src/swr/variants/get-variant/service.ts +++ b/libs/items/src/swr/variants/get-variant/service.ts @@ -1,12 +1,12 @@ import { handleVestidoErrorResponse } from '@vestido-ecommerce/utils'; -import { variantDetailsResponse } from './types'; +import { VariantDetailsResponse } from './types'; export async function getVariantDetails( variantId: string, itemId: string, headers?: Record, -): Promise { +): Promise { const url = `/api/items/${itemId}/variants/${encodeURIComponent(variantId)}`; const r = await fetch(url, { headers: { @@ -18,5 +18,5 @@ export async function getVariantDetails( } const data = await r.json(); - return data as variantDetailsResponse; + return data as VariantDetailsResponse; } diff --git a/libs/items/src/swr/variants/get-variant/types.ts b/libs/items/src/swr/variants/get-variant/types.ts index 85321719..fd9cfb83 100644 --- a/libs/items/src/swr/variants/get-variant/types.ts +++ b/libs/items/src/swr/variants/get-variant/types.ts @@ -1,7 +1,5 @@ -import { ItemVariant, VariantAttributeValue } from '@prisma/client'; +import { VestidoResponse } from '@vestido-ecommerce/utils'; -export type variantDetailsResponse = { - data: ItemVariant & { - attributeValues: VariantAttributeValue[]; - }; -}; +import { VariantDetailsResult } from '../../../services'; + +export type VariantDetailsResponse = VestidoResponse; diff --git a/libs/items/src/swr/variants/list-variants/types.ts b/libs/items/src/swr/variants/list-variants/types.ts index 189b473d..fcfd07c5 100644 --- a/libs/items/src/swr/variants/list-variants/types.ts +++ b/libs/items/src/swr/variants/list-variants/types.ts @@ -1,16 +1,12 @@ -import { ItemVariant, VariantAttributeValue } from '@prisma/client'; +import { VestidoResponse } from '@vestido-ecommerce/utils'; + +import { VariantListResult } from '../../../services'; export type VariantListRequest = { filters?: VariantListFilterParams; }; -export type VariantListResponse = { - data: Array< - ItemVariant & { - attributeValues: VariantAttributeValue[]; - } - >; -}; +export type VariantListResponse = VestidoResponse; type VariantListFilterParams = { // name?: string; diff --git a/libs/items/src/swr/variants/upsert-variant/service.ts b/libs/items/src/swr/variants/upsert-variant/service.ts index cb9cff56..48d83080 100644 --- a/libs/items/src/swr/variants/upsert-variant/service.ts +++ b/libs/items/src/swr/variants/upsert-variant/service.ts @@ -1,11 +1,11 @@ import { handleVestidoErrorResponse } from '@vestido-ecommerce/utils'; -import { variantUpsertRequest, variantUpsertResponse } from './types'; +import { VariantUpsertRequest, VariantUpsertResponse } from './types'; export async function upsertVariant( - args: variantUpsertRequest, + args: VariantUpsertRequest, itemId: string, -): Promise { +): Promise { let url = `/api/items/${itemId}/variants`; let method = 'POST'; const variantId = args.id; @@ -28,5 +28,5 @@ export async function upsertVariant( } const data = await r.json(); - return data as variantUpsertResponse; + return data as VariantUpsertResponse; } diff --git a/libs/items/src/swr/variants/upsert-variant/types.ts b/libs/items/src/swr/variants/upsert-variant/types.ts index 6df92e16..b1835178 100644 --- a/libs/items/src/swr/variants/upsert-variant/types.ts +++ b/libs/items/src/swr/variants/upsert-variant/types.ts @@ -1,9 +1,16 @@ -import { ItemVariant, VariantAttributeValue } from '@prisma/client'; +import { VestidoResponse } from '@vestido-ecommerce/utils'; -export type variantUpsertRequest = Partial; +import { + CreateVariantArgs, + CreateVariantResult, + UpdateVariantArgs, + UpdateVariantResult, +} from '../../../services'; -export type variantUpsertResponse = { - data: ItemVariant & { - attributeValues: VariantAttributeValue[]; - }; +export type VariantUpsertRequest = (CreateVariantArgs | UpdateVariantArgs) & { + id?: string; }; + +export type VariantUpsertResponse = VestidoResponse< + CreateVariantResult | UpdateVariantResult +>; diff --git a/libs/items/src/swr/variants/upsert-variant/upsert-variant.ts b/libs/items/src/swr/variants/upsert-variant/upsert-variant.ts index bef5ff85..25dd8615 100644 --- a/libs/items/src/swr/variants/upsert-variant/upsert-variant.ts +++ b/libs/items/src/swr/variants/upsert-variant/upsert-variant.ts @@ -4,7 +4,7 @@ import { useClearCacheOnSuccess } from '@vestido-ecommerce/utils'; import { VariantUpsertSWRKeys } from '../keys'; import { upsertVariant } from './service'; -import { variantUpsertRequest, variantUpsertResponse } from './types'; +import { VariantUpsertRequest, VariantUpsertResponse } from './types'; export const useVariantUpsert = (itemId: string) => { const key = [ @@ -14,10 +14,10 @@ export const useVariantUpsert = (itemId: string) => { ]; return useSWRMutation< - variantUpsertResponse, + VariantUpsertResponse, Error, string[] | null, - variantUpsertRequest + VariantUpsertRequest >(key, (_, { arg }) => upsertVariant({ ...arg }, itemId), { ...useClearCacheOnSuccess(VariantUpsertSWRKeys.VARIANT), }); diff --git a/libs/items/src/swr/wishlist/add-wishlist/types.ts b/libs/items/src/swr/wishlist/add-wishlist/types.ts index cf18de9c..5df5b267 100644 --- a/libs/items/src/swr/wishlist/add-wishlist/types.ts +++ b/libs/items/src/swr/wishlist/add-wishlist/types.ts @@ -1,10 +1,7 @@ -import { WishlistItem } from '@prisma/client'; +import { VestidoResponse } from '@vestido-ecommerce/utils'; -export type AddToWishListRequest = Omit< - WishlistItem, - 'id' | 'customerId' | 'variantId' | 'createdAt' | 'updatedAt' ->; +import { AddToWishlistArgs, AddToWishlistResult } from '../../../services'; -export type AddToWishListResponse = { - data: WishlistItem; -}; +export type AddToWishListRequest = AddToWishlistArgs; + +export type AddToWishListResponse = VestidoResponse; diff --git a/libs/items/src/swr/wishlist/get-wishlist/get-wishlist.ts b/libs/items/src/swr/wishlist/get-wishlist/get-wishlist.ts index 06e6b3d7..5c160a17 100644 --- a/libs/items/src/swr/wishlist/get-wishlist/get-wishlist.ts +++ b/libs/items/src/swr/wishlist/get-wishlist/get-wishlist.ts @@ -2,9 +2,9 @@ import useSWRImmutable from 'swr/immutable'; import { useAuth } from '@vestido-ecommerce/auth/client'; -import { WishlistItemResponse } from '../../../services/wishlist/get-wishlist/types'; import { WishlistSWRKeys } from '../keys'; import { getWishlist } from './service'; +import { WishlistItemResponse } from './types'; export function useWishlist() { const { isAuthenticated, authHeaders } = useAuth(); diff --git a/libs/items/src/swr/wishlist/get-wishlist/service.ts b/libs/items/src/swr/wishlist/get-wishlist/service.ts index 5f7dc8e7..870250e2 100644 --- a/libs/items/src/swr/wishlist/get-wishlist/service.ts +++ b/libs/items/src/swr/wishlist/get-wishlist/service.ts @@ -1,6 +1,6 @@ import { handleVestidoErrorResponse } from '@vestido-ecommerce/utils'; -import { WishlistItemResponse } from '../../../services/wishlist/get-wishlist/types'; +import { WishlistItemResponse } from './types'; export async function getWishlist( authHeaders: Record, diff --git a/libs/items/src/swr/wishlist/get-wishlist/types.ts b/libs/items/src/swr/wishlist/get-wishlist/types.ts new file mode 100644 index 00000000..f2d29cef --- /dev/null +++ b/libs/items/src/swr/wishlist/get-wishlist/types.ts @@ -0,0 +1,5 @@ +import { VestidoResponse } from '@vestido-ecommerce/utils'; + +import { WishlistItemResult } from '../../../services'; + +export type WishlistItemResponse = VestidoResponse; diff --git a/libs/items/src/swr/wishlist/remove-wishlist/remove-wishlist.ts b/libs/items/src/swr/wishlist/remove-wishlist/remove-wishlist.ts index 803a9ac4..8c662414 100644 --- a/libs/items/src/swr/wishlist/remove-wishlist/remove-wishlist.ts +++ b/libs/items/src/swr/wishlist/remove-wishlist/remove-wishlist.ts @@ -3,10 +3,9 @@ import useSWRMutation from 'swr/mutation'; import { useAuth } from '@vestido-ecommerce/auth/client'; import { useClearCacheOnSuccess } from '@vestido-ecommerce/utils'; -import { RemoveFromWishlistResponse } from '../../../services/wishlist/remove-from-wishlist'; import { WishlistSWRKeys } from '../keys'; import { removeFromWishList } from './service'; -import { RemoveFromWishListSwrRequest } from './types'; +import { RemoveFromWishlistRequest, RemoveFromWishlistResponse } from './types'; export const useRemoveFromWishlist = () => { const { isAuthenticated, authHeaders } = useAuth(); @@ -16,7 +15,7 @@ export const useRemoveFromWishlist = () => { RemoveFromWishlistResponse, Error, string[] | null, - RemoveFromWishListSwrRequest + RemoveFromWishlistRequest >(key, (_, { arg }) => removeFromWishList({ ...arg }, authHeaders), { ...useClearCacheOnSuccess(WishlistSWRKeys.WISHLIST), }); diff --git a/libs/items/src/swr/wishlist/remove-wishlist/service.ts b/libs/items/src/swr/wishlist/remove-wishlist/service.ts index 1118109e..9415ff0e 100644 --- a/libs/items/src/swr/wishlist/remove-wishlist/service.ts +++ b/libs/items/src/swr/wishlist/remove-wishlist/service.ts @@ -1,10 +1,9 @@ import { handleVestidoErrorResponse } from '@vestido-ecommerce/utils'; -import { RemoveFromWishlistResponse } from '../../../services/wishlist/remove-from-wishlist'; -import { RemoveFromWishListSwrRequest } from './types'; +import { RemoveFromWishlistRequest, RemoveFromWishlistResponse } from './types'; export async function removeFromWishList( - args: RemoveFromWishListSwrRequest, + args: RemoveFromWishlistRequest, authHeaders: Record, ): Promise { const r = await fetch(`/api/wishlist?itemId=${args.itemId}`, { diff --git a/libs/items/src/swr/wishlist/remove-wishlist/types.ts b/libs/items/src/swr/wishlist/remove-wishlist/types.ts index 1fd42f65..50939000 100644 --- a/libs/items/src/swr/wishlist/remove-wishlist/types.ts +++ b/libs/items/src/swr/wishlist/remove-wishlist/types.ts @@ -1,6 +1,13 @@ -import { RemoveFromWishlistRequest } from '../../../services/wishlist/remove-from-wishlist/types'; +import { VestidoResponse } from '@vestido-ecommerce/utils'; -export type RemoveFromWishListSwrRequest = Omit< - RemoveFromWishlistRequest, +import { + RemoveFromWishlistArgs, + RemoveFromWishlistResult, +} from '../../../services'; + +export type RemoveFromWishlistRequest = Omit< + RemoveFromWishlistArgs, 'customerId' >; +export type RemoveFromWishlistResponse = + VestidoResponse; 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/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 668cf74b..55276b15 100644 --- a/libs/models/prisma/schema.prisma +++ b/libs/models/prisma/schema.prisma @@ -168,9 +168,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[] @@ -187,8 +188,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 { @@ -196,6 +198,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 @@ -211,6 +214,7 @@ model ItemVariant { CartItem CartItem[] WishlistItem WishlistItem[] OrderItem OrderItem[] + StockLedger StockLedger[] } model VariantAttributeValue { @@ -524,6 +528,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 diff --git a/libs/orders/src/services/orders/cancel-order/service.ts b/libs/orders/src/services/orders/cancel-order/service.ts index 1d9dd162..c335daa3 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, releaseStock } 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', @@ -81,8 +75,6 @@ export async function cancelOrder( refundResponse = await refundRazorpay(refundData); - console.log('Refund Response: ', refundResponse); - if (!refundResponse || refundResponse.status === 'failed') { throw new VestidoError({ name: 'RazorpayRefundFailed', @@ -133,7 +125,30 @@ export async function cancelOrder( }, }); } - + //release the stock + const balances = Object.values( + await getStockBalances( + prismaTransaction, + order.orderItems.map((item) => ({ + itemId: item.itemId, + itemVariantId: item.variantId ?? undefined, + })), + ), + ).map((row) => row._stockBalanceRow); + + await releaseStock( + 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 }, diff --git a/libs/orders/src/services/orders/cancel-order/zod.ts b/libs/orders/src/services/orders/cancel-order/zod.ts index edbe12f0..584d4984 100644 --- a/libs/orders/src/services/orders/cancel-order/zod.ts +++ b/libs/orders/src/services/orders/cancel-order/zod.ts @@ -9,7 +9,7 @@ export const CancelOrderSchema = z.object({ 'NEED_TO_CHANGE_PAYMENT_METHOD', 'OTHER', ]), - remarks: z.string(), + remarks: z.string().nullable(), }); export type CancelOrderSchemaType = z.infer; diff --git a/libs/orders/src/services/orders/create-order/service.ts b/libs/orders/src/services/orders/create-order/service.ts index 4b5c0a28..501cc6c4 100644 --- a/libs/orders/src/services/orders/create-order/service.ts +++ b/libs/orders/src/services/orders/create-order/service.ts @@ -1,5 +1,8 @@ +import type { Payment } from '@prisma/client'; + import { sendSMS, SMSSenderID, SMSTemplate } from '@vestido-ecommerce/fast2sms'; import { clearCartOnOrderCreation } from '@vestido-ecommerce/items'; +import { getStockBalances, reserveStock } from '@vestido-ecommerce/items'; import { getPrismaClient } from '@vestido-ecommerce/models'; import { VestidoError } from '@vestido-ecommerce/utils'; @@ -15,9 +18,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 +29,101 @@ 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, + const { newOrder, newPayment } = await prisma.$transaction(async (prisma) => { + const createdOrder = await prisma.order.create({ + data: { + ...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', + })), + }, }, }, - shippingAddress: { - connect: { - id: addressId, + }); + + let newPayment: Payment | null = null; + + if (paymentType === 'CASH_ON_DELIVERY') { + newPayment = await prisma.payment.create({ + data: { + order: { connect: { id: createdOrder.id } }, + paymentGateway: 'CASH_ON_DELIVERY', + paymentGatewayRef: 'Null', + moreDetails: 'Null', + currency: 'INR', + amount: grandTotal, + status: 'PENDING', }, - }, - 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', - })), + }); + } else if (paymentType === 'REPLACEMENT_ORDER') { + newPayment = await prisma.payment.create({ + data: { + order: { connect: { id: createdOrder.id } }, + paymentGateway: 'REPLACEMENT_ORDER', + paymentGatewayRef: 'Null', + moreDetails: 'Null', + currency: 'INR', + amount: 0, + status: 'CAPTURED', }, - }, - }, - }); + }); + } + const balances = Object.values( + await getStockBalances( + prisma, + itemsWithTax.map((item) => ({ + itemId: item.itemId, + itemVariantId: item.variantId ?? undefined, + })), + ), + ).map((row) => row._stockBalanceRow); - let newPayment = null; - if (paymentType == 'CASH_ON_DELIVERY') { - newPayment = await prisma.payment.create({ - data: { - order: { - connect: { - id: newOrder.id, - }, - }, - paymentGateway: 'CASH_ON_DELIVERY', - paymentGatewayRef: 'Null', - moreDetails: 'Null', - currency: 'INR', - amount: grandTotal, - status: 'PENDING', + await reserveStock( + prisma, + { + refId: createdOrder.id, + remarks: 'Order Reservation', + items: itemsWithTax.map((item) => ({ + itemId: item.itemId, + itemVariantId: item.variantId ?? null, + qty: item.qty, + })), }, - }); + balances, + ); + return { newOrder: createdOrder, newPayment }; + }); + + if (paymentType === 'CASH_ON_DELIVERY') { const totalItems = itemsWithTax .reduce((sum, item) => sum + item.qty, 0) .toString(); @@ -115,45 +144,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 }; } diff --git a/third-party/razorpay/src/services/razorpay-webhook/service.ts b/third-party/razorpay/src/services/razorpay-webhook/service.ts index 58355b6c..24fc71e6 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, releaseStock } 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 = Object.values( + await getStockBalances( + transaction, + order.orderItems.map((item) => ({ + itemId: item.itemId, + itemVariantId: item.variantId ?? undefined, + })), + ), + ).map((row) => row._stockBalanceRow); + + await releaseStock( + 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 }, 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/[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; + }, +); 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..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 @@ -1,13 +1,25 @@ import { authMiddleware, roleMiddleware } from '@vestido-ecommerce/auth'; -import { createVariant, listVariants } from '@vestido-ecommerce/items'; +import { + createVariant, + getStockBalances, + listVariants, + reconcileStock, +} 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 reconcileStock({ + ...body, + itemId: params.item_id, + itemVariantId: params.variant_id, + }); + }, +); diff --git a/vestido/vestido-dashboard/modules/products/ProductForm.tsx b/vestido/vestido-dashboard/modules/products/ProductForm.tsx index 05862bc3..d9dc1647 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 = { @@ -44,6 +46,7 @@ const ProductForm: React.FC = ({ itemId, isNew }) => { 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 +92,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..319de829 100644 --- a/vestido/vestido-dashboard/modules/products/ProductSizeForm.tsx +++ b/vestido/vestido-dashboard/modules/products/ProductSizeForm.tsx @@ -5,10 +5,8 @@ import { useFieldArray, useFormContext } from 'react-hook-form'; import { useAttributes } from '@vestido-ecommerce/items/client'; 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 +52,7 @@ const ProductSizeForm: FC<{ className?: string }> = ({ className }) => { itemAttributeValueId: size.id, enabled: false, sku: null, - stockStatus: 'AVAILABLE', + stockStatus: 'OUT_OF_STOCK', } satisfies ItemVariantWithSize; }) .sort( @@ -93,22 +91,24 @@ const ProductSizeForm: FC<{ className?: string }> = ({ className }) => {
- - + OUT_OF_STOCK: 'text-red-600', + AVAILABLE: 'text-green-600', + LIMITED_STOCK: 'text-orange-500', + }[size.stockStatus] || 'text-gray-400' + }`} + > + {{ + OUT_OF_STOCK: 'Out of Stock', + AVAILABLE: 'Available', + LIMITED_STOCK: 'Limited Stock', + }[size.stockStatus] || size.stockStatus}{' '} +
+
+ {size.stockBalance ?? 0} nos. +
= ({ data, categoryId }) => { Description Has Variants Variants Count + Delete @@ -105,6 +106,7 @@ const ProductsTable: React.FC = ({ data, categoryId }) => { > {item.hasVariants ? `${item.variants.length}` : 'No variant'} + 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..b33ac783 --- /dev/null +++ b/vestido/vestido-dashboard/modules/products/update-bulk-stock-dialog.tsx @@ -0,0 +1,181 @@ +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 { useToast } from '@vestido-ecommerce/shadcn-ui/use-toast'; + +import { InputElement } from '../../forms/input-element'; +import { TextAreaElement } from '../../forms/textarea-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(); + + 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 + + +
+ +
+ {fields.map((field, index) => ( +
+ {/* Variant Name */} +
{field.name}
+ + {/* Current Stock */} +
+
Current Stock
+ +
+ + +
+ ))} +
+ + + + +
+ +
+
+ ); +} diff --git a/vestido/vestido-dashboard/modules/products/update-stock-dialog.tsx b/vestido/vestido-dashboard/modules/products/update-stock-dialog.tsx new file mode 100644 index 00000000..5e8e315a --- /dev/null +++ b/vestido/vestido-dashboard/modules/products/update-stock-dialog.tsx @@ -0,0 +1,129 @@ +import { ReactNode, useState } from 'react'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { 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 { useToast } from '@vestido-ecommerce/shadcn-ui/use-toast'; + +import { InputElement } from '../../forms/input-element'; +import { TextAreaElement } from '../../forms/textarea-element'; + +// Schema +const UpdateStockSchema = z.object({ + itemId: z.string(), + itemVariantId: z.string().nullish(), + qty: z.coerce.number().int().nonnegative(), + remarks: z.string().optional(), +}); + +type UpdateStockForm = z.infer; + +export function UpdateStockDialog({ + itemId, + itemVariantId = null, + currentStock, + onUpdated, + children, +}: { + itemId: string; + itemVariantId?: string | null; + currentStock: number; + children: ReactNode; + onUpdated?: (newQty: number) => void; +}) { + const { toast } = useToast(); + const [open, setOpen] = useState(false); + + const { trigger, isMutating } = useReconcileStock(); + + const form = useForm({ + resolver: zodResolver(UpdateStockSchema), + defaultValues: { + itemId, + itemVariantId, + qty: currentStock, + remarks: '', + }, + }); + + const handleSubmit = async (data: UpdateStockForm) => { + try { + await trigger({ + itemId: data.itemId, + itemVariantId: data.itemVariantId ?? null, + qty: data.qty, + remarks: data.remarks ?? '', + }); + + toast({ title: 'Stock updated successfully!' }); + setOpen(false); + + if (onUpdated) onUpdated(data.qty); + } catch (err) { + console.error(err); + toast({ + title: 'Error updating stock', + description: 'Please try again.', + variant: 'destructive', + }); + } + }; + + return ( + + {children} + + + Update Stock + + +
+ + + + +
+ + +
+ + + + + + +
+
+ ); +} diff --git a/vestido/vestido-storefront/modules/products/product-view.tsx b/vestido/vestido-storefront/modules/products/product-view.tsx index 3f8b03df..08dcae28 100644 --- a/vestido/vestido-storefront/modules/products/product-view.tsx +++ b/vestido/vestido-storefront/modules/products/product-view.tsx @@ -123,11 +123,11 @@ const ProductView: React.FC = ({ itemId }) => {

- {item?.stockStatus === 'LIMITED_STOCK' + {selectedVariant?.stockStatus === 'LIMITED_STOCK' ? 'Limited Stock' - : item?.stockStatus === 'OUT_OF_STOCK' + : selectedVariant?.stockStatus === 'OUT_OF_STOCK' ? 'Out of Stock' : 'Available'}