freshyo/packages/db_helper_sqlite/src/user-apis/order.ts
2026-03-26 17:16:56 +05:30

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>
}