738 lines
16 KiB
TypeScript
738 lines
16 KiB
TypeScript
import { db } from '../db/db_index'
|
|
import {
|
|
orders,
|
|
orderItems,
|
|
orderStatus,
|
|
addresses,
|
|
productInfo,
|
|
paymentInfoTable,
|
|
coupons,
|
|
couponUsage,
|
|
cartItems,
|
|
refunds,
|
|
units,
|
|
userDetails,
|
|
deliverySlotInfo,
|
|
} from '../db/schema'
|
|
import { and, eq, inArray, desc, gte, sql } from 'drizzle-orm'
|
|
import type {
|
|
UserOrderSummary,
|
|
UserOrderDetail,
|
|
UserRecentProduct,
|
|
} from '@packages/shared'
|
|
|
|
export interface OrderItemInput {
|
|
productId: number
|
|
quantity: number
|
|
slotId: number | null
|
|
}
|
|
|
|
export interface PlaceOrderInput {
|
|
userId: number
|
|
selectedItems: OrderItemInput[]
|
|
addressId: number
|
|
paymentMethod: 'online' | 'cod'
|
|
couponId?: number
|
|
userNotes?: string
|
|
isFlash?: boolean
|
|
}
|
|
|
|
export interface OrderGroupData {
|
|
slotId: number | null
|
|
items: Array<{
|
|
productId: number
|
|
quantity: number
|
|
slotId: number | null
|
|
product: typeof productInfo.$inferSelect
|
|
}>
|
|
}
|
|
|
|
export interface PlacedOrder {
|
|
id: number
|
|
userId: number
|
|
addressId: number
|
|
slotId: number | null
|
|
totalAmount: string
|
|
deliveryCharge: string
|
|
isCod: boolean
|
|
isOnlinePayment: boolean
|
|
paymentInfoId: number | null
|
|
readableId: number
|
|
userNotes: string | null
|
|
orderGroupId: string
|
|
orderGroupProportion: string
|
|
isFlashDelivery: boolean
|
|
createdAt: Date
|
|
}
|
|
|
|
export interface OrderWithRelations {
|
|
id: number
|
|
userId: number
|
|
addressId: number
|
|
slotId: number | null
|
|
totalAmount: string
|
|
deliveryCharge: string
|
|
isCod: boolean
|
|
isOnlinePayment: boolean
|
|
isFlashDelivery: boolean
|
|
userNotes: string | null
|
|
createdAt: Date
|
|
orderItems: Array<{
|
|
id: number
|
|
productId: number
|
|
quantity: string
|
|
price: string
|
|
discountedPrice: string | null
|
|
is_packaged: boolean
|
|
product: {
|
|
id: number
|
|
name: string
|
|
images: unknown
|
|
}
|
|
}>
|
|
slot: {
|
|
deliveryTime: Date
|
|
} | null
|
|
paymentInfo: {
|
|
id: number
|
|
status: string
|
|
} | null
|
|
orderStatus: Array<{
|
|
id: number
|
|
isCancelled: boolean
|
|
isDelivered: boolean
|
|
paymentStatus: string
|
|
cancelReason: string | null
|
|
}>
|
|
refunds: Array<{
|
|
refundStatus: string
|
|
refundAmount: string | null
|
|
}>
|
|
}
|
|
|
|
export interface OrderDetailWithRelations {
|
|
id: number
|
|
userId: number
|
|
addressId: number
|
|
slotId: number | null
|
|
totalAmount: string
|
|
deliveryCharge: string
|
|
isCod: boolean
|
|
isOnlinePayment: boolean
|
|
isFlashDelivery: boolean
|
|
userNotes: string | null
|
|
createdAt: Date
|
|
orderItems: Array<{
|
|
id: number
|
|
productId: number
|
|
quantity: string
|
|
price: string
|
|
discountedPrice: string | null
|
|
is_packaged: boolean
|
|
product: {
|
|
id: number
|
|
name: string
|
|
images: unknown
|
|
}
|
|
}>
|
|
slot: {
|
|
deliveryTime: Date
|
|
} | null
|
|
paymentInfo: {
|
|
id: number
|
|
status: string
|
|
} | null
|
|
orderStatus: Array<{
|
|
id: number
|
|
isCancelled: boolean
|
|
isDelivered: boolean
|
|
paymentStatus: string
|
|
cancelReason: string | null
|
|
}>
|
|
refunds: Array<{
|
|
refundStatus: string
|
|
refundAmount: string | null
|
|
}>
|
|
}
|
|
|
|
export interface CouponValidationResult {
|
|
id: number
|
|
couponCode: string
|
|
isInvalidated: boolean
|
|
validTill: Date | null
|
|
maxLimitForUser: number | null
|
|
minOrder: string | null
|
|
discountPercent: string | null
|
|
flatDiscount: string | null
|
|
maxValue: string | null
|
|
usages: Array<{
|
|
id: number
|
|
userId: number
|
|
}>
|
|
}
|
|
|
|
export interface CouponUsageWithCoupon {
|
|
id: number
|
|
couponId: number
|
|
orderId: number | null
|
|
coupon: {
|
|
id: number
|
|
couponCode: string
|
|
discountPercent: string | null
|
|
flatDiscount: string | null
|
|
maxValue: string | null
|
|
}
|
|
}
|
|
|
|
export async function validateAndGetCoupon(
|
|
couponId: number | undefined,
|
|
userId: number,
|
|
totalAmount: number
|
|
): Promise<CouponValidationResult | null> {
|
|
if (!couponId) return null
|
|
|
|
const coupon = await db.query.coupons.findFirst({
|
|
where: eq(coupons.id, couponId),
|
|
with: {
|
|
usages: { where: eq(couponUsage.userId, userId) },
|
|
},
|
|
})
|
|
|
|
if (!coupon) throw new Error('Invalid coupon')
|
|
if (coupon.isInvalidated) throw new Error('Coupon is no longer valid')
|
|
if (coupon.validTill && new Date(coupon.validTill) < new Date())
|
|
throw new Error('Coupon has expired')
|
|
if (
|
|
coupon.maxLimitForUser &&
|
|
coupon.usages.length >= coupon.maxLimitForUser
|
|
)
|
|
throw new Error('Coupon usage limit exceeded')
|
|
if (
|
|
coupon.minOrder &&
|
|
parseFloat(coupon.minOrder.toString()) > totalAmount
|
|
)
|
|
throw new Error('Order amount does not meet coupon minimum requirement')
|
|
|
|
return coupon as CouponValidationResult
|
|
}
|
|
|
|
export function applyDiscountToOrder(
|
|
orderTotal: number,
|
|
appliedCoupon: CouponValidationResult | null,
|
|
proportion: number
|
|
): { finalOrderTotal: number; orderGroupProportion: number } {
|
|
let finalOrderTotal = orderTotal
|
|
|
|
if (appliedCoupon) {
|
|
if (appliedCoupon.discountPercent) {
|
|
const discount = Math.min(
|
|
(orderTotal *
|
|
parseFloat(appliedCoupon.discountPercent.toString())) /
|
|
100,
|
|
appliedCoupon.maxValue
|
|
? parseFloat(appliedCoupon.maxValue.toString()) * proportion
|
|
: Infinity
|
|
)
|
|
finalOrderTotal -= discount
|
|
} else if (appliedCoupon.flatDiscount) {
|
|
const discount = Math.min(
|
|
parseFloat(appliedCoupon.flatDiscount.toString()) * proportion,
|
|
appliedCoupon.maxValue
|
|
? parseFloat(appliedCoupon.maxValue.toString()) * proportion
|
|
: finalOrderTotal
|
|
)
|
|
finalOrderTotal -= discount
|
|
}
|
|
}
|
|
|
|
return { finalOrderTotal, orderGroupProportion: proportion }
|
|
}
|
|
|
|
export async function getAddressByIdAndUser(
|
|
addressId: number,
|
|
userId: number
|
|
) {
|
|
return db.query.addresses.findFirst({
|
|
where: and(eq(addresses.userId, userId), eq(addresses.id, addressId)),
|
|
})
|
|
}
|
|
|
|
export async function getProductById(productId: number) {
|
|
return db.query.productInfo.findFirst({
|
|
where: eq(productInfo.id, productId),
|
|
})
|
|
}
|
|
|
|
export async function checkUserSuspended(userId: number): Promise<boolean> {
|
|
const userDetail = await db.query.userDetails.findFirst({
|
|
where: eq(userDetails.userId, userId),
|
|
})
|
|
return userDetail?.isSuspended ?? false
|
|
}
|
|
|
|
export async function getSlotCapacityStatus(slotId: number): Promise<boolean> {
|
|
const slot = await db.query.deliverySlotInfo.findFirst({
|
|
where: eq(deliverySlotInfo.id, slotId),
|
|
columns: {
|
|
isCapacityFull: true,
|
|
},
|
|
})
|
|
return slot?.isCapacityFull ?? false
|
|
}
|
|
|
|
export async function placeOrderTransaction(params: {
|
|
userId: number
|
|
ordersData: Array<{
|
|
order: Omit<typeof orders.$inferInsert, 'id'>
|
|
orderItems: Omit<typeof orderItems.$inferInsert, 'id'>[]
|
|
orderStatus: Omit<typeof orderStatus.$inferInsert, 'id'>
|
|
}>
|
|
paymentMethod: 'online' | 'cod'
|
|
totalWithDelivery: number
|
|
}): Promise<PlacedOrder[]> {
|
|
const { userId, ordersData, paymentMethod } = params
|
|
|
|
return db.transaction(async (tx) => {
|
|
let sharedPaymentInfoId: number | null = null
|
|
if (paymentMethod === 'online') {
|
|
const [paymentInfo] = await tx
|
|
.insert(paymentInfoTable)
|
|
.values({
|
|
status: 'pending',
|
|
gateway: 'razorpay',
|
|
merchantOrderId: `multi_order_${Date.now()}`,
|
|
})
|
|
.returning()
|
|
sharedPaymentInfoId = paymentInfo.id
|
|
}
|
|
|
|
const ordersToInsert: Omit<typeof orders.$inferInsert, 'id'>[] =
|
|
ordersData.map((od) => ({
|
|
...od.order,
|
|
paymentInfoId: sharedPaymentInfoId,
|
|
}))
|
|
|
|
const insertedOrders = await tx.insert(orders).values(ordersToInsert).returning()
|
|
|
|
const allOrderItems: Omit<typeof orderItems.$inferInsert, 'id'>[] = []
|
|
const allOrderStatuses: Omit<typeof orderStatus.$inferInsert, 'id'>[] = []
|
|
|
|
insertedOrders.forEach((order, index) => {
|
|
const od = ordersData[index]
|
|
od.orderItems.forEach((item) => {
|
|
allOrderItems.push({ ...item, orderId: order.id })
|
|
})
|
|
allOrderStatuses.push({
|
|
...od.orderStatus,
|
|
orderId: order.id,
|
|
})
|
|
})
|
|
|
|
await tx.insert(orderItems).values(allOrderItems)
|
|
await tx.insert(orderStatus).values(allOrderStatuses)
|
|
|
|
return insertedOrders as PlacedOrder[]
|
|
})
|
|
}
|
|
|
|
export async function deleteCartItemsForOrder(
|
|
userId: number,
|
|
productIds: number[]
|
|
): Promise<void> {
|
|
await db.delete(cartItems).where(
|
|
and(
|
|
eq(cartItems.userId, userId),
|
|
inArray(cartItems.productId, productIds)
|
|
)
|
|
)
|
|
}
|
|
|
|
export async function recordCouponUsage(
|
|
userId: number,
|
|
couponId: number,
|
|
orderId: number
|
|
): Promise<void> {
|
|
await db.insert(couponUsage).values({
|
|
userId,
|
|
couponId,
|
|
orderId,
|
|
orderItemId: null,
|
|
usedAt: new Date(),
|
|
})
|
|
}
|
|
|
|
export async function getOrdersWithRelations(
|
|
userId: number,
|
|
offset: number,
|
|
pageSize: number
|
|
): Promise<OrderWithRelations[]> {
|
|
return db.query.orders.findMany({
|
|
where: eq(orders.userId, userId),
|
|
with: {
|
|
orderItems: {
|
|
with: {
|
|
product: {
|
|
columns: {
|
|
id: true,
|
|
name: true,
|
|
images: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
slot: {
|
|
columns: {
|
|
deliveryTime: true,
|
|
},
|
|
},
|
|
paymentInfo: {
|
|
columns: {
|
|
id: true,
|
|
status: true,
|
|
},
|
|
},
|
|
orderStatus: {
|
|
columns: {
|
|
id: true,
|
|
isCancelled: true,
|
|
isDelivered: true,
|
|
paymentStatus: true,
|
|
cancelReason: true,
|
|
},
|
|
},
|
|
refunds: {
|
|
columns: {
|
|
refundStatus: true,
|
|
refundAmount: true,
|
|
},
|
|
},
|
|
},
|
|
orderBy: (ordersTable: typeof orders) => [desc(ordersTable.createdAt)],
|
|
limit: pageSize,
|
|
offset: offset,
|
|
}) as Promise<OrderWithRelations[]>
|
|
}
|
|
|
|
export async function getOrderCount(userId: number): Promise<number> {
|
|
const result = await db
|
|
.select({ count: sql`count(*)` })
|
|
.from(orders)
|
|
.where(eq(orders.userId, userId))
|
|
|
|
return Number(result[0]?.count ?? 0)
|
|
}
|
|
|
|
export async function getOrderByIdWithRelations(
|
|
orderId: number,
|
|
userId: number
|
|
): Promise<OrderDetailWithRelations | null> {
|
|
const order = await db.query.orders.findFirst({
|
|
where: and(eq(orders.id, orderId), eq(orders.userId, userId)),
|
|
with: {
|
|
orderItems: {
|
|
with: {
|
|
product: {
|
|
columns: {
|
|
id: true,
|
|
name: true,
|
|
images: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
slot: {
|
|
columns: {
|
|
deliveryTime: true,
|
|
},
|
|
},
|
|
paymentInfo: {
|
|
columns: {
|
|
id: true,
|
|
status: true,
|
|
},
|
|
},
|
|
orderStatus: {
|
|
columns: {
|
|
id: true,
|
|
isCancelled: true,
|
|
isDelivered: true,
|
|
paymentStatus: true,
|
|
cancelReason: true,
|
|
},
|
|
with: {
|
|
refundCoupon: {
|
|
columns: {
|
|
id: true,
|
|
couponCode: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
refunds: {
|
|
columns: {
|
|
refundStatus: true,
|
|
refundAmount: true,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
return order as OrderDetailWithRelations | null
|
|
}
|
|
|
|
export async function getCouponUsageForOrder(
|
|
orderId: number
|
|
): Promise<CouponUsageWithCoupon[]> {
|
|
return db.query.couponUsage.findMany({
|
|
where: eq(couponUsage.orderId, orderId),
|
|
with: {
|
|
coupon: {
|
|
columns: {
|
|
id: true,
|
|
couponCode: true,
|
|
discountPercent: true,
|
|
flatDiscount: true,
|
|
maxValue: true,
|
|
},
|
|
},
|
|
},
|
|
}) as Promise<CouponUsageWithCoupon[]>
|
|
}
|
|
|
|
export async function getOrderBasic(orderId: number) {
|
|
return db.query.orders.findFirst({
|
|
where: eq(orders.id, orderId),
|
|
with: {
|
|
orderStatus: {
|
|
columns: {
|
|
id: true,
|
|
isCancelled: true,
|
|
isDelivered: true,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
export async function cancelOrderTransaction(
|
|
orderId: number,
|
|
statusId: number,
|
|
reason: string,
|
|
isCod: boolean
|
|
): Promise<void> {
|
|
await db.transaction(async (tx) => {
|
|
await tx
|
|
.update(orderStatus)
|
|
.set({
|
|
isCancelled: true,
|
|
cancelReason: reason,
|
|
cancellationUserNotes: reason,
|
|
cancellationReviewed: false,
|
|
})
|
|
.where(eq(orderStatus.id, statusId))
|
|
|
|
const refundStatus = isCod ? 'na' : 'pending'
|
|
|
|
await tx.insert(refunds).values({
|
|
orderId,
|
|
refundStatus,
|
|
})
|
|
})
|
|
}
|
|
|
|
export async function updateOrderNotes(
|
|
orderId: number,
|
|
userNotes: string
|
|
): Promise<void> {
|
|
await db
|
|
.update(orders)
|
|
.set({
|
|
userNotes: userNotes || null,
|
|
})
|
|
.where(eq(orders.id, orderId))
|
|
}
|
|
|
|
export async function getRecentlyDeliveredOrderIds(
|
|
userId: number,
|
|
limit: number,
|
|
since: Date
|
|
): Promise<number[]> {
|
|
const recentOrders = await db
|
|
.select({ id: orders.id })
|
|
.from(orders)
|
|
.innerJoin(orderStatus, eq(orders.id, orderStatus.orderId))
|
|
.where(
|
|
and(
|
|
eq(orders.userId, userId),
|
|
eq(orderStatus.isDelivered, true),
|
|
gte(orders.createdAt, since)
|
|
)
|
|
)
|
|
.orderBy(desc(orders.createdAt))
|
|
.limit(limit)
|
|
|
|
return recentOrders.map((order) => order.id)
|
|
}
|
|
|
|
export async function getProductIdsFromOrders(
|
|
orderIds: number[]
|
|
): Promise<number[]> {
|
|
const orderItemsResult = await db
|
|
.select({ productId: orderItems.productId })
|
|
.from(orderItems)
|
|
.where(inArray(orderItems.orderId, orderIds))
|
|
|
|
return [...new Set(orderItemsResult.map((item) => item.productId))]
|
|
}
|
|
|
|
export interface RecentProductData {
|
|
id: number
|
|
name: string
|
|
shortDescription: string | null
|
|
price: string
|
|
images: unknown
|
|
isOutOfStock: boolean
|
|
unitShortNotation: string
|
|
incrementStep: number
|
|
}
|
|
|
|
export async function getProductsForRecentOrders(
|
|
productIds: number[],
|
|
limit: number
|
|
): Promise<RecentProductData[]> {
|
|
const results = await db
|
|
.select({
|
|
id: productInfo.id,
|
|
name: productInfo.name,
|
|
shortDescription: productInfo.shortDescription,
|
|
price: productInfo.price,
|
|
images: productInfo.images,
|
|
isOutOfStock: productInfo.isOutOfStock,
|
|
unitShortNotation: units.shortNotation,
|
|
incrementStep: productInfo.incrementStep,
|
|
})
|
|
.from(productInfo)
|
|
.innerJoin(units, eq(productInfo.unitId, units.id))
|
|
.where(
|
|
and(
|
|
inArray(productInfo.id, productIds),
|
|
eq(productInfo.isSuspended, false)
|
|
)
|
|
)
|
|
.orderBy(desc(productInfo.createdAt))
|
|
.limit(limit)
|
|
|
|
return results.map((product) => ({
|
|
...product,
|
|
price: String(product.price ?? '0'),
|
|
}))
|
|
}
|
|
|
|
// ============================================================================
|
|
// Post-Order Handler Helpers (for Telegram notifications)
|
|
// ============================================================================
|
|
|
|
export interface OrderWithFullData {
|
|
id: number
|
|
totalAmount: string
|
|
isFlashDelivery: boolean
|
|
address: {
|
|
name: string | null
|
|
addressLine1: string | null
|
|
addressLine2: string | null
|
|
city: string | null
|
|
state: string | null
|
|
pincode: string | null
|
|
phone: string | null
|
|
} | null
|
|
orderItems: Array<{
|
|
quantity: string
|
|
product: {
|
|
name: string
|
|
} | null
|
|
}>
|
|
slot: {
|
|
deliveryTime: Date
|
|
} | null
|
|
}
|
|
|
|
export async function getOrdersByIdsWithFullData(
|
|
orderIds: number[]
|
|
): Promise<OrderWithFullData[]> {
|
|
return db.query.orders.findMany({
|
|
where: inArray(orders.id, orderIds),
|
|
with: {
|
|
address: {
|
|
columns: {
|
|
name: true,
|
|
addressLine1: true,
|
|
addressLine2: true,
|
|
city: true,
|
|
state: true,
|
|
pincode: true,
|
|
phone: true,
|
|
},
|
|
},
|
|
orderItems: {
|
|
with: {
|
|
product: {
|
|
columns: {
|
|
name: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
slot: {
|
|
columns: {
|
|
deliveryTime: true,
|
|
},
|
|
},
|
|
},
|
|
}) as Promise<OrderWithFullData[]>
|
|
}
|
|
|
|
export interface OrderWithCancellationData extends OrderWithFullData {
|
|
refunds: Array<{
|
|
refundStatus: string
|
|
}>
|
|
}
|
|
|
|
export async function getOrderByIdWithFullData(
|
|
orderId: number
|
|
): Promise<OrderWithCancellationData | null> {
|
|
return db.query.orders.findFirst({
|
|
where: eq(orders.id, orderId),
|
|
with: {
|
|
address: {
|
|
columns: {
|
|
name: true,
|
|
addressLine1: true,
|
|
addressLine2: true,
|
|
city: true,
|
|
state: true,
|
|
pincode: true,
|
|
phone: true,
|
|
},
|
|
},
|
|
orderItems: {
|
|
with: {
|
|
product: {
|
|
columns: {
|
|
name: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
slot: {
|
|
columns: {
|
|
deliveryTime: true,
|
|
},
|
|
},
|
|
refunds: {
|
|
columns: {
|
|
refundStatus: true,
|
|
},
|
|
},
|
|
},
|
|
}) as Promise<OrderWithCancellationData | null>
|
|
}
|