711 lines
23 KiB
TypeScript
711 lines
23 KiB
TypeScript
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';
|
|
|
|
const createCouponBodySchema = z.object({
|
|
couponCode: z.string().optional(),
|
|
isUserBased: z.boolean().optional(),
|
|
discountPercent: z.number().optional(),
|
|
flatDiscount: z.number().optional(),
|
|
minOrder: z.number().optional(),
|
|
targetUser: z.number().optional(),
|
|
productIds: z.array(z.number()).optional().nullable(),
|
|
applicableUsers: z.array(z.number()).optional(),
|
|
applicableProducts: z.array(z.number()).optional(),
|
|
maxValue: z.number().optional(),
|
|
isApplyForAll: z.boolean().optional(),
|
|
validTill: z.string().optional(),
|
|
maxLimitForUser: z.number().optional(),
|
|
exclusiveApply: z.boolean().optional(),
|
|
});
|
|
|
|
const validateCouponBodySchema = z.object({
|
|
code: z.string(),
|
|
userId: z.number(),
|
|
orderAmount: z.number(),
|
|
});
|
|
|
|
export const couponRouter = router({
|
|
create: 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)");
|
|
}
|
|
|
|
// If user-based, applicableUsers is required (unless it's apply for all)
|
|
if (isUserBased && (!applicableUsers || applicableUsers.length === 0) && !isApplyForAll) {
|
|
throw new Error("applicableUsers is required for user-based coupons (or set isApplyForAll to true)");
|
|
}
|
|
|
|
// Cannot be both user-based and apply for all
|
|
if (isUserBased && isApplyForAll) {
|
|
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) {
|
|
throw new Error("Unauthorized");
|
|
}
|
|
|
|
// 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) {
|
|
throw new Error("Coupon code already exists");
|
|
}
|
|
|
|
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: maxLimitForUser,
|
|
exclusiveApply: exclusiveApply || false,
|
|
}).returning();
|
|
|
|
const coupon = result[0];
|
|
|
|
// Insert applicable users
|
|
if (applicableUsers && applicableUsers.length > 0) {
|
|
await db.insert(couponApplicableUsers).values(
|
|
applicableUsers.map(userId => ({
|
|
couponId: coupon.id,
|
|
userId,
|
|
}))
|
|
);
|
|
}
|
|
|
|
// Insert applicable products
|
|
if (applicableProducts && applicableProducts.length > 0) {
|
|
await db.insert(couponApplicableProducts).values(
|
|
applicableProducts.map(productId => ({
|
|
couponId: coupon.id,
|
|
productId,
|
|
}))
|
|
);
|
|
}
|
|
|
|
return coupon;
|
|
}),
|
|
|
|
getAll: 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(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;
|
|
|
|
return { coupons: couponsList, nextCursor };
|
|
}),
|
|
|
|
getById: protectedProcedure
|
|
.input(z.object({ id: z.number() }))
|
|
.query(async ({ input }) => {
|
|
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,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!result) {
|
|
throw new Error("Coupon not found");
|
|
}
|
|
|
|
return {
|
|
...result,
|
|
productIds: (result.productIds as number[]) || undefined,
|
|
applicableUsers: result.applicableUsers.map(au => au.user),
|
|
applicableProducts: result.applicableProducts.map(ap => ap.product),
|
|
};
|
|
}),
|
|
|
|
update: protectedProcedure
|
|
.input(z.object({
|
|
id: z.number(),
|
|
updates: createCouponBodySchema.extend({
|
|
isInvalidated: z.boolean().optional(),
|
|
}),
|
|
}))
|
|
.mutation(async ({ input }) => {
|
|
const { id, updates } = input;
|
|
|
|
// Validation: ensure discount types are valid
|
|
if (updates.discountPercent !== undefined && updates.flatDiscount !== undefined) {
|
|
if (updates.discountPercent && updates.flatDiscount) {
|
|
throw new Error("Cannot have both discountPercent and flatDiscount");
|
|
}
|
|
}
|
|
|
|
// 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");
|
|
}
|
|
}
|
|
|
|
// 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");
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
const result = await db.update(coupons)
|
|
.set(updateData)
|
|
.where(eq(coupons.id, id))
|
|
.returning();
|
|
|
|
if (result.length === 0) {
|
|
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));
|
|
if (updates.applicableUsers.length > 0) {
|
|
await db.insert(couponApplicableUsers).values(
|
|
updates.applicableUsers.map(userId => ({
|
|
couponId: id,
|
|
userId,
|
|
}))
|
|
);
|
|
}
|
|
}
|
|
|
|
// Update applicable products: delete existing and insert new
|
|
if (updates.applicableProducts !== undefined) {
|
|
await db.delete(couponApplicableProducts).where(eq(couponApplicableProducts.couponId, id));
|
|
if (updates.applicableProducts.length > 0) {
|
|
await db.insert(couponApplicableProducts).values(
|
|
updates.applicableProducts.map(productId => ({
|
|
couponId: id,
|
|
productId,
|
|
}))
|
|
);
|
|
}
|
|
}
|
|
|
|
return result[0];
|
|
}),
|
|
|
|
delete: protectedProcedure
|
|
.input(z.object({ id: z.number() }))
|
|
.mutation(async ({ input }) => {
|
|
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");
|
|
}
|
|
|
|
return { message: "Coupon invalidated successfully" };
|
|
}),
|
|
|
|
validate: protectedProcedure
|
|
.input(validateCouponBodySchema)
|
|
.query(async ({ input }) => {
|
|
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)
|
|
),
|
|
});
|
|
|
|
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,
|
|
};
|
|
}),
|
|
|
|
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
|
|
|
|
// Get staff user ID from auth middleware
|
|
const staffUserId = ctx.staffUser?.id;
|
|
if (!staffUserId) {
|
|
throw new Error("Unauthorized");
|
|
}
|
|
|
|
// Generate secret code if not provided (use couponCode as base)
|
|
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) {
|
|
throw new Error("Secret code already exists");
|
|
}
|
|
|
|
const result = await db.insert(reservedCoupons).values({
|
|
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,
|
|
}).returning();
|
|
|
|
const coupon = result[0];
|
|
|
|
// Insert applicable products if provided
|
|
if (applicableProducts && applicableProducts.length > 0) {
|
|
await db.insert(couponApplicableProducts).values(
|
|
applicableProducts.map(productId => ({
|
|
couponId: coupon.id,
|
|
productId,
|
|
}))
|
|
);
|
|
}
|
|
|
|
return coupon;
|
|
}),
|
|
|
|
getUsersMiniInfo: protectedProcedure
|
|
.input(z.object({
|
|
search: z.string().optional(),
|
|
limit: z.number().min(1).max(50).default(20),
|
|
offset: z.number().min(0).default(0),
|
|
}))
|
|
.query(async ({ input }) => {
|
|
const { search, limit } = input;
|
|
|
|
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: input.offset,
|
|
orderBy: (users, { asc }) => [asc(users.name)],
|
|
});
|
|
|
|
return {
|
|
users: userList.map(user => ({
|
|
id: user.id,
|
|
name: user.name || 'Unknown',
|
|
mobile: user.mobile,
|
|
}))
|
|
};
|
|
}),
|
|
|
|
createCoupon: protectedProcedure
|
|
.input(z.object({
|
|
mobile: z.string().min(1, 'Mobile number is required'),
|
|
}))
|
|
.mutation(async ({ input, ctx }) => {
|
|
const { mobile } = input;
|
|
|
|
// 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, '');
|
|
|
|
// 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),
|
|
});
|
|
|
|
if (!user) {
|
|
// Create new user
|
|
const [newUser] = await db.insert(users).values({
|
|
name: null,
|
|
email: null,
|
|
mobile: cleanMobile,
|
|
}).returning();
|
|
user = newUser;
|
|
}
|
|
|
|
// 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}`;
|
|
|
|
// Check if coupon code already exists (very unlikely but safe)
|
|
const existingCode = await db.query.coupons.findFirst({
|
|
where: eq(coupons.couponCode, couponCode),
|
|
});
|
|
|
|
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
|
|
}).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,
|
|
userMobile: user.mobile,
|
|
discountPercent: 20,
|
|
minOrder: 1000,
|
|
maxValue: 500,
|
|
maxLimitForUser: 1,
|
|
},
|
|
};
|
|
}),
|
|
});
|