import { db } from '../db/db_index' import { orders, orderItems, orderStatus, addresses, productInfo, productSkus, paymentInfoTable, coupons, couponUsage, cartItems, refunds, units, userDetails, deliverySlotInfo, } from '../db/schema' import { and, eq, inArray, desc, sql } from 'drizzle-orm' import type { UserOrderSummary, UserOrderDetail, PlacedOrder, CouponUsageWithCoupon, } from '@packages/shared' import { coerceDate } from '../lib/date' import { runBatched } from '../lib/run-batched' import { composeSkuName, composeUnitNotation } from '../lib/sku-features' 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 skuId: number quantity: string price: string discountedPrice: string | null is_packaged: boolean sku: { id: number name: string | null images: unknown product: { name: string } | null } | null }> 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 type OrderDetailWithRelations = OrderWithRelations; 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 async function validateAndGetCoupon( couponId: number | undefined, userId: number, totalAmount: number ): Promise { 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') if (coupon.isFirstOrderOnly) { const orderCount = await getOrderCount(userId) if (orderCount > 0) { throw new Error( "This is first order only coupon. You already placed your first order. This coupon doesn't apply to you" ) } } 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(skuId: number) { const sku = await db.query.productSkus.findFirst({ where: eq(productSkus.id, skuId), with: { marketStats: true, product: true, }, }) if (!sku) { return null } const marketStats = sku.marketStats return { ...sku, price: marketStats?.ourPrice ? String(marketStats.ourPrice) : '0', marketPrice: marketStats?.marketPrice ? String(marketStats.marketPrice) : null, flashPrice: marketStats?.flashPrice ? String(marketStats.flashPrice) : null, isFlashAvailable: marketStats?.isFlashAvailable ?? false, isOutOfStock: marketStats?.isOutOfStock ?? false, } } export async function checkUserSuspended(userId: number): Promise { const userDetail = await db.query.userDetails.findFirst({ where: eq(userDetails.userId, userId), }) return userDetail?.isSuspended ?? false } export async function getSlotCapacityStatus(slotId: number): Promise { 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 orderItems: Omit[] orderStatus: Omit }> paymentMethod: 'online' | 'cod' totalWithDelivery: number }): Promise { 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[] = ordersData.map((od) => ({ ...od.order, paymentInfoId: sharedPaymentInfoId, })) const insertedOrders = await tx.insert(orders).values(ordersToInsert).returning() const allOrderItems: Omit[] = [] const allOrderStatuses: Omit[] = [] 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, }) }) const chunkSize = 5 for (let i = 0; i < allOrderItems.length; i += chunkSize) { await tx.insert(orderItems).values(allOrderItems.slice(i, i + chunkSize)) } await tx.insert(orderStatus).values(allOrderStatuses) return insertedOrders as PlacedOrder[] }) } export async function deleteCartItemsForOrder( userId: number, skuIds: number[] ): Promise { await db.delete(cartItems).where( and( eq(cartItems.userId, userId), inArray(cartItems.skuId, skuIds) ) ) } export async function recordCouponUsage( userId: number, couponId: number, orderId: number ): Promise { await db.insert(couponUsage).values({ userId, couponId, orderId, orderItemId: null, usedAt: new Date(), }) } export async function getOrdersWithRelations( userId: number, offset: number, pageSize: number ): Promise { const ordersWithRelations = await db.query.orders.findMany({ where: eq(orders.userId, userId), with: { orderItems: { with: { sku: { with: { product: { columns: { name: true, }, }, }, 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: [desc(orders.createdAt)], limit: pageSize, offset: offset, }) return ordersWithRelations.map((order) => { const createdAt = coerceDate(order.createdAt) ?? new Date(0) const slot = order.slot ? { ...order.slot, deliveryTime: coerceDate(order.slot.deliveryTime) ?? new Date(0), } : null return { ...order, createdAt, slot, } }) as OrderWithRelations[] } export async function getOrderCount(userId: number): Promise { 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 { const order = await db.query.orders.findFirst({ where: and(eq(orders.id, orderId), eq(orders.userId, userId)), with: { orderItems: { with: { sku: { with: { product: { columns: { name: true, }, }, }, 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, }, }, }, }) if (!order) { return null } const createdAt = coerceDate(order.createdAt) ?? new Date(0) const slot = order.slot ? { ...order.slot, deliveryTime: coerceDate(order.slot.deliveryTime) ?? new Date(0), } : null return { ...order, createdAt, slot, } as OrderDetailWithRelations } export async function getCouponUsageForOrder( orderId: number ): Promise { 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 } 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 { 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 { await db .update(orders) .set({ userNotes: userNotes || null, }) .where(eq(orders.id, orderId)) } // ============================================================================ // Post-Order Handler Helpers (for Telegram notifications) // ============================================================================ export interface OrderWithFullData { id: number userId: 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 sku: { product: { name: string } | null } | null }> slot: { deliveryTime: Date } | null } export async function getOrdersByIdsWithFullData( orderIds: number[] ): Promise { if (orderIds.length === 0) return [] const orderChunks = await runBatched(db, orderIds, 10, async (tx, chunk) => { return tx.query.orders.findMany({ where: inArray(orders.id, chunk), with: { address: { columns: { name: true, addressLine1: true, addressLine2: true, city: true, state: true, pincode: true, phone: true, }, }, orderItems: { with: { sku: { columns: { name: true, }, with: { product: { columns: { name: true }, }, features: true, }, }, }, }, slot: { columns: { deliveryTime: true, }, }, }, }) }) return orderChunks.flat() as unknown as OrderWithFullData[] } export interface OrderWithCancellationData extends OrderWithFullData { refunds: Array<{ refundStatus: string }> } export async function getOrderByIdWithFullData( orderId: number ): Promise { 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: { sku: { with: { product: { columns: { name: true, }, }, }, }, }, }, slot: { columns: { deliveryTime: true, }, }, refunds: { columns: { refundStatus: true, }, }, }, }) as Promise }