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, asc } 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: 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: 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: asc(users.name),
|
|
})
|
|
|
|
return {
|
|
users: userList.map((user: typeof users.$inferSelect) => ({
|
|
id: user.id,
|
|
name: user.name || 'Unknown',
|
|
mobile: user.mobile,
|
|
}))
|
|
}
|
|
}
|