freshyo/packages/db_helper_postgres/src/admin-apis/order.ts
2026-03-25 19:30:01 +05:30

710 lines
22 KiB
TypeScript

import { db } from '../db/db_index'
import {
addresses,
complaints,
couponUsage,
orderItems,
orders,
orderStatus,
payments,
refunds,
} from '../db/schema'
import { and, desc, eq, inArray, lt, SQL } from 'drizzle-orm'
import type {
AdminOrderDetails,
AdminOrderRow,
AdminOrderStatusRecord,
AdminOrderUpdateResult,
AdminOrderItemPackagingResult,
AdminOrderMessageResult,
AdminOrderBasicResult,
AdminGetSlotOrdersResult,
AdminGetAllOrdersResultWithUserId,
AdminRebalanceSlotsResult,
AdminCancelOrderResult,
AdminRefundRecord,
RefundStatus,
PaymentStatus,
} from '@packages/shared'
import type { InferSelectModel } from 'drizzle-orm'
const isPaymentStatus = (value: string): value is PaymentStatus =>
value === 'pending' || value === 'success' || value === 'cod' || value === 'failed'
const isRefundStatus = (value: string): value is RefundStatus =>
value === 'success' || value === 'pending' || value === 'failed' || value === 'none' || value === 'na' || value === 'processed'
type OrderStatusRow = InferSelectModel<typeof orderStatus>
const mapOrderStatusRecord = (record: OrderStatusRow): AdminOrderStatusRecord => ({
id: record.id,
orderTime: record.orderTime,
userId: record.userId,
orderId: record.orderId,
isPackaged: record.isPackaged,
isDelivered: record.isDelivered,
isCancelled: record.isCancelled,
cancelReason: record.cancelReason ?? null,
isCancelledByAdmin: record.isCancelledByAdmin ?? null,
paymentStatus: isPaymentStatus(record.paymentStatus) ? record.paymentStatus : 'pending',
cancellationUserNotes: record.cancellationUserNotes ?? null,
cancellationAdminNotes: record.cancellationAdminNotes ?? null,
cancellationReviewed: record.cancellationReviewed,
cancellationReviewedAt: record.cancellationReviewedAt ?? null,
refundCouponId: record.refundCouponId ?? null,
})
export async function updateOrderNotes(orderId: number, adminNotes: string | null): Promise<AdminOrderRow | null> {
const [result] = await db
.update(orders)
.set({ adminNotes })
.where(eq(orders.id, orderId))
.returning()
return result || null
}
export async function updateOrderPackaged(orderId: string, isPackaged: boolean): Promise<AdminOrderUpdateResult> {
const orderIdNumber = parseInt(orderId)
await db
.update(orderItems)
.set({ is_packaged: isPackaged })
.where(eq(orderItems.orderId, orderIdNumber))
if (!isPackaged) {
await db
.update(orderStatus)
.set({ isPackaged, isDelivered: false })
.where(eq(orderStatus.orderId, orderIdNumber))
} else {
await db
.update(orderStatus)
.set({ isPackaged })
.where(eq(orderStatus.orderId, orderIdNumber))
}
const order = await db.query.orders.findFirst({
where: eq(orders.id, orderIdNumber),
})
return { success: true, userId: order?.userId ?? null }
}
export async function updateOrderDelivered(orderId: string, isDelivered: boolean): Promise<AdminOrderUpdateResult> {
const orderIdNumber = parseInt(orderId)
await db
.update(orderStatus)
.set({ isDelivered })
.where(eq(orderStatus.orderId, orderIdNumber))
const order = await db.query.orders.findFirst({
where: eq(orders.id, orderIdNumber),
})
return { success: true, userId: order?.userId ?? null }
}
export async function getOrderDetails(orderId: number): Promise<AdminOrderDetails | null> {
// 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,
refunds: true,
},
})
if (!orderData) {
return null
}
const couponUsageData = await db.query.couponUsage.findMany({
where: eq(couponUsage.orderId, orderData.id),
with: {
coupon: true,
},
})
let couponData = null
if (couponUsageData.length > 0) {
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())
}
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,
}
}
const statusRecord = orderData.orderStatus?.[0]
const orderStatusRecord = statusRecord ? mapOrderStatusRecord(statusRecord) : null
let status: 'pending' | 'delivered' | 'cancelled' = 'pending'
if (orderStatusRecord?.isCancelled) {
status = 'cancelled'
} else if (orderStatusRecord?.isDelivered) {
status = 'delivered'
}
const refund = orderData.refunds?.[0]
const refundStatus = refund?.refundStatus && isRefundStatus(refund.refundStatus)
? refund.refundStatus
: null
const refundRecord: AdminRefundRecord | null = refund
? {
id: refund.id,
orderId: refund.orderId,
refundAmount: refund.refundAmount,
refundStatus,
merchantRefundId: refund.merchantRefundId,
refundProcessedAt: refund.refundProcessedAt,
createdAt: refund.createdAt,
}
: null
return {
id: orderData.id,
readableId: orderData.id,
userId: orderData.user.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:
parseFloat(orderData.totalAmount?.toString() || '0') -
parseFloat(orderData.deliveryCharge?.toString() || '0'),
deliveryCharge: parseFloat(orderData.deliveryCharge?.toString() || '0'),
adminNotes: orderData.adminNotes,
userNotes: orderData.userNotes,
createdAt: orderData.createdAt,
status,
isPackaged: orderStatusRecord?.isPackaged || false,
isDelivered: orderStatusRecord?.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,
cancelReason: orderStatusRecord?.cancelReason || null,
cancellationReviewed: orderStatusRecord?.cancellationReviewed || false,
isRefundDone: refundStatus === 'processed' || false,
refundStatus,
refundAmount: refund?.refundAmount
? parseFloat(refund.refundAmount.toString())
: null,
couponData,
couponCode: couponData?.couponCode || null,
couponDescription: couponData?.couponDescription || null,
discountAmount: couponData?.discountAmount || null,
orderStatus: orderStatusRecord,
refundRecord,
isFlashDelivery: orderData.isFlashDelivery,
}
}
export async function updateOrderItemPackaging(
orderItemId: number,
isPackaged?: boolean,
isPackageVerified?: boolean
): Promise<AdminOrderItemPackagingResult> {
const orderItem = await db.query.orderItems.findFirst({
where: eq(orderItems.id, orderItemId),
})
if (!orderItem) {
return { success: false, updated: false }
}
const updateData: Partial<{
is_packaged: boolean
is_package_verified: boolean
}> = {}
if (isPackaged !== undefined) {
updateData.is_packaged = isPackaged
}
if (isPackageVerified !== undefined) {
updateData.is_package_verified = isPackageVerified
}
await db
.update(orderItems)
.set(updateData)
.where(eq(orderItems.id, orderItemId))
return { success: true, updated: true }
}
export async function removeDeliveryCharge(orderId: number): Promise<AdminOrderMessageResult | null> {
const order = await db.query.orders.findFirst({
where: eq(orders.id, orderId),
})
if (!order) {
return null
}
const currentDeliveryCharge = parseFloat(order.deliveryCharge?.toString() || '0')
const currentTotalAmount = parseFloat(order.totalAmount?.toString() || '0')
const newTotalAmount = currentTotalAmount - currentDeliveryCharge
await db
.update(orders)
.set({
deliveryCharge: '0',
totalAmount: newTotalAmount.toString(),
})
.where(eq(orders.id, orderId))
return { success: true, message: 'Delivery charge removed' }
}
export async function getSlotOrders(slotId: string): Promise<AdminGetSlotOrdersResult> {
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]
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,
}))
const paymentMode: 'COD' | 'Online' = order.isCod ? 'COD' : 'Online'
return {
id: order.id,
readableId: order.id,
customerName: order.user.name || order.user.mobile+'',
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,
paymentStatus: isPaymentStatus(statusRecord?.paymentStatus || 'pending')
? statusRecord?.paymentStatus || 'pending'
: 'pending',
slotId: order.slotId,
adminNotes: order.adminNotes,
userNotes: order.userNotes,
}
})
return { success: true, data: formattedOrders }
}
export async function updateAddressCoords(
addressId: number,
latitude: number,
longitude: number
): Promise<AdminOrderBasicResult> {
const result = await db
.update(addresses)
.set({
adminLatitude: latitude,
adminLongitude: longitude,
})
.where(eq(addresses.id, addressId))
.returning()
return { success: result.length > 0 }
}
type GetAllOrdersInput = {
cursor?: number
limit: number
slotId?: number | null
packagedFilter?: 'all' | 'packaged' | 'not_packaged'
deliveredFilter?: 'all' | 'delivered' | 'not_delivered'
cancellationFilter?: 'all' | 'cancelled' | 'not_cancelled'
flashDeliveryFilter?: 'all' | 'flash' | 'regular'
}
export async function getAllOrders(input: GetAllOrdersInput): Promise<AdminGetAllOrdersResultWithUserId> {
const {
cursor,
limit,
slotId,
packagedFilter,
deliveredFilter,
cancellationFilter,
flashDeliveryFilter,
} = input
let whereCondition: SQL<unknown> | undefined = eq(orders.id, orders.id)
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,
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)
return {
id: order.id,
orderId: order.id.toString(),
readableId: order.id,
customerName: order.user.name || order.user.mobile + '',
customerMobile: order.user.mobile,
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?.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,
userNegativityScore: 0,
userId: order.userId,
}
})
return {
orders: formattedOrders,
nextCursor: hasMore ? ordersToReturn[ordersToReturn.length - 1].id : undefined,
}
}
export async function rebalanceSlots(slotIds: number[]): Promise<AdminRebalanceSlotsResult> {
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.`,
}
}
export async function cancelOrder(orderId: number, reason: string): Promise<AdminCancelOrderResult> {
const order = await db.query.orders.findFirst({
where: eq(orders.id, orderId),
with: {
orderStatus: true,
},
})
if (!order) {
return { success: false, message: 'Order not found', error: 'order_not_found' }
}
const status = order.orderStatus[0]
if (!status) {
return { success: false, message: 'Order status not found', error: 'status_not_found' }
}
if (status.isCancelled) {
return { success: false, message: 'Order is already cancelled', error: 'already_cancelled' }
}
if (status.isDelivered) {
return { success: false, message: 'Cannot cancel delivered order', error: 'already_delivered' }
}
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 }
})
return {
success: true,
message: 'Order cancelled successfully',
orderId: result.orderId,
userId: result.userId,
}
}
export async function deleteOrderById(orderId: number): Promise<void> {
await db.transaction(async (tx) => {
await tx.delete(orderItems).where(eq(orderItems.orderId, orderId))
await tx.delete(orderStatus).where(eq(orderStatus.orderId, orderId))
await tx.delete(payments).where(eq(payments.orderId, orderId))
await tx.delete(refunds).where(eq(refunds.orderId, orderId))
await tx.delete(couponUsage).where(eq(couponUsage.orderId, orderId))
await tx.delete(complaints).where(eq(complaints.orderId, orderId))
await tx.delete(orders).where(eq(orders.id, orderId))
})
}