import { router, protectedProcedure } from "../trpc-index"; import { z } from "zod"; import { db } from "../../db/db_index"; import { orders, orderItems, orderStatus, users, addresses, refunds, coupons, couponUsage, } from "../../db/schema"; import { eq, and, gte, lt, desc, SQL, inArray } from "drizzle-orm"; import dayjs from "dayjs"; import utc from "dayjs/plugin/utc"; import { ApiError } from "../../lib/api-error"; import { sendOrderPackagedNotification, sendOrderDeliveredNotification, } from "../../lib/notif-job"; import { publishCancellation } from "../../lib/post-order-handler"; const updateOrderNotesSchema = z.object({ orderId: z.number(), adminNotes: z.string(), }); const getFullOrderSchema = z.object({ orderId: z.number(), }); const getOrderDetailsSchema = z.object({ orderId: z.number(), }); const updatePackagedSchema = z.object({ orderId: z.string(), isPackaged: z.boolean(), }); const updateDeliveredSchema = z.object({ orderId: z.string(), isDelivered: z.boolean(), }); const updateOrderItemPackagingSchema = z.object({ orderItemId: z.number(), isPackaged: z.boolean().optional(), isPackageVerified: z.boolean().optional(), }); const getSlotOrdersSchema = z.object({ slotId: z.string(), }); const getTodaysOrdersSchema = z.object({ slotId: z.string().optional(), }); const getAllOrdersSchema = z.object({ cursor: z.number().optional(), limit: z.number().default(20), slotId: z.number().optional().nullable(), packagedFilter: z .enum(["all", "packaged", "not_packaged"]) .optional() .default("all"), deliveredFilter: z .enum(["all", "delivered", "not_delivered"]) .optional() .default("all"), cancellationFilter: z .enum(["all", "cancelled", "not_cancelled"]) .optional() .default("all"), flashDeliveryFilter: z .enum(["all", "flash", "regular"]) .optional() .default("all"), }); export const orderRouter = router({ updateNotes: protectedProcedure .input(updateOrderNotesSchema) .mutation(async ({ input }) => { const { orderId, adminNotes } = input; const result = await db .update(orders) .set({ adminNotes: adminNotes || null, }) .where(eq(orders.id, orderId)) .returning(); if (result.length === 0) { throw new Error("Order not found"); } return result[0]; }), getFullOrder: protectedProcedure .input(getFullOrderSchema) .query(async ({ input }) => { const { orderId } = input; const orderData = await db.query.orders.findFirst({ where: eq(orders.id, orderId), with: { user: true, address: true, slot: true, orderItems: { with: { product: { with: { unit: true, }, }, }, }, payment: true, paymentInfo: true, }, }); if (!orderData) { throw new Error("Order not found"); } // Get order status separately const statusRecord = await db.query.orderStatus.findFirst({ where: eq(orderStatus.orderId, orderId), }); let status: "pending" | "delivered" | "cancelled" = "pending"; if (statusRecord?.isCancelled) { status = "cancelled"; } else if (statusRecord?.isDelivered) { status = "delivered"; } // Get refund details if order is cancelled let refund = null; if (status === "cancelled") { refund = await db.query.refunds.findFirst({ where: eq(refunds.orderId, orderId), }); } return { id: orderData.id, readableId: orderData.id, customerName: `${orderData.user.name}`, customerEmail: orderData.user.email, customerMobile: orderData.user.mobile, address: { line1: orderData.address.addressLine1, line2: orderData.address.addressLine2, city: orderData.address.city, state: orderData.address.state, pincode: orderData.address.pincode, phone: orderData.address.phone, }, slotInfo: orderData.slot ? { time: orderData.slot.deliveryTime.toISOString(), sequence: orderData.slot.deliverySequence, } : null, isCod: orderData.isCod, isOnlinePayment: orderData.isOnlinePayment, totalAmount: orderData.totalAmount, adminNotes: orderData.adminNotes, userNotes: orderData.userNotes, createdAt: orderData.createdAt, status, isPackaged: orderData.orderItems.every((item) => item.is_packaged) || false, isDelivered: statusRecord?.isDelivered || false, items: orderData.orderItems.map((item) => ({ id: item.id, name: item.product.name, quantity: item.quantity, price: item.price, unit: item.product.unit?.shortNotation, amount: parseFloat(item.price.toString()) * parseFloat(item.quantity || "0"), })), payment: orderData.payment ? { status: orderData.payment.status, gateway: orderData.payment.gateway, merchantOrderId: orderData.payment.merchantOrderId, } : null, paymentInfo: orderData.paymentInfo ? { status: orderData.paymentInfo.status, gateway: orderData.paymentInfo.gateway, merchantOrderId: orderData.paymentInfo.merchantOrderId, } : null, // Cancellation details (only present for cancelled orders) cancelReason: statusRecord?.cancelReason || null, cancellationReviewed: statusRecord?.cancellationReviewed || false, isRefundDone: refund?.refundStatus === "processed" || false, }; }), getOrderDetails: protectedProcedure .input(getOrderDetailsSchema) .query(async ({ input }) => { const { orderId } = input; // Single optimized query with all relations const orderData = await db.query.orders.findFirst({ where: eq(orders.id, orderId), with: { user: true, address: true, slot: true, orderItems: { with: { product: { with: { unit: true, }, }, }, }, payment: true, paymentInfo: true, orderStatus: true, // Include in main query refunds: true, // Include in main query }, }); if (!orderData) { throw new Error("Order not found"); } // Get coupon usage for this specific order using new orderId field const couponUsageData = await db.query.couponUsage.findMany({ where: eq(couponUsage.orderId, orderData.id), // Use new orderId field with: { coupon: true, }, }); let couponData = null; if (couponUsageData.length > 0) { // Calculate total discount from multiple coupons let totalDiscountAmount = 0; const orderTotal = parseFloat(orderData.totalAmount.toString()); for (const usage of couponUsageData) { let discountAmount = 0; if (usage.coupon.discountPercent) { discountAmount = (orderTotal * parseFloat(usage.coupon.discountPercent.toString())) / 100; } else if (usage.coupon.flatDiscount) { discountAmount = parseFloat(usage.coupon.flatDiscount.toString()); } // Apply max value limit if set if ( usage.coupon.maxValue && discountAmount > parseFloat(usage.coupon.maxValue.toString()) ) { discountAmount = parseFloat(usage.coupon.maxValue.toString()); } totalDiscountAmount += discountAmount; } couponData = { couponCode: couponUsageData .map((u) => u.coupon.couponCode) .join(", "), couponDescription: `${couponUsageData.length} coupons applied`, discountAmount: totalDiscountAmount, }; } // Status determination from included relation const statusRecord = orderData.orderStatus?.[0]; let status: "pending" | "delivered" | "cancelled" = "pending"; if (statusRecord?.isCancelled) { status = "cancelled"; } else if (statusRecord?.isDelivered) { status = "delivered"; } // Always include refund data (will be null/undefined if not cancelled) const refund = orderData.refunds?.[0]; return { id: orderData.id, readableId: orderData.id, customerName: `${orderData.user.name}`, customerEmail: orderData.user.email, customerMobile: orderData.user.mobile, address: { name: orderData.address.name, line1: orderData.address.addressLine1, line2: orderData.address.addressLine2, city: orderData.address.city, state: orderData.address.state, pincode: orderData.address.pincode, phone: orderData.address.phone, }, slotInfo: orderData.slot ? { time: orderData.slot.deliveryTime.toISOString(), sequence: orderData.slot.deliverySequence, } : null, isCod: orderData.isCod, isOnlinePayment: orderData.isOnlinePayment, totalAmount: orderData.totalAmount, adminNotes: orderData.adminNotes, userNotes: orderData.userNotes, createdAt: orderData.createdAt, status, isPackaged: statusRecord?.isPackaged || false, isDelivered: statusRecord?.isDelivered || false, items: orderData.orderItems.map((item) => ({ id: item.id, name: item.product.name, quantity: item.quantity, productSize: item.product.productQuantity, price: item.price, unit: item.product.unit?.shortNotation, amount: parseFloat(item.price.toString()) * parseFloat(item.quantity || "0"), isPackaged: item.is_packaged, isPackageVerified: item.is_package_verified, })), payment: orderData.payment ? { status: orderData.payment.status, gateway: orderData.payment.gateway, merchantOrderId: orderData.payment.merchantOrderId, } : null, paymentInfo: orderData.paymentInfo ? { status: orderData.paymentInfo.status, gateway: orderData.paymentInfo.gateway, merchantOrderId: orderData.paymentInfo.merchantOrderId, } : null, // Cancellation details (always included, null if not cancelled) cancelReason: statusRecord?.cancelReason || null, cancellationReviewed: statusRecord?.cancellationReviewed || false, isRefundDone: refund?.refundStatus === "processed" || false, refundStatus: refund?.refundStatus as RefundStatus, refundAmount: refund?.refundAmount ? parseFloat(refund.refundAmount.toString()) : null, // Coupon information couponData: couponData, couponCode: couponData?.couponCode || null, couponDescription: couponData?.couponDescription || null, discountAmount: couponData?.discountAmount || null, orderStatus: statusRecord, refundRecord: refund, isFlashDelivery: orderData.isFlashDelivery, }; }), updatePackaged: protectedProcedure .input(updatePackagedSchema) .mutation(async ({ input }) => { const { orderId, isPackaged } = input; // Update all order items to the specified packaged state await db .update(orderItems) .set({ is_packaged: isPackaged }) .where(eq(orderItems.orderId, parseInt(orderId))); // Also update the order status table for backward compatibility if (!isPackaged) { await db .update(orderStatus) .set({ isPackaged, isDelivered: false }) .where(eq(orderStatus.orderId, parseInt(orderId))); } else { await db .update(orderStatus) .set({ isPackaged }) .where(eq(orderStatus.orderId, parseInt(orderId))); } const order = await db.query.orders.findFirst({ where: eq(orders.id, parseInt(orderId)), }); if (order) await sendOrderPackagedNotification(order.userId, orderId); return { success: true }; }), updateDelivered: protectedProcedure .input(updateDeliveredSchema) .mutation(async ({ input }) => { const { orderId, isDelivered } = input; await db .update(orderStatus) .set({ isDelivered }) .where(eq(orderStatus.orderId, parseInt(orderId))); const order = await db.query.orders.findFirst({ where: eq(orders.id, parseInt(orderId)), }); if (order) await sendOrderDeliveredNotification(order.userId, orderId); return { success: true }; }), updateOrderItemPackaging: protectedProcedure .input(updateOrderItemPackagingSchema) .mutation(async ({ input }) => { const { orderItemId, isPackaged, isPackageVerified } = input; // Validate that orderItem exists const orderItem = await db.query.orderItems.findFirst({ where: eq(orderItems.id, orderItemId), }); if (!orderItem) { throw new ApiError("Order item not found", 404); } // Build update object with only provided fields const updateData: any = {}; if (isPackaged !== undefined) { updateData.is_packaged = isPackaged; } if (isPackageVerified !== undefined) { updateData.is_package_verified = isPackageVerified; } // Update the order item await db .update(orderItems) .set(updateData) .where(eq(orderItems.id, orderItemId)); return { success: true }; }), getSlotOrders: protectedProcedure .input(getSlotOrdersSchema) .query(async ({ input }) => { const { slotId } = input; const slotOrders = await db.query.orders.findMany({ where: eq(orders.slotId, parseInt(slotId)), with: { user: true, address: true, slot: true, orderItems: { with: { product: { with: { unit: true, }, }, }, }, orderStatus: true, }, }); const filteredOrders = slotOrders.filter((order) => { const statusRecord = order.orderStatus[0]; return ( order.isCod || (statusRecord && statusRecord.paymentStatus === "success") ); }); const formattedOrders = filteredOrders.map((order) => { const statusRecord = order.orderStatus[0]; // assuming one status per order let status: "pending" | "delivered" | "cancelled" = "pending"; if (statusRecord?.isCancelled) { status = "cancelled"; } else if (statusRecord?.isDelivered) { status = "delivered"; } const items = order.orderItems.map((item) => ({ id: item.id, name: item.product.name, quantity: parseFloat(item.quantity), price: parseFloat(item.price.toString()), amount: parseFloat(item.quantity) * parseFloat(item.price.toString()), unit: item.product.unit?.shortNotation || "", isPackaged: item.is_packaged, isPackageVerified: item.is_package_verified, })); return { id: order.id, readableId: order.id, customerName: order.user.name, address: `${order.address.addressLine1}${ order.address.addressLine2 ? `, ${order.address.addressLine2}` : "" }, ${order.address.city}, ${order.address.state} - ${ order.address.pincode }, Phone: ${order.address.phone}`, addressId: order.addressId, latitude: order.address.adminLatitude ?? order.address.latitude, longitude: order.address.adminLongitude ?? order.address.longitude, totalAmount: parseFloat(order.totalAmount), items, deliveryTime: order.slot?.deliveryTime.toISOString() || null, status, isPackaged: order.orderItems.every((item) => item.is_packaged) || false, isDelivered: statusRecord?.isDelivered || false, isCod: order.isCod, paymentMode: order.isCod ? "COD" : "Online", paymentStatus: statusRecord?.paymentStatus || "pending", slotId: order.slotId, adminNotes: order.adminNotes, userNotes: order.userNotes, }; }); return { success: true, data: formattedOrders }; }), getTodaysOrders: protectedProcedure .input(getTodaysOrdersSchema) .query(async ({ input }) => { const { slotId } = input; const start = dayjs().startOf("day").toDate(); const end = dayjs().endOf("day").toDate(); let whereCondition = and( gte(orders.createdAt, start), lt(orders.createdAt, end) ); if (slotId) { whereCondition = and( whereCondition, eq(orders.slotId, parseInt(slotId)) ); } const todaysOrders = await db.query.orders.findMany({ where: whereCondition, with: { user: true, address: true, slot: true, orderItems: { with: { product: { with: { unit: true, }, }, }, }, orderStatus: true, }, }); const filteredOrders = todaysOrders.filter((order) => { const statusRecord = order.orderStatus[0]; return ( order.isCod || (statusRecord && statusRecord.paymentStatus === "success") ); }); const formattedOrders = filteredOrders.map((order) => { const statusRecord = order.orderStatus[0]; // assuming one status per order let status: "pending" | "delivered" | "cancelled" = "pending"; if (statusRecord?.isCancelled) { status = "cancelled"; } else if (statusRecord?.isDelivered) { status = "delivered"; } const items = order.orderItems.map((item) => ({ name: item.product.name, quantity: parseFloat(item.quantity), price: parseFloat(item.price.toString()), amount: parseFloat(item.quantity) * parseFloat(item.price.toString()), unit: item.product.unit?.shortNotation || "", })); return { orderId: order.id.toString(), readableId: order.id, customerName: order.user.name, address: `${order.address.addressLine1}${ order.address.addressLine2 ? `, ${order.address.addressLine2}` : "" }, ${order.address.city}, ${order.address.state} - ${ order.address.pincode }`, totalAmount: parseFloat(order.totalAmount), items, deliveryTime: order.slot?.deliveryTime.toISOString() || null, status, isPackaged: order.orderItems.every((item) => item.is_packaged) || false, isDelivered: statusRecord?.isDelivered || false, isCod: order.isCod, paymentMode: order.isCod ? "COD" : "Online", paymentStatus: statusRecord?.paymentStatus || "pending", slotId: order.slotId, adminNotes: order.adminNotes, userNotes: order.userNotes, }; }); return { success: true, data: formattedOrders }; }), updateAddressCoords: protectedProcedure .input( z.object({ addressId: z.number(), latitude: z.number(), longitude: z.number(), }) ) .mutation(async ({ input }) => { const { addressId, latitude, longitude } = input; const result = await db .update(addresses) .set({ adminLatitude: latitude, adminLongitude: longitude, }) .where(eq(addresses.id, addressId)) .returning(); if (result.length === 0) { throw new ApiError("Address not found", 404); } return { success: true }; }), getAll: protectedProcedure .input(getAllOrdersSchema) .query(async ({ input }) => { try { const { cursor, limit, slotId, packagedFilter, deliveredFilter, cancellationFilter, flashDeliveryFilter, } = input; let whereCondition: SQL | undefined = eq(orders.id, orders.id); // always true if (cursor) { whereCondition = and(whereCondition, lt(orders.id, cursor)); } if (slotId) { whereCondition = and(whereCondition, eq(orders.slotId, slotId)); } if (packagedFilter === "packaged") { whereCondition = and( whereCondition, eq(orderStatus.isPackaged, true) ); } else if (packagedFilter === "not_packaged") { whereCondition = and( whereCondition, eq(orderStatus.isPackaged, false) ); } if (deliveredFilter === "delivered") { whereCondition = and( whereCondition, eq(orderStatus.isDelivered, true) ); } else if (deliveredFilter === "not_delivered") { whereCondition = and( whereCondition, eq(orderStatus.isDelivered, false) ); } if (cancellationFilter === "cancelled") { whereCondition = and( whereCondition, eq(orderStatus.isCancelled, true) ); } else if (cancellationFilter === "not_cancelled") { whereCondition = and( whereCondition, eq(orderStatus.isCancelled, false) ); } if (flashDeliveryFilter === "flash") { whereCondition = and( whereCondition, eq(orders.isFlashDelivery, true) ); } else if (flashDeliveryFilter === "regular") { whereCondition = and( whereCondition, eq(orders.isFlashDelivery, false) ); } const allOrders = await db.query.orders.findMany({ where: whereCondition, orderBy: desc(orders.createdAt), limit: limit + 1, // fetch one extra to check if there's more with: { user: true, address: true, slot: true, orderItems: { with: { product: { with: { unit: true, }, }, }, }, orderStatus: true, }, }); const hasMore = allOrders.length > limit; const ordersToReturn = hasMore ? allOrders.slice(0, limit) : allOrders; const filteredOrders = ordersToReturn.filter((order) => { const statusRecord = order.orderStatus[0]; return ( order.isCod || (statusRecord && statusRecord.paymentStatus === "success") ); }); const formattedOrders = filteredOrders.map((order) => { const statusRecord = order.orderStatus[0]; let status: "pending" | "delivered" | "cancelled" = "pending"; if (statusRecord?.isCancelled) { status = "cancelled"; } else if (statusRecord?.isDelivered) { status = "delivered"; } const items = order.orderItems .map((item) => ({ id: item.id, name: item.product.name, quantity: parseFloat(item.quantity), price: parseFloat(item.price.toString()), amount: parseFloat(item.quantity) * parseFloat(item.price.toString()), unit: item.product.unit?.shortNotation || "", productSize: item.product.productQuantity, isPackaged: item.is_packaged, isPackageVerified: item.is_package_verified, })) .sort((first, second) => first.id - second.id); dayjs.extend(utc); return { id: order.id, orderId: order.id.toString(), readableId: order.id, customerName: order.user.name, address: `${order.address.addressLine1}${ order.address.addressLine2 ? `, ${order.address.addressLine2}` : "" }, ${order.address.city}, ${order.address.state} - ${ order.address.pincode }, Phone: ${order.address.phone}`, addressId: order.addressId, latitude: order.address.adminLatitude ?? order.address.latitude, longitude: order.address.adminLongitude ?? order.address.longitude, totalAmount: parseFloat(order.totalAmount), deliveryCharge: parseFloat(order.deliveryCharge || "0"), items, createdAt: order.createdAt, // deliveryTime: order.slot ? dayjs.utc(order.slot.deliveryTime).format('ddd, MMM D • h:mm A') : 'Not scheduled', deliveryTime: order.slot?.deliveryTime.toISOString() || null, status, isPackaged: order.orderItems.every((item) => item.is_packaged) || false, isDelivered: statusRecord?.isDelivered || false, isCod: order.isCod, isFlashDelivery: order.isFlashDelivery, userNotes: order.userNotes, adminNotes: order.adminNotes, }; }); return { orders: formattedOrders, nextCursor: hasMore ? ordersToReturn[ordersToReturn.length - 1].id : undefined, }; } catch (e) { console.log({ e }); } }), rebalanceSlots: protectedProcedure .input(z.object({ slotIds: z.array(z.number()).min(1).max(50) })) .mutation(async ({ input }) => { const slotIds = input.slotIds; const ordersList = await db.query.orders.findMany({ where: inArray(orders.slotId, slotIds), with: { orderItems: { with: { product: true } }, couponUsages: { with: { coupon: true } }, } }); const processedOrdersData = ordersList.map((order) => { let newTotal = order.orderItems.reduce((acc,item) => { const latestPrice = +item.product.price; const amount = (latestPrice * Number(item.quantity)); return acc+amount; },0) order.orderItems.forEach(item => { item.price = item.product.price; item.discountedPrice = item.product.price }) const coupon = order.couponUsages[0]?.coupon; let discount = 0; if(coupon && !coupon.isInvalidated && (!coupon.validTill || new Date(coupon.validTill) > new Date())) { const proportion = Number(order.orderGroupProportion || 1); if(coupon.discountPercent) { const maxDiscount = Number(coupon.maxValue || Infinity) * proportion; discount = Math.min((newTotal * parseFloat(coupon.discountPercent)) / 100, maxDiscount); } else { discount = Number(coupon.flatDiscount) * proportion; } } newTotal -= discount const { couponUsages, orderItems: orderItemsRaw, ...rest} = order; const updatedOrderItems = orderItemsRaw.map(item => { const { product, ...rawOrderItem } = item; return rawOrderItem; }) return {order: rest, updatedOrderItems, newTotal } }) const updatedOrderIds: number[] = []; await db.transaction(async (tx) => { for (const { order, updatedOrderItems, newTotal } of processedOrdersData) { await tx.update(orders).set({ totalAmount: newTotal.toString() }).where(eq(orders.id, order.id)); updatedOrderIds.push(order.id); for (const item of updatedOrderItems) { await tx.update(orderItems).set({ price: item.price, discountedPrice: item.discountedPrice }).where(eq(orderItems.id, item.id)); } } }); return { success: true, updatedOrders: updatedOrderIds, message: `Rebalanced ${updatedOrderIds.length} orders.` }; }), cancelOrder: protectedProcedure .input(z.object({ orderId: z.number(), reason: z.string().min(1, "Cancellation reason is required"), })) .mutation(async ({ input }) => { const { orderId, reason } = input; const order = await db.query.orders.findFirst({ where: eq(orders.id, orderId), with: { orderStatus: true, }, }); if (!order) { throw new ApiError("Order not found", 404); } const status = order.orderStatus[0]; if (!status) { throw new ApiError("Order status not found", 400); } if (status.isCancelled) { throw new ApiError("Order is already cancelled", 400); } if (status.isDelivered) { throw new ApiError("Cannot cancel delivered order", 400); } const result = await db.transaction(async (tx) => { await tx .update(orderStatus) .set({ isCancelled: true, isCancelledByAdmin: true, cancelReason: reason, cancellationAdminNotes: reason, cancellationReviewed: true, cancellationReviewedAt: new Date(), }) .where(eq(orderStatus.id, status.id)); const refundStatus = order.isCod ? "na" : "pending"; await tx.insert(refunds).values({ orderId: order.id, refundStatus, }); return { orderId: order.id, userId: order.userId }; }); // Publish to Redis for Telegram notification await publishCancellation(result.orderId, 'admin', reason); return { success: true, message: "Order cancelled successfully" }; }), }); // {"id": "order_Rhh00qJNdjUp8o", "notes": {"retry": "true", "customerOrderId": "14"}, "amount": 21000, "entity": "order", "status": "created", "receipt": "order_14_retry", "attempts": 0, "currency": "INR", "offer_id": null, "signature": "6df20655021f1d6841340f2a2ef2ef9378cb3d43495ab09e85f08aea1a851583", "amount_due": 21000, "created_at": 1763575791, "payment_id": "pay_Rhh15cLL28YM7j", "amount_paid": 0} type RefundStatus = "success" | "pending" | "failed" | "none" | "na";