From d9652405cabe67404d927178b32a9ebd12992d5b Mon Sep 17 00:00:00 2001 From: shafi54 <108669266+shafi-aviz@users.noreply.github.com> Date: Wed, 25 Mar 2026 01:43:02 +0530 Subject: [PATCH] enh --- apps/backend/src/dbService.ts | 32 +- .../trpc/apis/admin-apis/apis/complaint.ts | 42 +- .../src/trpc/apis/admin-apis/apis/const.ts | 25 +- .../src/trpc/apis/admin-apis/apis/coupon.ts | 761 ++++++++---------- packages/db_helper_postgres/index.ts | 3 + .../src/helper_methods/complaint.ts | 74 ++ .../src/helper_methods/const.ts | 29 + .../src/helper_methods/coupon.ts | 633 +++++++++++++++ packages/shared/types/complaint.types.ts | 20 + packages/shared/types/const.types.ts | 15 + packages/shared/types/coupon.types.ts | 41 + packages/shared/types/index.ts | 3 + 12 files changed, 1216 insertions(+), 462 deletions(-) create mode 100644 packages/db_helper_postgres/src/helper_methods/complaint.ts create mode 100644 packages/db_helper_postgres/src/helper_methods/const.ts create mode 100644 packages/db_helper_postgres/src/helper_methods/coupon.ts create mode 100644 packages/shared/types/complaint.types.ts create mode 100644 packages/shared/types/const.types.ts create mode 100644 packages/shared/types/coupon.types.ts diff --git a/apps/backend/src/dbService.ts b/apps/backend/src/dbService.ts index d94595b..7d744de 100644 --- a/apps/backend/src/dbService.ts +++ b/apps/backend/src/dbService.ts @@ -9,7 +9,37 @@ export { db } from 'postgresService'; export * from 'postgresService'; // Re-export methods from postgresService (implementation lives there) -export { getBanners, getBannerById, createBanner, updateBanner, deleteBanner } from 'postgresService'; +export { + // Banner methods + getBanners, + getBannerById, + createBanner, + updateBanner, + deleteBanner, + // Complaint methods + getComplaints, + resolveComplaint, + // Constants methods + getAllConstants, + upsertConstants, + // Coupon methods (batch 1 - non-transaction) + getAllCoupons, + getCouponById, + invalidateCoupon, + validateCoupon, + getReservedCoupons, + getUsersForCoupon, + // Coupon methods (batch 2 - transactions) + createCouponWithRelations, + updateCouponWithRelations, + generateCancellationCoupon, + createReservedCouponWithProducts, + createCouponForUser, + checkUsersExist, + checkCouponExists, + checkReservedCouponExists, + getOrderWithUser, +} from 'postgresService'; // Re-export types from local types file (to avoid circular dependencies) export type { Banner } from './types/db.types'; diff --git a/apps/backend/src/trpc/apis/admin-apis/apis/complaint.ts b/apps/backend/src/trpc/apis/admin-apis/apis/complaint.ts index 17d65d4..8c028b4 100644 --- a/apps/backend/src/trpc/apis/admin-apis/apis/complaint.ts +++ b/apps/backend/src/trpc/apis/admin-apis/apis/complaint.ts @@ -1,9 +1,8 @@ import { router, protectedProcedure } from '@/src/trpc/trpc-index' import { z } from 'zod'; -import { db } from '@/src/db/db_index' -import { complaints, users } from '@/src/db/schema' -import { eq, desc, lt, and } from 'drizzle-orm'; import { generateSignedUrlsFromS3Urls } from '@/src/lib/s3-client' +import { getComplaints as getComplaintsFromDb, resolveComplaint as resolveComplaintInDb } from '@/src/dbService' +import type { ComplaintWithUser } from '@packages/shared' export const complaintRouter = router({ getAll: protectedProcedure @@ -11,7 +10,27 @@ export const complaintRouter = router({ cursor: z.number().optional(), limit: z.number().default(20), })) - .query(async ({ input }) => { + .query(async ({ input }): Promise<{ + complaints: Array<{ + id: number; + text: string; + userId: number; + userName: string | null; + userMobile: string | null; + orderId: number | null; + status: string; + createdAt: Date; + images: string[]; + }>; + nextCursor?: number; + }> => { + const { cursor, limit } = input; + + // Using dbService helper (new implementation) + const { complaints: complaintsData, hasMore } = await getComplaintsFromDb(cursor, limit); + + /* + // Old implementation - direct DB query: const { cursor, limit } = input; let whereCondition = cursor @@ -37,10 +56,13 @@ export const complaintRouter = router({ .limit(limit + 1); const hasMore = complaintsData.length > limit; + const complaintsToReturn = hasMore ? complaintsData.slice(0, limit) : complaintsData; + */ + const complaintsToReturn = hasMore ? complaintsData.slice(0, limit) : complaintsData; const complaintsWithSignedImages = await Promise.all( - complaintsToReturn.map(async (c) => { + complaintsToReturn.map(async (c: ComplaintWithUser) => { const signedImages = c.images ? await generateSignedUrlsFromS3Urls(c.images as string[]) : []; @@ -69,12 +91,18 @@ export const complaintRouter = router({ resolve: protectedProcedure .input(z.object({ id: z.string(), response: z.string().optional() })) - .mutation(async ({ input }) => { + .mutation(async ({ input }): Promise<{ message: string }> => { + // Using dbService helper (new implementation) + await resolveComplaintInDb(parseInt(input.id), input.response); + + /* + // Old implementation - direct DB query: await db .update(complaints) .set({ isResolved: true, response: input.response }) .where(eq(complaints.id, parseInt(input.id))); + */ return { message: 'Complaint resolved successfully' }; }), -}); \ No newline at end of file +}); diff --git a/apps/backend/src/trpc/apis/admin-apis/apis/const.ts b/apps/backend/src/trpc/apis/admin-apis/apis/const.ts index a426087..6047f2f 100644 --- a/apps/backend/src/trpc/apis/admin-apis/apis/const.ts +++ b/apps/backend/src/trpc/apis/admin-apis/apis/const.ts @@ -1,22 +1,27 @@ import { router, protectedProcedure } from '@/src/trpc/trpc-index' import { z } from 'zod'; -import { db } from '@/src/db/db_index' -import { keyValStore } from '@/src/db/schema' import { computeConstants } from '@/src/lib/const-store' import { CONST_KEYS } from '@/src/lib/const-keys' +import { getAllConstants as getAllConstantsFromDb, upsertConstants as upsertConstantsInDb } from '@/src/dbService' +import type { Constant, ConstantUpdateResult } from '@packages/shared' export const constRouter = router({ getConstants: protectedProcedure - .query(async () => { - + .query(async (): Promise => { + // Using dbService helper (new implementation) + const constants = await getAllConstantsFromDb(); + + /* + // Old implementation - direct DB query: const constants = await db.select().from(keyValStore); const resp = constants.map(c => ({ key: c.key, value: c.value, })); + */ - return resp; + return constants; }), updateConstants: protectedProcedure @@ -26,7 +31,7 @@ export const constRouter = router({ value: z.any(), })), })) - .mutation(async ({ input }) => { + .mutation(async ({ input }): Promise => { const { constants } = input; const validKeys = Object.values(CONST_KEYS) as string[]; @@ -38,6 +43,11 @@ export const constRouter = router({ throw new Error(`Invalid constant keys: ${invalidKeys.join(', ')}`); } + // Using dbService helper (new implementation) + await upsertConstantsInDb(constants); + + /* + // Old implementation - direct DB query: await db.transaction(async (tx) => { for (const { key, value } of constants) { await tx.insert(keyValStore) @@ -48,6 +58,7 @@ export const constRouter = router({ }); } }); + */ // Refresh all constants in Redis after database update await computeConstants(); @@ -58,4 +69,4 @@ export const constRouter = router({ keys: constants.map(c => c.key), }; }), -}); \ No newline at end of file +}); diff --git a/apps/backend/src/trpc/apis/admin-apis/apis/coupon.ts b/apps/backend/src/trpc/apis/admin-apis/apis/coupon.ts index 4eb3017..70767c1 100644 --- a/apps/backend/src/trpc/apis/admin-apis/apis/coupon.ts +++ b/apps/backend/src/trpc/apis/admin-apis/apis/coupon.ts @@ -1,9 +1,26 @@ import { router, protectedProcedure } from '@/src/trpc/trpc-index' import { z } from 'zod'; -import { db } from '@/src/db/db_index' -import { coupons, users, staffUsers, orders, couponApplicableUsers, couponApplicableProducts, orderStatus, reservedCoupons } from '@/src/db/schema' -import { eq, and, like, or, inArray, lt } from 'drizzle-orm'; import dayjs from 'dayjs'; +import { + // Batch 1 - Non-transaction methods + getAllCoupons as getAllCouponsFromDb, + getCouponById as getCouponByIdFromDb, + invalidateCoupon as invalidateCouponInDb, + validateCoupon as validateCouponInDb, + getReservedCoupons as getReservedCouponsFromDb, + getUsersForCoupon as getUsersForCouponFromDb, + // Batch 2 - Transaction methods + createCouponWithRelations, + updateCouponWithRelations, + generateCancellationCoupon, + createReservedCouponWithProducts, + createCouponForUser, + checkUsersExist, + checkCouponExists, + checkReservedCouponExists, + getOrderWithUser, +} from '@/src/dbService' +import type { Coupon, CouponValidationResult, UserMiniInfo } from '@packages/shared' const createCouponBodySchema = z.object({ couponCode: z.string().optional(), @@ -31,7 +48,7 @@ const validateCouponBodySchema = z.object({ export const couponRouter = router({ create: protectedProcedure .input(createCouponBodySchema) - .mutation(async ({ input, ctx }) => { + .mutation(async ({ input, ctx }): Promise => { const { couponCode, isUserBased, discountPercent, flatDiscount, minOrder, productIds, applicableUsers, applicableProducts, maxValue, isApplyForAll, validTill, maxLimitForUser, exclusiveApply } = input; // Validation: ensure at least one discount type is provided @@ -49,17 +66,6 @@ export const couponRouter = router({ throw new Error("Cannot be both user-based and apply for all users"); } - // If applicableUsers is provided, verify users exist - if (applicableUsers && applicableUsers.length > 0) { - const existingUsers = await db.query.users.findMany({ - where: inArray(users.id, applicableUsers), - columns: { id: true }, - }); - if (existingUsers.length !== applicableUsers.length) { - throw new Error("Some applicable users not found"); - } - } - // Get staff user ID from auth middleware const staffUserId = ctx.staffUser?.id; if (!staffUserId) { @@ -69,22 +75,27 @@ export const couponRouter = router({ // Generate coupon code if not provided let finalCouponCode = couponCode; if (!finalCouponCode) { - // Generate a unique coupon code const timestamp = Date.now().toString().slice(-6); const random = Math.random().toString(36).substring(2, 8).toUpperCase(); finalCouponCode = `MF${timestamp}${random}`; } - // Check if coupon code already exists - const existingCoupon = await db.query.coupons.findFirst({ - where: eq(coupons.couponCode, finalCouponCode), - }); - - if (existingCoupon) { + // Using dbService helper (new implementation) + const codeExists = await checkCouponExists(finalCouponCode); + if (codeExists) { throw new Error("Coupon code already exists"); } - const result = await db.insert(coupons).values({ + // If applicableUsers is provided, verify users exist + if (applicableUsers && applicableUsers.length > 0) { + const usersExist = await checkUsersExist(applicableUsers); + if (!usersExist) { + throw new Error("Some applicable users not found"); + } + } + + const coupon = await createCouponWithRelations( + { couponCode: finalCouponCode, isUserBased: isUserBased || false, discountPercent: discountPercent?.toString(), @@ -95,9 +106,29 @@ export const couponRouter = router({ maxValue: maxValue?.toString(), isApplyForAll: isApplyForAll || false, validTill: validTill ? dayjs(validTill).toDate() : undefined, - maxLimitForUser: maxLimitForUser, + maxLimitForUser, exclusiveApply: exclusiveApply || false, - }).returning(); + }, + applicableUsers, + applicableProducts + ); + + /* + // Old implementation - direct DB query with transaction: + const result = await db.insert(coupons).values({ + couponCode: finalCouponCode, + isUserBased: isUserBased || false, + discountPercent: discountPercent?.toString(), + flatDiscount: flatDiscount?.toString(), + minOrder: minOrder?.toString(), + productIds: productIds || null, + createdBy: staffUserId, + maxValue: maxValue?.toString(), + isApplyForAll: isApplyForAll || false, + validTill: validTill ? dayjs(validTill).toDate() : undefined, + maxLimitForUser, + exclusiveApply: exclusiveApply || false, + }).returning(); const coupon = result[0]; @@ -120,6 +151,7 @@ export const couponRouter = router({ })) ); } + */ return coupon; }), @@ -130,71 +162,22 @@ export const couponRouter = router({ limit: z.number().default(50), search: z.string().optional(), })) - .query(async ({ input }) => { + .query(async ({ input }): Promise<{ coupons: any[]; nextCursor?: number }> => { const { cursor, limit, search } = input; - let whereCondition = undefined; - const conditions = []; - - if (cursor) { - conditions.push(lt(coupons.id, cursor)); - } - - if (search && search.trim()) { - conditions.push(like(coupons.couponCode, `%${search}%`)); - } - - if (conditions.length > 0) { - whereCondition = and(...conditions); - } - - const result = await db.query.coupons.findMany({ - where: whereCondition, - with: { - creator: true, - applicableUsers: { - with: { - user: true, - }, - }, - applicableProducts: { - with: { - product: true, - }, - }, - }, - orderBy: (coupons, { desc }) => [desc(coupons.createdAt)], - limit: limit + 1, - }); - - const hasMore = result.length > limit; - const couponsList = hasMore ? result.slice(0, limit) : result; - const nextCursor = hasMore ? result[result.length - 1].id : undefined; + const { coupons: couponsList, hasMore } = await getAllCouponsFromDb(cursor, limit, search); + + const nextCursor = hasMore ? couponsList[couponsList.length - 1].id : undefined; return { coupons: couponsList, nextCursor }; }), getById: protectedProcedure .input(z.object({ id: z.number() })) - .query(async ({ input }) => { + .query(async ({ input }): Promise => { const couponId = input.id; - const result = await db.query.coupons.findFirst({ - where: eq(coupons.id, couponId), - with: { - creator: true, - applicableUsers: { - with: { - user: true, - }, - }, - applicableProducts: { - with: { - product: true, - }, - }, - }, - }); + const result = await getCouponByIdFromDb(couponId); if (!result) { throw new Error("Coupon not found"); @@ -203,8 +186,8 @@ export const couponRouter = router({ return { ...result, productIds: (result.productIds as number[]) || undefined, - applicableUsers: result.applicableUsers.map(au => au.user), - applicableProducts: result.applicableProducts.map(ap => ap.product), + applicableUsers: result.applicableUsers.map((au: any) => au.user), + applicableProducts: result.applicableProducts.map((ap: any) => ap.product), }; }), @@ -215,7 +198,7 @@ export const couponRouter = router({ isInvalidated: z.boolean().optional(), }), })) - .mutation(async ({ input }) => { + .mutation(async ({ input }): Promise => { const { id, updates } = input; // Validation: ensure discount types are valid @@ -225,43 +208,31 @@ export const couponRouter = router({ } } - // If updating to user-based, applicableUsers is required - if (updates.isUserBased && (!updates.applicableUsers || updates.applicableUsers.length === 0)) { - const existingCount = await db.$count(couponApplicableUsers, eq(couponApplicableUsers.couponId, id)); - if (existingCount === 0) { - throw new Error("applicableUsers is required for user-based coupons"); - } - } + // Prepare update data + const updateData: any = {}; + if (updates.couponCode !== undefined) updateData.couponCode = updates.couponCode; + if (updates.isUserBased !== undefined) updateData.isUserBased = updates.isUserBased; + if (updates.discountPercent !== undefined) updateData.discountPercent = updates.discountPercent?.toString(); + if (updates.flatDiscount !== undefined) updateData.flatDiscount = updates.flatDiscount?.toString(); + if (updates.minOrder !== undefined) updateData.minOrder = updates.minOrder?.toString(); + if (updates.maxValue !== undefined) updateData.maxValue = updates.maxValue?.toString(); + if (updates.isApplyForAll !== undefined) updateData.isApplyForAll = updates.isApplyForAll; + if (updates.validTill !== undefined) updateData.validTill = updates.validTill ? dayjs(updates.validTill).toDate() : null; + if (updates.maxLimitForUser !== undefined) updateData.maxLimitForUser = updates.maxLimitForUser; + if (updates.exclusiveApply !== undefined) updateData.exclusiveApply = updates.exclusiveApply; + if (updates.isInvalidated !== undefined) updateData.isInvalidated = updates.isInvalidated; + if (updates.productIds !== undefined) updateData.productIds = updates.productIds; - // If applicableUsers is provided, verify users exist - if (updates.applicableUsers && updates.applicableUsers.length > 0) { - const existingUsers = await db.query.users.findMany({ - where: inArray(users.id, updates.applicableUsers), - columns: { id: true }, - }); - if (existingUsers.length !== updates.applicableUsers.length) { - throw new Error("Some applicable users not found"); - } - } + // Using dbService helper (new implementation) + const coupon = await updateCouponWithRelations( + id, + updateData, + updates.applicableUsers, + updates.applicableProducts + ); - const updateData: any = { ...updates }; - delete updateData.applicableUsers; // Remove since we use couponApplicableUsers table - if (updates.discountPercent !== undefined) { - updateData.discountPercent = updates.discountPercent?.toString(); - } - if (updates.flatDiscount !== undefined) { - updateData.flatDiscount = updates.flatDiscount?.toString(); - } - if (updates.minOrder !== undefined) { - updateData.minOrder = updates.minOrder?.toString(); - } - if (updates.maxValue !== undefined) { - updateData.maxValue = updates.maxValue?.toString(); - } - if (updates.validTill !== undefined) { - updateData.validTill = updates.validTill ? dayjs(updates.validTill).toDate() : null; - } - + /* + // Old implementation - direct DB query: const result = await db.update(coupons) .set(updateData) .where(eq(coupons.id, id)) @@ -271,8 +242,6 @@ export const couponRouter = router({ throw new Error("Coupon not found"); } - console.log('updated coupon successfully') - // Update applicable users: delete existing and insert new if (updates.applicableUsers !== undefined) { await db.delete(couponApplicableUsers).where(eq(couponApplicableUsers.couponId, id)); @@ -298,246 +267,43 @@ export const couponRouter = router({ ); } } + */ - return result[0]; + return coupon; }), delete: protectedProcedure .input(z.object({ id: z.number() })) - .mutation(async ({ input }) => { + .mutation(async ({ input }): Promise<{ message: string }> => { const { id } = input; - const result = await db.update(coupons) - .set({ isInvalidated: true }) - .where(eq(coupons.id, id)) - .returning(); - - if (result.length === 0) { - throw new Error("Coupon not found"); - } + await invalidateCouponInDb(id); return { message: "Coupon invalidated successfully" }; }), validate: protectedProcedure .input(validateCouponBodySchema) - .query(async ({ input }) => { + .query(async ({ input }): Promise => { const { code, userId, orderAmount } = input; if (!code || typeof code !== 'string') { return { valid: false, message: "Invalid coupon code" }; } - const coupon = await db.query.coupons.findFirst({ - where: and( - eq(coupons.couponCode, code.toUpperCase()), - eq(coupons.isInvalidated, false) - ), - }); + const result = await validateCouponInDb(code, userId, orderAmount); - if (!coupon) { - return { valid: false, message: "Coupon not found or invalidated" }; - } - - // Check expiry date - if (coupon.validTill && new Date(coupon.validTill) < new Date()) { - return { valid: false, message: "Coupon has expired" }; - } - - // Check if coupon applies to all users or specific user - if (!coupon.isApplyForAll && !coupon.isUserBased) { - return { valid: false, message: "Coupon is not available for use" }; - } - - // Check minimum order amount - const minOrderValue = coupon.minOrder ? parseFloat(coupon.minOrder) : 0; - if (minOrderValue > 0 && orderAmount < minOrderValue) { - return { valid: false, message: `Minimum order amount is ${minOrderValue}` }; - } - - // Calculate discount - let discountAmount = 0; - if (coupon.discountPercent) { - const percent = parseFloat(coupon.discountPercent); - discountAmount = (orderAmount * percent) / 100; - } else if (coupon.flatDiscount) { - discountAmount = parseFloat(coupon.flatDiscount); - } - - // Apply max value limit - const maxValueLimit = coupon.maxValue ? parseFloat(coupon.maxValue) : 0; - if (maxValueLimit > 0 && discountAmount > maxValueLimit) { - discountAmount = maxValueLimit; - } - - return { - valid: true, - discountAmount, - coupon: { - id: coupon.id, - discountPercent: coupon.discountPercent, - flatDiscount: coupon.flatDiscount, - maxValue: coupon.maxValue, - } - }; - }), - - generateCancellationCoupon: protectedProcedure - .input( - z.object({ - orderId: z.number(), - }) - ) - .mutation(async ({ input, ctx }) => { - const { orderId } = input; - - // Get staff user ID from auth middleware - const staffUserId = ctx.staffUser?.id; - if (!staffUserId) { - throw new Error("Unauthorized"); - } - - // Find the order with user and order status information - const order = await db.query.orders.findFirst({ - where: eq(orders.id, orderId), - with: { - user: true, - orderStatus: true, - }, - }); - - if (!order) { - throw new Error("Order not found"); - } - - // Check if order is cancelled (check if any status entry has isCancelled: true) - // const isOrderCancelled = order.orderStatus?.some(status => status.isCancelled) || false; - // if (!isOrderCancelled) { - // throw new Error("Order is not cancelled"); - // } - - // // Check if payment method is COD - // if (order.isCod) { - // throw new Error("Can't generate refund coupon for CoD Order"); - // } - - // Verify user exists - if (!order.user) { - throw new Error("User not found for this order"); - } - - // Generate coupon code: first 3 letters of user name or mobile + orderId - const userNamePrefix = (order.user.name || order.user.mobile || 'USR').substring(0, 3).toUpperCase(); - const couponCode = `${userNamePrefix}${orderId}`; - - // Check if coupon code already exists - const existingCoupon = await db.query.coupons.findFirst({ - where: eq(coupons.couponCode, couponCode), - }); - - if (existingCoupon) { - throw new Error("Coupon code already exists"); - } - - // Get order total amount - const orderAmount = parseFloat(order.totalAmount); - - // Calculate expiry date (30 days from now) - const expiryDate = new Date(); - expiryDate.setDate(expiryDate.getDate() + 30); - - // Create the coupon and update order status in a transaction - const coupon = await db.transaction(async (tx) => { - // Create the coupon - const result = await tx.insert(coupons).values({ - couponCode, - isUserBased: true, - flatDiscount: orderAmount.toString(), - minOrder: orderAmount.toString(), - maxValue: orderAmount.toString(), - validTill: expiryDate, - maxLimitForUser: 1, - createdBy: staffUserId, - isApplyForAll: false, - }).returning(); - - const coupon = result[0]; - - // Insert applicable users - await tx.insert(couponApplicableUsers).values({ - couponId: coupon.id, - userId: order.userId, - }); - - // Update order_status with refund coupon ID - await tx.update(orderStatus) - .set({ refundCouponId: coupon.id }) - .where(eq(orderStatus.orderId, orderId)); - - return coupon; - }); - - return coupon; - }), - - getReservedCoupons: protectedProcedure - .input(z.object({ - cursor: z.number().optional(), - limit: z.number().default(50), - search: z.string().optional(), - })) - .query(async ({ input }) => { - const { cursor, limit, search } = input; - - let whereCondition = undefined; - const conditions = []; - - if (cursor) { - conditions.push(lt(reservedCoupons.id, cursor)); - } - - if (search && search.trim()) { - conditions.push(or( - like(reservedCoupons.secretCode, `%${search}%`), - like(reservedCoupons.couponCode, `%${search}%`) - )); - } - - if (conditions.length > 0) { - whereCondition = and(...conditions); - } - - const result = await db.query.reservedCoupons.findMany({ - where: whereCondition, - with: { - redeemedUser: true, - creator: true, - }, - orderBy: (reservedCoupons, { desc }) => [desc(reservedCoupons.createdAt)], - limit: limit + 1, // Fetch one extra to check if there's more - }); - - const hasMore = result.length > limit; - const coupons = hasMore ? result.slice(0, limit) : result; - const nextCursor = hasMore ? result[result.length - 1].id : undefined; - - return { - coupons, - nextCursor, - }; + return result; }), - createReservedCoupon: protectedProcedure - .input(createCouponBodySchema) - .mutation(async ({ input, ctx }) => { - const { couponCode, isUserBased, discountPercent, flatDiscount, minOrder, productIds, applicableUsers, applicableProducts, maxValue, isApplyForAll, validTill, maxLimitForUser, exclusiveApply } = input; - - // Validation: ensure at least one discount type is provided - if ((!discountPercent && !flatDiscount) || (discountPercent && flatDiscount)) { - throw new Error("Either discountPercent or flatDiscount must be provided (but not both)"); - } - - // For reserved coupons, applicableUsers is not used, as it's redeemed by one user + generateCancellationCoupon: protectedProcedure + .input( + z.object({ + orderId: z.number(), + }) + ) + .mutation(async ({ input, ctx }): Promise => { + const { orderId } = input; // Get staff user ID from auth middleware const staffUserId = ctx.staffUser?.id; @@ -545,21 +311,144 @@ export const couponRouter = router({ throw new Error("Unauthorized"); } - // Generate secret code if not provided (use couponCode as base) + // Using dbService helper (new implementation) + const order = await getOrderWithUser(orderId); + + if (!order) { + throw new Error("Order not found"); + } + + if (!order.user) { + throw new Error("User not found for this order"); + } + + // Generate coupon code: first 3 letters of user name or mobile + orderId + const userNamePrefix = (order.user.name || order.user.mobile || 'USR').substring(0, 3).toUpperCase(); + const couponCode = `${userNamePrefix}${orderId}`; + + // Check if coupon code already exists + const codeExists = await checkCouponExists(couponCode); + if (codeExists) { + throw new Error("Coupon code already exists"); + } + + // Get order total amount + const orderAmount = parseFloat(order.totalAmount); + + const coupon = await generateCancellationCoupon( + orderId, + staffUserId, + order.userId, + orderAmount, + couponCode + ); + + /* + // Old implementation - direct DB query with transaction: + const coupon = await db.transaction(async (tx) => { + // Calculate expiry date (30 days from now) + const expiryDate = new Date(); + expiryDate.setDate(expiryDate.getDate() + 30); + + // Create the coupon + const result = await tx.insert(coupons).values({ + couponCode, + isUserBased: true, + flatDiscount: orderAmount.toString(), + minOrder: orderAmount.toString(), + maxValue: orderAmount.toString(), + validTill: expiryDate, + maxLimitForUser: 1, + createdBy: staffUserId, + isApplyForAll: false, + }).returning(); + + const coupon = result[0]; + + // Insert applicable users + await tx.insert(couponApplicableUsers).values({ + couponId: coupon.id, + userId: order.userId, + }); + + // Update order_status with refund coupon ID + await tx.update(orderStatus) + .set({ refundCouponId: coupon.id }) + .where(eq(orderStatus.orderId, orderId)); + + return coupon; + }); + */ + + return coupon; + }), + + getReservedCoupons: protectedProcedure + .input(z.object({ + cursor: z.number().optional(), + limit: z.number().default(50), + search: z.string().optional(), + })) + .query(async ({ input }): Promise<{ coupons: any[]; nextCursor?: number }> => { + const { cursor, limit, search } = input; + + const { coupons: result, hasMore } = await getReservedCouponsFromDb(cursor, limit, search); + + const nextCursor = hasMore ? result[result.length - 1].id : undefined; + + return { + coupons: result, + nextCursor, + }; + }), + + createReservedCoupon: protectedProcedure + .input(createCouponBodySchema) + .mutation(async ({ input, ctx }): Promise => { + const { couponCode, isUserBased, discountPercent, flatDiscount, minOrder, productIds, applicableProducts, maxValue, validTill, maxLimitForUser, exclusiveApply } = input; + + // Validation: ensure at least one discount type is provided + if ((!discountPercent && !flatDiscount) || (discountPercent && flatDiscount)) { + throw new Error("Either discountPercent or flatDiscount must be provided (but not both)"); + } + + // Get staff user ID from auth middleware + const staffUserId = ctx.staffUser?.id; + if (!staffUserId) { + throw new Error("Unauthorized"); + } + + // Generate secret code if not provided let secretCode = couponCode || `SECRET${Date.now().toString().slice(-6)}${Math.random().toString(36).substring(2, 8).toUpperCase()}`; - // Check if secret code already exists - const existing = await db.query.reservedCoupons.findFirst({ - where: eq(reservedCoupons.secretCode, secretCode), - }); - - if (existing) { + // Using dbService helper (new implementation) + const codeExists = await checkReservedCouponExists(secretCode); + if (codeExists) { throw new Error("Secret code already exists"); } + const coupon = await createReservedCouponWithProducts( + { + secretCode, + couponCode: couponCode || `RESERVED${Date.now().toString().slice(-6)}`, + discountPercent: discountPercent?.toString(), + flatDiscount: flatDiscount?.toString(), + minOrder: minOrder?.toString(), + productIds, + maxValue: maxValue?.toString(), + validTill: validTill ? dayjs(validTill).toDate() : undefined, + maxLimitForUser, + exclusiveApply: exclusiveApply || false, + createdBy: staffUserId, + }, + applicableProducts + ); + + /* + // Old implementation - direct DB query: const result = await db.insert(reservedCoupons).values({ secretCode, - couponCode: couponCode || `RESERVED${Date.now().toString().slice(-6)}`, + couponCode: couponCode || RESERVED${Date.now().toString().slice(-6)}, discountPercent: discountPercent?.toString(), flatDiscount: flatDiscount?.toString(), minOrder: minOrder?.toString(), @@ -582,6 +471,7 @@ export const couponRouter = router({ })) ); } + */ return coupon; }), @@ -592,120 +482,97 @@ export const couponRouter = router({ limit: z.number().min(1).max(50).default(20), offset: z.number().min(0).default(0), })) - .query(async ({ input }) => { - const { search, limit } = input; + .query(async ({ input }): Promise<{ users: UserMiniInfo[] }> => { + const { search, limit, offset } = input; - let whereCondition = undefined; - if (search && search.trim()) { - whereCondition = or( - like(users.name, `%${search}%`), - like(users.mobile, `%${search}%`) - ); - } + const result = await getUsersForCouponFromDb(search, limit, offset); - const userList = await db.query.users.findMany({ - where: whereCondition, - columns: { - id: true, - name: true, - mobile: true, - }, - limit: limit, - offset: input.offset, - orderBy: (users, { asc }) => [asc(users.name)], - }); - - return { - users: userList.map(user => ({ - id: user.id, - name: user.name || 'Unknown', - mobile: user.mobile, - })) - }; + return result; }), - createCoupon: protectedProcedure - .input(z.object({ - mobile: z.string().min(1, 'Mobile number is required'), - })) - .mutation(async ({ input, ctx }) => { - const { mobile } = input; + createCoupon: protectedProcedure + .input(z.object({ + mobile: z.string().min(1, 'Mobile number is required'), + })) + .mutation(async ({ input, ctx }): Promise<{ success: boolean; coupon: any }> => { + const { mobile } = input; - // Get staff user ID from auth middleware - const staffUserId = ctx.staffUser?.id; - if (!staffUserId) { - throw new Error("Unauthorized"); - } + // Get staff user ID from auth middleware + const staffUserId = ctx.staffUser?.id; + if (!staffUserId) { + throw new Error("Unauthorized"); + } - // Clean mobile number (remove non-digits) - const cleanMobile = mobile.replace(/\D/g, ''); + // Clean mobile number (remove non-digits) + const cleanMobile = mobile.replace(/\D/g, ''); - // Validate: exactly 10 digits - if (cleanMobile.length !== 10) { - throw new Error("Mobile number must be exactly 10 digits"); - } + // Validate: exactly 10 digits + if (cleanMobile.length !== 10) { + throw new Error("Mobile number must be exactly 10 digits"); + } - // Check if user exists, create if not - let user = await db.query.users.findFirst({ - where: eq(users.mobile, cleanMobile), - }); + // Generate unique coupon code + const timestamp = Date.now().toString().slice(-6); + const random = Math.random().toString(36).substring(2, 6).toUpperCase(); + const couponCode = `MF${cleanMobile.slice(-4)}${timestamp}${random}`; - if (!user) { - // Create new user - const [newUser] = await db.insert(users).values({ - name: null, - email: null, - mobile: cleanMobile, - }).returning(); - user = newUser; - } + // Using dbService helper (new implementation) + const codeExists = await checkCouponExists(couponCode); + if (codeExists) { + throw new Error("Generated coupon code already exists - please try again"); + } - // Generate unique coupon code - const timestamp = Date.now().toString().slice(-6); - const random = Math.random().toString(36).substring(2, 6).toUpperCase(); - const couponCode = `MF${cleanMobile.slice(-4)}${timestamp}${random}`; + const { coupon, user } = await createCouponForUser(cleanMobile, couponCode, staffUserId); - // Check if coupon code already exists (very unlikely but safe) - const existingCode = await db.query.coupons.findFirst({ - where: eq(coupons.couponCode, couponCode), - }); + /* + // Old implementation - direct DB query with transaction: + // Check if user exists, create if not + let user = await db.query.users.findFirst({ + where: eq(users.mobile, cleanMobile), + }); - if (existingCode) { - throw new Error("Generated coupon code already exists - please try again"); - } - - // Create the coupon - const [coupon] = await db.insert(coupons).values({ - couponCode, - isUserBased: true, - discountPercent: "20", // 20% discount - minOrder: "1000", // ₹1000 minimum order - maxValue: "500", // ₹500 maximum discount - maxLimitForUser: 1, // One-time use - isApplyForAll: false, - exclusiveApply: false, - createdBy: staffUserId, - validTill: dayjs().add(90, 'days').toDate(), // 90 days from now + if (!user) { + const [newUser] = await db.insert(users).values({ + name: null, + email: null, + mobile: cleanMobile, }).returning(); + user = newUser; + } - // Associate coupon with user - await db.insert(couponApplicableUsers).values({ - couponId: coupon.id, + // Create the coupon + const [coupon] = await db.insert(coupons).values({ + couponCode, + isUserBased: true, + discountPercent: "20", + minOrder: "1000", + maxValue: "500", + maxLimitForUser: 1, + isApplyForAll: false, + exclusiveApply: false, + createdBy: staffUserId, + validTill: dayjs().add(90, 'days').toDate(), + }).returning(); + + // Associate coupon with user + await db.insert(couponApplicableUsers).values({ + couponId: coupon.id, + userId: user.id, + }); + */ + + return { + success: true, + coupon: { + id: coupon.id, + couponCode: coupon.couponCode, userId: user.id, - }); - - return { - success: true, - coupon: { - id: coupon.id, - couponCode: coupon.couponCode, - userId: user.id, - userMobile: user.mobile, - discountPercent: 20, - minOrder: 1000, - maxValue: 500, - maxLimitForUser: 1, - }, - }; - }), + userMobile: user.mobile, + discountPercent: 20, + minOrder: 1000, + maxValue: 500, + maxLimitForUser: 1, + }, + }; + }), }); diff --git a/packages/db_helper_postgres/index.ts b/packages/db_helper_postgres/index.ts index a5f005e..a55459b 100644 --- a/packages/db_helper_postgres/index.ts +++ b/packages/db_helper_postgres/index.ts @@ -9,3 +9,6 @@ export * from './src/db/schema'; // Re-export helper methods export * from './src/helper_methods/banner'; +export * from './src/helper_methods/complaint'; +export * from './src/helper_methods/const'; +export * from './src/helper_methods/coupon'; diff --git a/packages/db_helper_postgres/src/helper_methods/complaint.ts b/packages/db_helper_postgres/src/helper_methods/complaint.ts new file mode 100644 index 0000000..0e2cec8 --- /dev/null +++ b/packages/db_helper_postgres/src/helper_methods/complaint.ts @@ -0,0 +1,74 @@ +import { db } from '../db/db_index'; +import { complaints, users } from '../db/schema'; +import { eq, desc, lt } from 'drizzle-orm'; + +export interface Complaint { + id: number; + complaintBody: string; + userId: number; + orderId: number | null; + isResolved: boolean; + response: string | null; + createdAt: Date; + images: string[] | null; +} + +export interface ComplaintWithUser extends Complaint { + userName: string | null; + userMobile: string | null; +} + +export async function getComplaints( + cursor?: number, + limit: number = 20 +): Promise<{ complaints: ComplaintWithUser[]; hasMore: boolean }> { + let whereCondition = cursor ? lt(complaints.id, cursor) : undefined; + + const complaintsData = await db + .select({ + id: complaints.id, + complaintBody: complaints.complaintBody, + userId: complaints.userId, + orderId: complaints.orderId, + isResolved: complaints.isResolved, + response: complaints.response, + createdAt: complaints.createdAt, + images: complaints.images, + userName: users.name, + userMobile: users.mobile, + }) + .from(complaints) + .leftJoin(users, eq(complaints.userId, users.id)) + .where(whereCondition) + .orderBy(desc(complaints.id)) + .limit(limit + 1); + + const hasMore = complaintsData.length > limit; + const complaintsToReturn = hasMore ? complaintsData.slice(0, limit) : complaintsData; + + return { + complaints: complaintsToReturn.map((c) => ({ + id: c.id, + complaintBody: c.complaintBody, + userId: c.userId, + orderId: c.orderId, + isResolved: c.isResolved, + response: c.response, + createdAt: c.createdAt, + images: c.images, + userName: c.userName, + userMobile: c.userMobile, + })), + hasMore, + }; +} + +export async function resolveComplaint( + id: number, + response?: string +): Promise { + await db + .update(complaints) + .set({ isResolved: true, response }) + .where(eq(complaints.id, id)); +} diff --git a/packages/db_helper_postgres/src/helper_methods/const.ts b/packages/db_helper_postgres/src/helper_methods/const.ts new file mode 100644 index 0000000..9d3ba86 --- /dev/null +++ b/packages/db_helper_postgres/src/helper_methods/const.ts @@ -0,0 +1,29 @@ +import { db } from '../db/db_index'; +import { keyValStore } from '../db/schema'; + +export interface Constant { + key: string; + value: any; +} + +export async function getAllConstants(): Promise { + const constants = await db.select().from(keyValStore); + + return constants.map(c => ({ + key: c.key, + value: c.value, + })); +} + +export async function upsertConstants(constants: Constant[]): Promise { + await db.transaction(async (tx) => { + for (const { key, value } of constants) { + await tx.insert(keyValStore) + .values({ key, value }) + .onConflictDoUpdate({ + target: keyValStore.key, + set: { value }, + }); + } + }); +} diff --git a/packages/db_helper_postgres/src/helper_methods/coupon.ts b/packages/db_helper_postgres/src/helper_methods/coupon.ts new file mode 100644 index 0000000..b87eee5 --- /dev/null +++ b/packages/db_helper_postgres/src/helper_methods/coupon.ts @@ -0,0 +1,633 @@ +import { db } from '../db/db_index'; +import { coupons, reservedCoupons, users } from '../db/schema'; +import { eq, and, like, or, inArray, lt, desc } from 'drizzle-orm'; + +export interface Coupon { + id: number; + couponCode: string; + isUserBased: boolean; + discountPercent: string | null; + flatDiscount: string | null; + minOrder: string | null; + productIds: number[] | null; + maxValue: string | null; + isApplyForAll: boolean; + validTill: Date | null; + maxLimitForUser: number | null; + exclusiveApply: boolean; + isInvalidated: boolean; + createdAt: Date; + createdBy: number; +} + +export async function getAllCoupons( + cursor?: number, + limit: number = 50, + search?: string +): Promise<{ coupons: any[]; hasMore: boolean }> { + let whereCondition = undefined; + const conditions = []; + + if (cursor) { + conditions.push(lt(coupons.id, cursor)); + } + + if (search && search.trim()) { + conditions.push(like(coupons.couponCode, `%${search}%`)); + } + + if (conditions.length > 0) { + whereCondition = and(...conditions); + } + + const result = await db.query.coupons.findMany({ + where: whereCondition, + with: { + creator: true, + applicableUsers: { + with: { + user: true, + }, + }, + applicableProducts: { + with: { + product: true, + }, + }, + }, + orderBy: (coupons, { desc }) => [desc(coupons.createdAt)], + limit: limit + 1, + }); + + const hasMore = result.length > limit; + const couponsList = hasMore ? result.slice(0, limit) : result; + + return { coupons: couponsList, hasMore }; +} + +export async function getCouponById(id: number): Promise { + const result = await db.query.coupons.findFirst({ + where: eq(coupons.id, id), + with: { + creator: true, + applicableUsers: { + with: { + user: true, + }, + }, + applicableProducts: { + with: { + product: true, + }, + }, + }, + }); + + return result || null; +} + +export async function invalidateCoupon(id: number): Promise { + const result = await db.update(coupons) + .set({ isInvalidated: true }) + .where(eq(coupons.id, id)) + .returning(); + + return result[0]; +} + +export interface CouponValidationResult { + valid: boolean; + message?: string; + discountAmount?: number; + coupon?: Partial; +} + +export async function validateCoupon( + code: string, + userId: number, + orderAmount: number +): Promise { + const coupon = await db.query.coupons.findFirst({ + where: and( + eq(coupons.couponCode, code.toUpperCase()), + eq(coupons.isInvalidated, false) + ), + }); + + if (!coupon) { + return { valid: false, message: "Coupon not found or invalidated" }; + } + + // Check expiry date + if (coupon.validTill && new Date(coupon.validTill) < new Date()) { + return { valid: false, message: "Coupon has expired" }; + } + + // Check if coupon applies to all users or specific user + if (!coupon.isApplyForAll && !coupon.isUserBased) { + return { valid: false, message: "Coupon is not available for use" }; + } + + // Check minimum order amount + const minOrderValue = coupon.minOrder ? parseFloat(coupon.minOrder) : 0; + if (minOrderValue > 0 && orderAmount < minOrderValue) { + return { valid: false, message: `Minimum order amount is ${minOrderValue}` }; + } + + // Calculate discount + let discountAmount = 0; + if (coupon.discountPercent) { + const percent = parseFloat(coupon.discountPercent); + discountAmount = (orderAmount * percent) / 100; + } else if (coupon.flatDiscount) { + discountAmount = parseFloat(coupon.flatDiscount); + } + + // Apply max value limit + const maxValueLimit = coupon.maxValue ? parseFloat(coupon.maxValue) : 0; + if (maxValueLimit > 0 && discountAmount > maxValueLimit) { + discountAmount = maxValueLimit; + } + + return { + valid: true, + discountAmount, + coupon: { + id: coupon.id, + discountPercent: coupon.discountPercent, + flatDiscount: coupon.flatDiscount, + maxValue: coupon.maxValue, + } + }; +} + +export async function getReservedCoupons( + cursor?: number, + limit: number = 50, + search?: string +): Promise<{ coupons: any[]; hasMore: boolean }> { + let whereCondition = undefined; + const conditions = []; + + if (cursor) { + conditions.push(lt(reservedCoupons.id, cursor)); + } + + if (search && search.trim()) { + conditions.push(or( + like(reservedCoupons.secretCode, `%${search}%`), + like(reservedCoupons.couponCode, `%${search}%`) + )); + } + + if (conditions.length > 0) { + whereCondition = and(...conditions); + } + + const result = await db.query.reservedCoupons.findMany({ + where: whereCondition, + with: { + redeemedUser: true, + creator: true, + }, + orderBy: (reservedCoupons, { desc }) => [desc(reservedCoupons.createdAt)], + limit: limit + 1, + }); + + const hasMore = result.length > limit; + const couponsList = hasMore ? result.slice(0, limit) : result; + + return { coupons: couponsList, hasMore }; +} + +export interface UserMiniInfo { + id: number; + name: string; + mobile: string | null; +} + +export async function getUsersForCoupon( + search?: string, + limit: number = 20, + offset: number = 0 +): Promise<{ users: UserMiniInfo[] }> { + let whereCondition = undefined; + if (search && search.trim()) { + whereCondition = or( + like(users.name, `%${search}%`), + like(users.mobile, `%${search}%`) + ); + } + + const userList = await db.query.users.findMany({ + where: whereCondition, + columns: { + id: true, + name: true, + mobile: true, + }, + limit: limit, + offset: offset, + orderBy: (users, { asc }) => [asc(users.name)], + }); + + return { + users: userList.map(user => ({ + id: user.id, + name: user.name || 'Unknown', + mobile: user.mobile, + })) + }; +} + +// ============================================================================ +// BATCH 2: Transaction Methods +// ============================================================================ + +import { couponApplicableUsers, couponApplicableProducts, orders, orderStatus } from '../db/schema'; + +export interface CreateCouponInput { + couponCode: string; + isUserBased: boolean; + discountPercent?: string; + flatDiscount?: string; + minOrder?: string; + productIds?: number[] | null; + maxValue?: string; + isApplyForAll: boolean; + validTill?: Date; + maxLimitForUser?: number; + exclusiveApply: boolean; + createdBy: number; +} + +export async function createCouponWithRelations( + input: CreateCouponInput, + applicableUsers?: number[], + applicableProducts?: number[] +): Promise { + return await db.transaction(async (tx) => { + // Create the coupon + const [coupon] = await tx.insert(coupons).values({ + couponCode: input.couponCode, + isUserBased: input.isUserBased, + discountPercent: input.discountPercent, + flatDiscount: input.flatDiscount, + minOrder: input.minOrder, + productIds: input.productIds, + createdBy: input.createdBy, + maxValue: input.maxValue, + isApplyForAll: input.isApplyForAll, + validTill: input.validTill, + maxLimitForUser: input.maxLimitForUser, + exclusiveApply: input.exclusiveApply, + }).returning(); + + // Insert applicable users + if (applicableUsers && applicableUsers.length > 0) { + await tx.insert(couponApplicableUsers).values( + applicableUsers.map(userId => ({ + couponId: coupon.id, + userId, + })) + ); + } + + // Insert applicable products + if (applicableProducts && applicableProducts.length > 0) { + await tx.insert(couponApplicableProducts).values( + applicableProducts.map(productId => ({ + couponId: coupon.id, + productId, + })) + ); + } + + return { + id: coupon.id, + couponCode: coupon.couponCode, + isUserBased: coupon.isUserBased, + discountPercent: coupon.discountPercent, + flatDiscount: coupon.flatDiscount, + minOrder: coupon.minOrder, + productIds: coupon.productIds, + maxValue: coupon.maxValue, + isApplyForAll: coupon.isApplyForAll, + validTill: coupon.validTill, + maxLimitForUser: coupon.maxLimitForUser, + exclusiveApply: coupon.exclusiveApply, + isInvalidated: coupon.isInvalidated, + createdAt: coupon.createdAt, + createdBy: coupon.createdBy, + }; + }); +} + +export interface UpdateCouponInput { + couponCode?: string; + isUserBased?: boolean; + discountPercent?: string; + flatDiscount?: string; + minOrder?: string; + productIds?: number[] | null; + maxValue?: string; + isApplyForAll?: boolean; + validTill?: Date | null; + maxLimitForUser?: number; + exclusiveApply?: boolean; + isInvalidated?: boolean; +} + +export async function updateCouponWithRelations( + id: number, + input: UpdateCouponInput, + applicableUsers?: number[], + applicableProducts?: number[] +): Promise { + return await db.transaction(async (tx) => { + // Update the coupon + const [coupon] = await tx.update(coupons) + .set({ + ...input, + lastUpdated: new Date(), + }) + .where(eq(coupons.id, id)) + .returning(); + + // Update applicable users: delete existing and insert new + if (applicableUsers !== undefined) { + await tx.delete(couponApplicableUsers).where(eq(couponApplicableUsers.couponId, id)); + if (applicableUsers.length > 0) { + await tx.insert(couponApplicableUsers).values( + applicableUsers.map(userId => ({ + couponId: id, + userId, + })) + ); + } + } + + // Update applicable products: delete existing and insert new + if (applicableProducts !== undefined) { + await tx.delete(couponApplicableProducts).where(eq(couponApplicableProducts.couponId, id)); + if (applicableProducts.length > 0) { + await tx.insert(couponApplicableProducts).values( + applicableProducts.map(productId => ({ + couponId: id, + productId, + })) + ); + } + } + + return { + id: coupon.id, + couponCode: coupon.couponCode, + isUserBased: coupon.isUserBased, + discountPercent: coupon.discountPercent, + flatDiscount: coupon.flatDiscount, + minOrder: coupon.minOrder, + productIds: coupon.productIds, + maxValue: coupon.maxValue, + isApplyForAll: coupon.isApplyForAll, + validTill: coupon.validTill, + maxLimitForUser: coupon.maxLimitForUser, + exclusiveApply: coupon.exclusiveApply, + isInvalidated: coupon.isInvalidated, + createdAt: coupon.createdAt, + createdBy: coupon.createdBy, + }; + }); +} + +export async function generateCancellationCoupon( + orderId: number, + staffUserId: number, + userId: number, + orderAmount: number, + couponCode: string +): Promise { + return await db.transaction(async (tx) => { + // Calculate expiry date (30 days from now) + const expiryDate = new Date(); + expiryDate.setDate(expiryDate.getDate() + 30); + + // Create the coupon + const [coupon] = await tx.insert(coupons).values({ + couponCode, + isUserBased: true, + flatDiscount: orderAmount.toString(), + minOrder: orderAmount.toString(), + maxValue: orderAmount.toString(), + validTill: expiryDate, + maxLimitForUser: 1, + createdBy: staffUserId, + isApplyForAll: false, + }).returning(); + + // Insert applicable users + await tx.insert(couponApplicableUsers).values({ + couponId: coupon.id, + userId, + }); + + // Update order_status with refund coupon ID + await tx.update(orderStatus) + .set({ refundCouponId: coupon.id }) + .where(eq(orderStatus.orderId, orderId)); + + return { + id: coupon.id, + couponCode: coupon.couponCode, + isUserBased: coupon.isUserBased, + discountPercent: coupon.discountPercent, + flatDiscount: coupon.flatDiscount, + minOrder: coupon.minOrder, + productIds: coupon.productIds, + maxValue: coupon.maxValue, + isApplyForAll: coupon.isApplyForAll, + validTill: coupon.validTill, + maxLimitForUser: coupon.maxLimitForUser, + exclusiveApply: coupon.exclusiveApply, + isInvalidated: coupon.isInvalidated, + createdAt: coupon.createdAt, + createdBy: coupon.createdBy, + }; + }); +} + +export interface CreateReservedCouponInput { + secretCode: string; + couponCode: string; + discountPercent?: string; + flatDiscount?: string; + minOrder?: string; + productIds?: number[] | null; + maxValue?: string; + validTill?: Date; + maxLimitForUser?: number; + exclusiveApply: boolean; + createdBy: number; +} + +export async function createReservedCouponWithProducts( + input: CreateReservedCouponInput, + applicableProducts?: number[] +): Promise { + return await db.transaction(async (tx) => { + const [coupon] = await tx.insert(reservedCoupons).values({ + secretCode: input.secretCode, + couponCode: input.couponCode, + discountPercent: input.discountPercent, + flatDiscount: input.flatDiscount, + minOrder: input.minOrder, + productIds: input.productIds, + maxValue: input.maxValue, + validTill: input.validTill, + maxLimitForUser: input.maxLimitForUser, + exclusiveApply: input.exclusiveApply, + createdBy: input.createdBy, + }).returning(); + + // Insert applicable products if provided + if (applicableProducts && applicableProducts.length > 0) { + await tx.insert(couponApplicableProducts).values( + applicableProducts.map(productId => ({ + couponId: coupon.id, + productId, + })) + ); + } + + return coupon; + }); +} + +export async function getOrCreateUserByMobile( + mobile: string +): Promise<{ id: number; mobile: string; name: string | null }> { + return await db.transaction(async (tx) => { + // Check if user exists + let user = await tx.query.users.findFirst({ + where: eq(users.mobile, mobile), + }); + + if (!user) { + // Create new user + const [newUser] = await tx.insert(users).values({ + name: null, + email: null, + mobile, + }).returning(); + user = newUser; + } + + return { + id: user.id, + mobile: user.mobile, + name: user.name, + }; + }); +} + +export async function createCouponForUser( + mobile: string, + couponCode: string, + staffUserId: number +): Promise<{ coupon: Coupon; user: { id: number; mobile: string; name: string | null } }> { + return await db.transaction(async (tx) => { + // Get or create user + let user = await tx.query.users.findFirst({ + where: eq(users.mobile, mobile), + }); + + if (!user) { + const [newUser] = await tx.insert(users).values({ + name: null, + email: null, + mobile, + }).returning(); + user = newUser; + } + + // Create the coupon + const [coupon] = await tx.insert(coupons).values({ + couponCode, + isUserBased: true, + discountPercent: "20", + minOrder: "1000", + maxValue: "500", + maxLimitForUser: 1, + isApplyForAll: false, + exclusiveApply: false, + createdBy: staffUserId, + validTill: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000), // 90 days from now + }).returning(); + + // Associate coupon with user + await tx.insert(couponApplicableUsers).values({ + couponId: coupon.id, + userId: user.id, + }); + + return { + coupon: { + id: coupon.id, + couponCode: coupon.couponCode, + isUserBased: coupon.isUserBased, + discountPercent: coupon.discountPercent, + flatDiscount: coupon.flatDiscount, + minOrder: coupon.minOrder, + productIds: coupon.productIds, + maxValue: coupon.maxValue, + isApplyForAll: coupon.isApplyForAll, + validTill: coupon.validTill, + maxLimitForUser: coupon.maxLimitForUser, + exclusiveApply: coupon.exclusiveApply, + isInvalidated: coupon.isInvalidated, + createdAt: coupon.createdAt, + createdBy: coupon.createdBy, + }, + user: { + id: user.id, + mobile: user.mobile, + name: user.name, + }, + }; + }); +} + +// ============================================================================ +// Utility Functions +// ============================================================================ + +export async function checkUsersExist(userIds: number[]): Promise { + const existingUsers = await db.query.users.findMany({ + where: inArray(users.id, userIds), + columns: { id: true }, + }); + return existingUsers.length === userIds.length; +} + +export async function checkCouponExists(couponCode: string): Promise { + const existing = await db.query.coupons.findFirst({ + where: eq(coupons.couponCode, couponCode), + }); + return !!existing; +} + +export async function checkReservedCouponExists(secretCode: string): Promise { + const existing = await db.query.reservedCoupons.findFirst({ + where: eq(reservedCoupons.secretCode, secretCode), + }); + return !!existing; +} + +export async function getOrderWithUser(orderId: number): Promise { + return await db.query.orders.findFirst({ + where: eq(orders.id, orderId), + with: { + user: true, + }, + }); +} diff --git a/packages/shared/types/complaint.types.ts b/packages/shared/types/complaint.types.ts new file mode 100644 index 0000000..6387a36 --- /dev/null +++ b/packages/shared/types/complaint.types.ts @@ -0,0 +1,20 @@ +/** + * Complaint Types + * Central type definitions for complaint-related data structures + */ + +export interface Complaint { + id: number; + complaintBody: string; + userId: number; + orderId: number | null; + isResolved: boolean; + response: string | null; + createdAt: Date; + images: string[] | null; +} + +export interface ComplaintWithUser extends Complaint { + userName: string | null; + userMobile: string | null; +} diff --git a/packages/shared/types/const.types.ts b/packages/shared/types/const.types.ts new file mode 100644 index 0000000..b1db5e4 --- /dev/null +++ b/packages/shared/types/const.types.ts @@ -0,0 +1,15 @@ +/** + * Constants Types + * Central type definitions for key-value store constants + */ + +export interface Constant { + key: string; + value: any; +} + +export interface ConstantUpdateResult { + success: boolean; + updatedCount: number; + keys: string[]; +} diff --git a/packages/shared/types/coupon.types.ts b/packages/shared/types/coupon.types.ts new file mode 100644 index 0000000..4f4c0a8 --- /dev/null +++ b/packages/shared/types/coupon.types.ts @@ -0,0 +1,41 @@ +/** + * Coupon Types + * Central type definitions for coupon-related data structures + */ + +export interface Coupon { + id: number; + couponCode: string; + isUserBased: boolean; + discountPercent: string | null; + flatDiscount: string | null; + minOrder: string | null; + productIds: number[] | null; + maxValue: string | null; + isApplyForAll: boolean; + validTill: Date | null; + maxLimitForUser: number | null; + exclusiveApply: boolean; + isInvalidated: boolean; + createdAt: Date; + createdBy: number; +} + +export interface ReservedCoupon extends Coupon { + secretCode: string; + redeemedUserId: number | null; + redeemedAt: Date | null; +} + +export interface CouponValidationResult { + valid: boolean; + message?: string; + discountAmount?: number; + coupon?: Partial; +} + +export interface UserMiniInfo { + id: number; + name: string; + mobile: string | null; +} diff --git a/packages/shared/types/index.ts b/packages/shared/types/index.ts index c934a34..0188fee 100644 --- a/packages/shared/types/index.ts +++ b/packages/shared/types/index.ts @@ -2,3 +2,6 @@ // Re-export all types from the types folder export type { Banner } from './banner.types'; +export type { Complaint, ComplaintWithUser } from './complaint.types'; +export type { Constant, ConstantUpdateResult } from './const.types'; +export type { Coupon, ReservedCoupon, CouponValidationResult, UserMiniInfo } from './coupon.types';