import { router, protectedProcedure } from '@/src/trpc/trpc-index'; import { z } from 'zod'; import { db } from '@/src/db/db_index'; import { coupons, couponUsage, couponApplicableUsers, reservedCoupons, couponApplicableProducts } from '@/src/db/schema'; import { eq, and, or, gt, isNull, sql } from 'drizzle-orm'; import { ApiError } from '@/src/lib/api-error'; import { users } from '@/src/db/schema'; type CouponWithRelations = typeof coupons.$inferSelect & { applicableUsers: (typeof couponApplicableUsers.$inferSelect & { user: typeof users.$inferSelect })[]; usages: typeof couponUsage.$inferSelect[]; }; export interface EligibleCoupon { id: number; code: string; discountType: 'percentage' | 'flat'; discountValue: number; maxValue?: number; minOrder?: number; description: string; exclusiveApply?: boolean; isEligible: boolean; ineligibilityReason?: string; } const generateCouponDescription = (coupon: any): string => { let desc = ''; if (coupon.discountPercent) { desc += `${coupon.discountPercent}% off`; } else if (coupon.flatDiscount) { desc += `₹${coupon.flatDiscount} off`; } if (coupon.minOrder) { desc += ` on orders above ₹${coupon.minOrder}`; } if (coupon.maxValue) { desc += ` (max discount ₹${coupon.maxValue})`; } return desc; }; export interface CouponDisplay { id: number; code: string; discountType: 'percentage' | 'flat'; discountValue: number; maxValue?: number; minOrder?: number; description: string; validTill?: Date; usageCount: number; maxLimitForUser?: number; isExpired: boolean; isUsedUp: boolean; } export const userCouponRouter = router({ getEligible: protectedProcedure .query(async ({ ctx }) => { try { const userId = ctx.user.userId; // Get all active, non-expired coupons const allCoupons = await db.query.coupons.findMany({ where: and( eq(coupons.isInvalidated, false), or( isNull(coupons.validTill), gt(coupons.validTill, new Date()) ) ), with: { usages: { where: eq(couponUsage.userId, userId) }, applicableUsers: { with: { user: true } }, applicableProducts: { with: { product: true } }, } }); // Filter to only coupons applicable to current user const applicableCoupons = allCoupons.filter(coupon => { if(!coupon.isUserBased) return true; const applicableUsers = coupon.applicableUsers || []; return applicableUsers.some(au => au.userId === userId); }); return { success: true, data: applicableCoupons }; } catch(e) { console.log(e) throw new ApiError("Unable to get coupons") } }), getProductCoupons: protectedProcedure .input(z.object({ productId: z.number().int().positive() })) .query(async ({ input, ctx }) => { const userId = ctx.user.userId; const { productId } = input; // Get all active, non-expired coupons const allCoupons = await db.query.coupons.findMany({ where: and( eq(coupons.isInvalidated, false), or( isNull(coupons.validTill), gt(coupons.validTill, new Date()) ) ), with: { usages: { where: eq(couponUsage.userId, userId) }, applicableUsers: { with: { user: true } }, applicableProducts: { with: { product: true } }, } }); // Filter to only coupons applicable to current user and product const applicableCoupons = allCoupons.filter(coupon => { const applicableUsers = coupon.applicableUsers || []; const userApplicable = !coupon.isUserBased || applicableUsers.some(au => au.userId === userId); const applicableProducts = coupon.applicableProducts || []; const productApplicable = applicableProducts.length === 0 || applicableProducts.some(ap => ap.productId === productId); return userApplicable && productApplicable; }); return { success: true, data: applicableCoupons }; }), getMyCoupons: protectedProcedure .query(async ({ ctx }) => { const userId = ctx.user.userId; // Get all coupons const allCoupons = await db.query.coupons.findMany({ with: { usages: { where: eq(couponUsage.userId, userId) }, applicableUsers: { with: { user: true } } } }); // Filter coupons in JS: not invalidated, applicable to user, and not expired const applicableCoupons = (allCoupons as CouponWithRelations[]).filter(coupon => { const isNotInvalidated = !coupon.isInvalidated; const applicableUsers = coupon.applicableUsers || []; const isApplicable = coupon.isApplyForAll || applicableUsers.some(au => au.userId === userId); const isNotExpired = !coupon.validTill || new Date(coupon.validTill) > new Date(); return isNotInvalidated && isApplicable && isNotExpired; }); // Categorize coupons const personalCoupons: CouponDisplay[] = []; const generalCoupons: CouponDisplay[] = []; applicableCoupons.forEach(coupon => { const usageCount = coupon.usages.length; const isExpired = false; // Already filtered out expired coupons const isUsedUp = Boolean(coupon.maxLimitForUser && usageCount >= coupon.maxLimitForUser); const couponDisplay: CouponDisplay = { id: coupon.id, code: coupon.couponCode, discountType: coupon.discountPercent ? 'percentage' : 'flat', discountValue: parseFloat(coupon.discountPercent || coupon.flatDiscount || '0'), maxValue: coupon.maxValue ? parseFloat(coupon.maxValue) : undefined, minOrder: coupon.minOrder ? parseFloat(coupon.minOrder) : undefined, description: generateCouponDescription(coupon), validTill: coupon.validTill ? new Date(coupon.validTill) : undefined, usageCount, maxLimitForUser: coupon.maxLimitForUser ? parseInt(coupon.maxLimitForUser.toString()) : undefined, isExpired, isUsedUp, }; if ((coupon.applicableUsers || []).some(au => au.userId === userId) && !coupon.isApplyForAll) { // Personal coupon personalCoupons.push(couponDisplay); } else if (coupon.isApplyForAll) { // General coupon generalCoupons.push(couponDisplay); } }); return { success: true, data: { personal: personalCoupons, general: generalCoupons, } }; }), redeemReservedCoupon: protectedProcedure .input(z.object({ secretCode: z.string() })) .mutation(async ({ input, ctx }) => { const userId = ctx.user.userId; const { secretCode } = input; // Find the reserved coupon const reservedCoupon = await db.query.reservedCoupons.findFirst({ where: and( eq(reservedCoupons.secretCode, secretCode.toUpperCase()), eq(reservedCoupons.isRedeemed, false) ), }); if (!reservedCoupon) { throw new ApiError("Invalid or already redeemed coupon code", 400); } // Check if already redeemed by this user (in case of multiple attempts) if (reservedCoupon.redeemedBy === userId) { throw new ApiError("You have already redeemed this coupon", 400); } // Create the coupon in the main table const couponResult = await db.transaction(async (tx) => { // Insert into coupons const couponInsert = await tx.insert(coupons).values({ couponCode: reservedCoupon.couponCode, isUserBased: true, discountPercent: reservedCoupon.discountPercent, flatDiscount: reservedCoupon.flatDiscount, minOrder: reservedCoupon.minOrder, productIds: reservedCoupon.productIds, maxValue: reservedCoupon.maxValue, isApplyForAll: false, validTill: reservedCoupon.validTill, maxLimitForUser: reservedCoupon.maxLimitForUser, exclusiveApply: reservedCoupon.exclusiveApply, createdBy: reservedCoupon.createdBy, }).returning(); const coupon = couponInsert[0]; // Insert into couponApplicableUsers await tx.insert(couponApplicableUsers).values({ couponId: coupon.id, userId, }); // Copy applicable products if (reservedCoupon.productIds && Array.isArray(reservedCoupon.productIds) && reservedCoupon.productIds.length > 0) { // Assuming productIds are the IDs, but wait, in schema, productIds is jsonb, but in relations, couponApplicableProducts has productId // For simplicity, since reservedCoupons has productIds as jsonb, but to match, perhaps insert into couponApplicableProducts if needed // But in createReservedCoupon, I inserted applicableProducts into couponApplicableProducts // So for reserved, perhaps do the same, but since it's jsonb, maybe not. // For now, skip, as the coupon will have productIds in coupons table. } // Update reserved coupon as redeemed await tx.update(reservedCoupons).set({ isRedeemed: true, redeemedBy: userId, redeemedAt: new Date(), }).where(eq(reservedCoupons.id, reservedCoupon.id)); return coupon; }); return { success: true, coupon: couponResult }; }), });