496 lines
12 KiB
TypeScript
496 lines
12 KiB
TypeScript
import { db } from '../db/db_index';
|
|
import { coupons, reservedCoupons, users, orders, orderStatus, couponApplicableUsers, couponApplicableProducts } 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<any | null> {
|
|
return await db.query.coupons.findFirst({
|
|
where: eq(coupons.id, id),
|
|
with: {
|
|
creator: true,
|
|
applicableUsers: {
|
|
with: {
|
|
user: true,
|
|
},
|
|
},
|
|
applicableProducts: {
|
|
with: {
|
|
product: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
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<Coupon> {
|
|
return await db.transaction(async (tx) => {
|
|
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();
|
|
|
|
if (applicableUsers && applicableUsers.length > 0) {
|
|
await tx.insert(couponApplicableUsers).values(
|
|
applicableUsers.map(userId => ({
|
|
couponId: coupon.id,
|
|
userId,
|
|
}))
|
|
);
|
|
}
|
|
|
|
if (applicableProducts && applicableProducts.length > 0) {
|
|
await tx.insert(couponApplicableProducts).values(
|
|
applicableProducts.map(productId => ({
|
|
couponId: coupon.id,
|
|
productId,
|
|
}))
|
|
);
|
|
}
|
|
|
|
return coupon as Coupon;
|
|
});
|
|
}
|
|
|
|
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<Coupon> {
|
|
return await db.transaction(async (tx) => {
|
|
const [coupon] = await tx.update(coupons)
|
|
.set({
|
|
...input,
|
|
})
|
|
.where(eq(coupons.id, id))
|
|
.returning();
|
|
|
|
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,
|
|
}))
|
|
);
|
|
}
|
|
}
|
|
|
|
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 coupon as Coupon;
|
|
});
|
|
}
|
|
|
|
export async function invalidateCoupon(id: number): Promise<Coupon> {
|
|
const result = await db.update(coupons)
|
|
.set({ isInvalidated: true })
|
|
.where(eq(coupons.id, id))
|
|
.returning();
|
|
|
|
return result[0] as Coupon;
|
|
}
|
|
|
|
export interface CouponValidationResult {
|
|
valid: boolean;
|
|
message?: string;
|
|
discountAmount?: number;
|
|
coupon?: Partial<Coupon>;
|
|
}
|
|
|
|
export async function validateCoupon(
|
|
code: string,
|
|
userId: number,
|
|
orderAmount: number
|
|
): Promise<CouponValidationResult> {
|
|
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" };
|
|
}
|
|
|
|
if (coupon.validTill && new Date(coupon.validTill) < new Date()) {
|
|
return { valid: false, message: "Coupon has expired" };
|
|
}
|
|
|
|
if (!coupon.isApplyForAll && !coupon.isUserBased) {
|
|
return { valid: false, message: "Coupon is not available for use" };
|
|
}
|
|
|
|
const minOrderValue = coupon.minOrder ? parseFloat(coupon.minOrder) : 0;
|
|
if (minOrderValue > 0 && orderAmount < minOrderValue) {
|
|
return { valid: false, message: `Minimum order amount is ${minOrderValue}` };
|
|
}
|
|
|
|
let discountAmount = 0;
|
|
if (coupon.discountPercent) {
|
|
const percent = parseFloat(coupon.discountPercent);
|
|
discountAmount = (orderAmount * percent) / 100;
|
|
} else if (coupon.flatDiscount) {
|
|
discountAmount = parseFloat(coupon.flatDiscount);
|
|
}
|
|
|
|
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 async function createReservedCouponWithProducts(
|
|
input: any,
|
|
applicableProducts?: number[]
|
|
): Promise<any> {
|
|
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();
|
|
|
|
if (applicableProducts && applicableProducts.length > 0) {
|
|
await tx.insert(couponApplicableProducts).values(
|
|
applicableProducts.map(productId => ({
|
|
couponId: coupon.id,
|
|
productId,
|
|
}))
|
|
);
|
|
}
|
|
|
|
return coupon;
|
|
});
|
|
}
|
|
|
|
export async function checkUsersExist(userIds: number[]): Promise<boolean> {
|
|
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<boolean> {
|
|
const existing = await db.query.coupons.findFirst({
|
|
where: eq(coupons.couponCode, couponCode),
|
|
});
|
|
return !!existing;
|
|
}
|
|
|
|
export async function checkReservedCouponExists(secretCode: string): Promise<boolean> {
|
|
const existing = await db.query.reservedCoupons.findFirst({
|
|
where: eq(reservedCoupons.secretCode, secretCode),
|
|
});
|
|
return !!existing;
|
|
}
|
|
|
|
export async function generateCancellationCoupon(
|
|
orderId: number,
|
|
staffUserId: number,
|
|
userId: number,
|
|
orderAmount: number,
|
|
couponCode: string
|
|
): Promise<Coupon> {
|
|
return await db.transaction(async (tx) => {
|
|
const expiryDate = new Date();
|
|
expiryDate.setDate(expiryDate.getDate() + 30);
|
|
|
|
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();
|
|
|
|
await tx.insert(couponApplicableUsers).values({
|
|
couponId: coupon.id,
|
|
userId,
|
|
});
|
|
|
|
await tx.update(orderStatus)
|
|
.set({ refundCouponId: coupon.id })
|
|
.where(eq(orderStatus.orderId, orderId));
|
|
|
|
return coupon as Coupon;
|
|
});
|
|
}
|
|
|
|
export async function getOrderWithUser(orderId: number): Promise<any | null> {
|
|
return await db.query.orders.findFirst({
|
|
where: eq(orders.id, orderId),
|
|
with: {
|
|
user: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
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) => {
|
|
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;
|
|
}
|
|
|
|
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),
|
|
}).returning();
|
|
|
|
await tx.insert(couponApplicableUsers).values({
|
|
couponId: coupon.id,
|
|
userId: user.id,
|
|
});
|
|
|
|
return {
|
|
coupon: coupon as Coupon,
|
|
user: {
|
|
id: user.id,
|
|
mobile: user.mobile as string,
|
|
name: user.name,
|
|
},
|
|
};
|
|
});
|
|
}
|
|
|
|
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,
|
|
}))
|
|
};
|
|
}
|