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 { 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 { 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, }) }) 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 { 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 { await db.insert(couponUsage).values({ userId, couponId, orderId, orderItemId: null, usedAt: new Date(), }) } export async function getOrdersWithRelations( userId: number, offset: number, pageSize: number ): Promise { 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 } 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: { 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 { 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)) } export async function getRecentlyDeliveredOrderIds( userId: number, limit: number, since: Date ): Promise { 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 { 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 { 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 { 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 } 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: { product: { columns: { name: true, }, }, }, }, slot: { columns: { deliveryTime: true, }, }, refunds: { columns: { refundStatus: true, }, }, }, }) as Promise }