Compare commits

...

2 commits

Author SHA1 Message Date
shafi54
3c836e274d enh 2026-03-25 19:30:01 +05:30
shafi54
306244e8df enh 2026-03-25 18:11:46 +05:30
36 changed files with 5429 additions and 600 deletions

View file

@ -1,197 +0,0 @@
import React from 'react';
import { View, ScrollView, Dimensions } from 'react-native';
import { Image } from 'expo-image';
import { MyText, tw } from 'common-ui';
import { trpc } from '../src/trpc-client';
interface FullOrderViewProps {
orderId: number;
}
export const FullOrderView: React.FC<FullOrderViewProps> = ({ orderId }) => {
const { data: order, isLoading, error } = trpc.admin.order.getFullOrder.useQuery({ orderId });
if (isLoading) {
return (
<View style={tw`p-6`}>
<MyText style={tw`text-center text-gray-600`}>Loading order details...</MyText>
</View>
);
}
if (error || !order) {
return (
<View style={tw`p-6`}>
<MyText style={tw`text-center text-red-600`}>Failed to load order details</MyText>
</View>
);
}
const totalAmount = order.items.reduce((sum, item) => sum + item.amount, 0);
return (
<ScrollView
style={[tw`flex-1`, { maxHeight: Dimensions.get('window').height * 0.8 }]}
showsVerticalScrollIndicator={false}
>
<View style={tw`p-6`}>
<MyText style={tw`text-2xl font-bold text-gray-800 mb-6`}>Order #{order.readableId}</MyText>
{/* Customer Information */}
<View style={tw`bg-white rounded-xl p-4 mb-4 shadow-sm`}>
<MyText style={tw`text-lg font-semibold text-gray-800 mb-3`}>Customer Details</MyText>
<View style={tw`space-y-2`}>
<View style={tw`flex-row justify-between`}>
<MyText style={tw`text-gray-600`}>Name:</MyText>
<MyText style={tw`font-medium`}>{order.customerName}</MyText>
</View>
{order.customerEmail && (
<View style={tw`flex-row justify-between`}>
<MyText style={tw`text-gray-600`}>Email:</MyText>
<MyText style={tw`font-medium`}>{order.customerEmail}</MyText>
</View>
)}
<View style={tw`flex-row justify-between`}>
<MyText style={tw`text-gray-600`}>Mobile:</MyText>
<MyText style={tw`font-medium`}>{order.customerMobile}</MyText>
</View>
</View>
</View>
{/* Delivery Address */}
<View style={tw`bg-white rounded-xl p-4 mb-4 shadow-sm`}>
<MyText style={tw`text-lg font-semibold text-gray-800 mb-3`}>Delivery Address</MyText>
<View style={tw`space-y-1`}>
<MyText style={tw`text-gray-800`}>{order.address.line1}</MyText>
{order.address.line2 && <MyText style={tw`text-gray-800`}>{order.address.line2}</MyText>}
<MyText style={tw`text-gray-800`}>
{order.address.city}, {order.address.state} - {order.address.pincode}
</MyText>
<MyText style={tw`text-gray-800`}>Phone: {order.address.phone}</MyText>
</View>
</View>
{/* Order Details */}
<View style={tw`bg-white rounded-xl p-4 mb-4 shadow-sm`}>
<MyText style={tw`text-lg font-semibold text-gray-800 mb-3`}>Order Details</MyText>
<View style={tw`space-y-2`}>
<View style={tw`flex-row justify-between`}>
<MyText style={tw`text-gray-600`}>Order Date:</MyText>
<MyText style={tw`font-medium`}>
{new Date(order.createdAt).toLocaleDateString('en-IN', {
day: 'numeric',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
</MyText>
</View>
<View style={tw`flex-row justify-between`}>
<MyText style={tw`text-gray-600`}>Payment Method:</MyText>
<MyText style={tw`font-medium`}>
{order.isCod ? 'Cash on Delivery' : 'Online Payment'}
</MyText>
</View>
{order.slotInfo && (
<View style={tw`flex-row justify-between`}>
<MyText style={tw`text-gray-600`}>Delivery Slot:</MyText>
<MyText style={tw`font-medium`}>
{new Date(order.slotInfo.time).toLocaleDateString('en-IN', {
day: 'numeric',
month: 'short',
hour: '2-digit',
minute: '2-digit'
})}
</MyText>
</View>
)}
</View>
</View>
{/* Items */}
<View style={tw`bg-white rounded-xl p-4 mb-4 shadow-sm`}>
<MyText style={tw`text-lg font-semibold text-gray-800 mb-3`}>Items ({order.items.length})</MyText>
{order.items.map((item, index) => (
<View key={item.id} style={tw`flex-row items-center py-3 ${index !== order.items.length - 1 ? 'border-b border-gray-100' : ''}`}>
<View style={tw`flex-1`}>
<MyText style={tw`font-medium text-gray-800`} numberOfLines={2}>
{item.productName}
</MyText>
<MyText style={tw`text-sm text-gray-600`}>
Qty: {item.quantity} {item.unit} × {parseFloat(item.price.toString()).toFixed(2)}
</MyText>
</View>
<MyText style={tw`font-semibold text-gray-800`}>{item.amount.toFixed(2)}</MyText>
</View>
))}
</View>
{/* Payment Information */}
{(order.payment || order.paymentInfo) && (
<View style={tw`bg-white rounded-xl p-4 mb-4 shadow-sm`}>
<MyText style={tw`text-lg font-semibold text-gray-800 mb-3`}>Payment Information</MyText>
{order.payment && (
<View style={tw`space-y-2 mb-3`}>
<MyText style={tw`text-sm font-medium text-gray-700`}>Payment Details:</MyText>
<View style={tw`flex-row justify-between`}>
<MyText style={tw`text-gray-600`}>Status:</MyText>
<MyText style={tw`font-medium capitalize`}>{order.payment.status}</MyText>
</View>
<View style={tw`flex-row justify-between`}>
<MyText style={tw`text-gray-600`}>Gateway:</MyText>
<MyText style={tw`font-medium`}>{order.payment.gateway}</MyText>
</View>
<View style={tw`flex-row justify-between`}>
<MyText style={tw`text-gray-600`}>Order ID:</MyText>
<MyText style={tw`font-medium`}>{order.payment.merchantOrderId}</MyText>
</View>
</View>
)}
{order.paymentInfo && (
<View style={tw`space-y-2`}>
<MyText style={tw`text-sm font-medium text-gray-700`}>Payment Info:</MyText>
<View style={tw`flex-row justify-between`}>
<MyText style={tw`text-gray-600`}>Status:</MyText>
<MyText style={tw`font-medium capitalize`}>{order.paymentInfo.status}</MyText>
</View>
<View style={tw`flex-row justify-between`}>
<MyText style={tw`text-gray-600`}>Gateway:</MyText>
<MyText style={tw`font-medium`}>{order.paymentInfo.gateway}</MyText>
</View>
<View style={tw`flex-row justify-between`}>
<MyText style={tw`text-gray-600`}>Order ID:</MyText>
<MyText style={tw`font-medium`}>{order.paymentInfo.merchantOrderId}</MyText>
</View>
</View>
)}
</View>
)}
{/* User Notes */}
{order.userNotes && (
<View style={tw`bg-blue-50 rounded-xl p-4 mb-4`}>
<MyText style={tw`text-lg font-semibold text-gray-800 mb-2`}>Customer Notes</MyText>
<MyText style={tw`text-gray-700`}>{order.userNotes}</MyText>
</View>
)}
{/* Admin Notes */}
{order.adminNotes && (
<View style={tw`bg-yellow-50 rounded-xl p-4 mb-4`}>
<MyText style={tw`text-lg font-semibold text-gray-800 mb-2`}>Admin Notes</MyText>
<MyText style={tw`text-gray-700`}>{order.adminNotes}</MyText>
</View>
)}
{/* Total */}
<View style={tw`bg-blue-50 rounded-xl p-4`}>
<View style={tw`flex-row justify-between items-center`}>
<MyText style={tw`text-xl font-bold text-gray-800`}>Total Amount</MyText>
<MyText style={tw`text-2xl font-bold text-blue-600`}>{parseFloat(order.totalAmount.toString()).toFixed(2)}</MyText>
</View>
</View>
</View>
</ScrollView>
);
};

View file

@ -2,13 +2,17 @@
// This file re-exports everything from postgresService to provide a clean abstraction layer
// Implementation is the responsibility of postgresService package
import { getOrderDetails as getOrderDetailsFromDb } from 'postgresService'
import type { AdminOrderDetails } from '@packages/shared'
// Re-export database connection
export { db } from 'postgresService';
export { db } from 'postgresService'
// Re-export all schema exports
export * from 'postgresService';
export * from 'postgresService'
// Re-export methods from postgresService (implementation lives there)
export {
// Banner methods
getBanners,
@ -22,14 +26,13 @@ export {
// Constants methods
getAllConstants,
upsertConstants,
// Coupon methods (batch 1 - non-transaction)
// Coupon methods
getAllCoupons,
getCouponById,
invalidateCoupon,
validateCoupon,
getReservedCoupons,
getUsersForCoupon,
// Coupon methods (batch 2 - transactions)
createCouponWithRelations,
updateCouponWithRelations,
generateCancellationCoupon,
@ -48,10 +51,9 @@ export {
// Staff-user methods
getStaffUserByName,
getAllStaff,
getStaffByName,
getAllUsers,
getUserWithDetails,
updateUserSuspension,
updateUserSuspensionStatus,
checkStaffUserExists,
checkStaffRoleExists,
createStaffUser,
@ -90,12 +92,17 @@ export {
getOrderItemsByOrderIds,
getOrderStatusByOrderIds,
updateVendorOrderItemPackaging,
getVendorOrders,
// Product methods
getAllProducts,
getProductById,
deleteProduct,
createProduct,
updateProduct,
toggleProductOutOfStock,
updateSlotProducts,
getSlotProductIds,
getSlotsProductIds,
getAllUnits,
getAllProductTags,
getProductReviews,
@ -106,35 +113,123 @@ export {
deleteProductGroup,
addProductToGroup,
removeProductFromGroup,
updateProductPrices,
// Slots methods
getAllSlots,
getSlotById,
createSlot,
updateSlot,
deleteSlot,
getSlotProducts,
addProductToSlot,
removeProductFromSlot,
clearSlotProducts,
getActiveSlotsWithProducts,
getActiveSlots,
getSlotsAfterDate,
getSlotByIdWithRelations,
createSlotWithRelations,
updateSlotWithRelations,
deleteSlotById,
updateSlotCapacity,
getSlotDeliverySequence,
updateSlotDeliverySequence,
// Order methods
updateOrderNotes,
getOrderWithDetails,
getFullOrder,
getOrderDetails,
getAllOrders,
getOrdersBySlotId,
updateOrderPackaged,
updateOrderDelivered,
updateOrderItemPackaging,
updateAddressCoords,
getOrderStatus,
cancelOrder,
getTodaysOrders,
removeDeliveryCharge,
} from 'postgresService';
getSlotOrders,
updateAddressCoords,
getAllOrders,
rebalanceSlots,
cancelOrder,
deleteOrderById,
} from 'postgresService'
// Re-export types from local types file (to avoid circular dependencies)
export type { Banner } from './types/db.types';
export async function getOrderDetails(orderId: number): Promise<AdminOrderDetails | null> {
return getOrderDetailsFromDb(orderId)
}
// Re-export all types from shared package
export type {
// Admin types
Banner,
Complaint,
ComplaintWithUser,
Constant,
ConstantUpdateResult,
Coupon,
CouponValidationResult,
UserMiniInfo,
Store,
StaffUser,
StaffRole,
AdminOrderRow,
AdminOrderDetails,
AdminOrderUpdateResult,
AdminOrderItemPackagingResult,
AdminOrderMessageResult,
AdminOrderBasicResult,
AdminGetSlotOrdersResult,
AdminGetAllOrdersResult,
AdminGetAllOrdersResultWithUserId,
AdminRebalanceSlotsResult,
AdminCancelOrderResult,
AdminUnit,
AdminProduct,
AdminProductWithRelations,
AdminProductWithDetails,
AdminProductTagInfo,
AdminProductTagWithProducts,
AdminProductListResponse,
AdminProductResponse,
AdminDeleteProductResult,
AdminToggleOutOfStockResult,
AdminUpdateSlotProductsResult,
AdminSlotProductIdsResult,
AdminSlotsProductIdsResult,
AdminProductReview,
AdminProductReviewWithSignedUrls,
AdminProductReviewsResult,
AdminProductReviewResponse,
AdminProductGroup,
AdminProductGroupsResult,
AdminProductGroupResponse,
AdminProductGroupInfo,
AdminUpdateProductPricesResult,
AdminDeliverySlot,
AdminSlotProductSummary,
AdminSlotWithProducts,
AdminSlotWithProductsAndSnippets,
AdminSlotWithProductsAndSnippetsBase,
AdminSlotsResult,
AdminSlotsListResult,
AdminSlotResult,
AdminSlotCreateResult,
AdminSlotUpdateResult,
AdminSlotDeleteResult,
AdminDeliverySequence,
AdminDeliverySequenceResult,
AdminUpdateDeliverySequenceResult,
AdminUpdateSlotCapacityResult,
AdminVendorSnippet,
AdminVendorSnippetWithAccess,
AdminVendorSnippetWithSlot,
AdminVendorSnippetProduct,
AdminVendorSnippetWithProducts,
AdminVendorSnippetCreateInput,
AdminVendorSnippetUpdateInput,
AdminVendorSnippetDeleteResult,
AdminVendorSnippetOrderProduct,
AdminVendorSnippetOrderSummary,
AdminVendorSnippetOrdersResult,
AdminVendorSnippetOrdersWithSlotResult,
AdminVendorOrderSummary,
AdminUpcomingSlotsResult,
AdminVendorUpdatePackagingResult,
} from '@packages/shared';
export type {
// User types
User,
UserDetails,
Address,
Product,
CartItem,
Order,
OrderItem,
Payment,
} from '@packages/shared';

View file

@ -1,21 +1,5 @@
import { router, protectedProcedure } from "@/src/trpc/trpc-index"
import { z } from "zod";
import { db } from "@/src/db/db_index"
import {
orders,
orderItems,
orderStatus,
users,
addresses,
refunds,
coupons,
couponUsage,
complaints,
payments,
} from "@/src/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 "@/src/lib/api-error"
import {
sendOrderPackagedNotification,
@ -23,16 +7,38 @@ import {
} from "@/src/lib/notif-job";
import { publishCancellation } from "@/src/lib/post-order-handler"
import { getMultipleUserNegativityScores } from "@/src/stores/user-negativity-store"
import {
updateOrderNotes as updateOrderNotesInDb,
getOrderDetails as getOrderDetailsInDb,
updateOrderPackaged as updateOrderPackagedInDb,
updateOrderDelivered as updateOrderDeliveredInDb,
updateOrderItemPackaging as updateOrderItemPackagingInDb,
removeDeliveryCharge as removeDeliveryChargeInDb,
getSlotOrders as getSlotOrdersInDb,
updateAddressCoords as updateAddressCoordsInDb,
getAllOrders as getAllOrdersInDb,
rebalanceSlots as rebalanceSlotsInDb,
cancelOrder as cancelOrderInDb,
deleteOrderById as deleteOrderByIdInDb,
} from '@/src/dbService'
import type {
AdminCancelOrderResult,
AdminGetAllOrdersResult,
AdminGetSlotOrdersResult,
AdminOrderBasicResult,
AdminOrderDetails,
AdminOrderItemPackagingResult,
AdminOrderMessageResult,
AdminOrderRow,
AdminOrderUpdateResult,
AdminRebalanceSlotsResult,
} from "@packages/shared"
const updateOrderNotesSchema = z.object({
orderId: z.number(),
adminNotes: z.string(),
});
const getFullOrderSchema = z.object({
orderId: z.number(),
});
const getOrderDetailsSchema = z.object({
orderId: z.number(),
});
@ -57,10 +63,6 @@ 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),
@ -86,9 +88,13 @@ const getAllOrdersSchema = z.object({
export const orderRouter = router({
updateNotes: protectedProcedure
.input(updateOrderNotesSchema)
.mutation(async ({ input }) => {
.mutation(async ({ input }): Promise<AdminOrderRow> => {
const { orderId, adminNotes } = input;
const result = await updateOrderNotesInDb(orderId, adminNotes || null)
/*
// Old implementation - direct DB query:
const result = await db
.update(orders)
.set({
@ -100,125 +106,24 @@ export const orderRouter = router({
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");
if (!result) {
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,
};
return result as AdminOrderRow;
}),
getOrderDetails: protectedProcedure
.input(getOrderDetailsSchema)
.query(async ({ input }) => {
.query(async ({ input }): Promise<AdminOrderDetails> => {
const { orderId } = input;
const orderDetails = await getOrderDetailsInDb(orderId)
/*
// Old implementation - direct DB queries:
// Single optimized query with all relations
const orderData = await db.query.orders.findFirst({
where: eq(orders.id, orderId),
@ -237,8 +142,8 @@ export const orderRouter = router({
},
payment: true,
paymentInfo: true,
orderStatus: true, // Include in main query
refunds: true, // Include in main query
orderStatus: true,
refunds: true,
},
});
@ -248,7 +153,7 @@ export const orderRouter = router({
// 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
where: eq(couponUsage.orderId, orderData.id),
with: {
coupon: true,
},
@ -380,13 +285,24 @@ export const orderRouter = router({
refundRecord: refund,
isFlashDelivery: orderData.isFlashDelivery,
};
*/
if (!orderDetails) {
throw new Error('Order not found')
}
return orderDetails
}),
updatePackaged: protectedProcedure
.input(updatePackagedSchema)
.mutation(async ({ input }) => {
.mutation(async ({ input }): Promise<AdminOrderUpdateResult> => {
const { orderId, isPackaged } = input;
const result = await updateOrderPackagedInDb(orderId, isPackaged)
/*
// Old implementation - direct DB queries:
// Update all order items to the specified packaged state
await db
.update(orderItems)
@ -412,13 +328,22 @@ export const orderRouter = router({
if (order) await sendOrderPackagedNotification(order.userId, orderId);
return { success: true };
*/
if (result.userId) await sendOrderPackagedNotification(result.userId, orderId)
return { success: true, userId: result.userId }
}),
updateDelivered: protectedProcedure
.input(updateDeliveredSchema)
.mutation(async ({ input }) => {
.mutation(async ({ input }): Promise<AdminOrderUpdateResult> => {
const { orderId, isDelivered } = input;
const result = await updateOrderDeliveredInDb(orderId, isDelivered)
/*
// Old implementation - direct DB queries:
await db
.update(orderStatus)
.set({ isDelivered })
@ -430,13 +355,22 @@ export const orderRouter = router({
if (order) await sendOrderDeliveredNotification(order.userId, orderId);
return { success: true };
*/
if (result.userId) await sendOrderDeliveredNotification(result.userId, orderId)
return { success: true, userId: result.userId }
}),
updateOrderItemPackaging: protectedProcedure
.input(updateOrderItemPackagingSchema)
.mutation(async ({ input }) => {
.mutation(async ({ input }): Promise<AdminOrderItemPackagingResult> => {
const { orderItemId, isPackaged, isPackageVerified } = input;
const result = await updateOrderItemPackagingInDb(orderItemId, isPackaged, isPackageVerified)
/*
// Old implementation - direct DB queries:
// Validate that orderItem exists
const orderItem = await db.query.orderItems.findFirst({
where: eq(orderItems.id, orderItemId),
@ -462,13 +396,24 @@ export const orderRouter = router({
.where(eq(orderItems.id, orderItemId));
return { success: true };
*/
if (!result.updated) {
throw new ApiError('Order item not found', 404)
}
return result
}),
removeDeliveryCharge: protectedProcedure
.input(z.object({ orderId: z.number() }))
.mutation(async ({ input }) => {
.mutation(async ({ input }): Promise<AdminOrderMessageResult> => {
const { orderId } = input;
const result = await removeDeliveryChargeInDb(orderId)
/*
// Old implementation - direct DB queries:
const order = await db.query.orders.findFirst({
where: eq(orders.id, orderId),
});
@ -490,13 +435,24 @@ export const orderRouter = router({
.where(eq(orders.id, orderId));
return { success: true, message: 'Delivery charge removed' };
*/
if (!result) {
throw new Error('Order not found')
}
return result
}),
getSlotOrders: protectedProcedure
.input(getSlotOrdersSchema)
.query(async ({ input }) => {
.query(async ({ input }): Promise<AdminGetSlotOrdersResult> => {
const { slotId } = input;
const result = await getSlotOrdersInDb(slotId)
/*
// Old implementation - direct DB queries:
const slotOrders = await db.query.orders.findMany({
where: eq(orders.slotId, parseInt(slotId)),
with: {
@ -573,97 +529,9 @@ export const orderRouter = router({
});
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 };
return result
}),
updateAddressCoords: protectedProcedure
@ -674,9 +542,13 @@ export const orderRouter = router({
longitude: z.number(),
})
)
.mutation(async ({ input }) => {
.mutation(async ({ input }): Promise<AdminOrderBasicResult> => {
const { addressId, latitude, longitude } = input;
const result = await updateAddressCoordsInDb(addressId, latitude, longitude)
/*
// Old implementation - direct DB queries:
const result = await db
.update(addresses)
.set({
@ -691,12 +563,33 @@ export const orderRouter = router({
}
return { success: true };
*/
if (!result.success) {
throw new ApiError('Address not found', 404)
}
return result
}),
getAll: protectedProcedure
.input(getAllOrdersSchema)
.query(async ({ input }) => {
.query(async ({ input }): Promise<AdminGetAllOrdersResult | undefined> => {
try {
const result = await getAllOrdersInDb(input)
const userIds = [...new Set(result.orders.map((order) => order.userId))]
const negativityScores = await getMultipleUserNegativityScores(userIds)
const orders = result.orders.map((order) => {
const { userId, userNegativityScore, ...rest } = order
return {
...rest,
userNegativityScore: negativityScores[userId] || 0,
}
})
/*
// Old implementation - direct DB queries:
const {
cursor,
limit,
@ -858,6 +751,12 @@ export const orderRouter = router({
? ordersToReturn[ordersToReturn.length - 1].id
: undefined,
};
*/
return {
orders,
nextCursor: result.nextCursor,
}
} catch (e) {
console.log({ e });
}
@ -865,9 +764,13 @@ export const orderRouter = router({
rebalanceSlots: protectedProcedure
.input(z.object({ slotIds: z.array(z.number()).min(1).max(50) }))
.mutation(async ({ input }) => {
.mutation(async ({ input }): Promise<AdminRebalanceSlotsResult> => {
const slotIds = input.slotIds;
const result = await rebalanceSlotsInDb(slotIds)
/*
// Old implementation - direct DB queries:
const ordersList = await db.query.orders.findMany({
where: inArray(orders.slotId, slotIds),
with: {
@ -936,6 +839,9 @@ export const orderRouter = router({
});
return { success: true, updatedOrders: updatedOrderIds, message: `Rebalanced ${updatedOrderIds.length} orders.` };
*/
return result
}),
cancelOrder: protectedProcedure
@ -943,9 +849,13 @@ export const orderRouter = router({
orderId: z.number(),
reason: z.string().min(1, "Cancellation reason is required"),
}))
.mutation(async ({ input }) => {
.mutation(async ({ input }): Promise<AdminCancelOrderResult> => {
const { orderId, reason } = input;
const result = await cancelOrderInDb(orderId, reason)
/*
// Old implementation - direct DB queries:
const order = await db.query.orders.findFirst({
where: eq(orders.id, orderId),
with: {
@ -997,14 +907,40 @@ export const orderRouter = router({
await publishCancellation(result.orderId, 'admin', reason);
return { success: true, message: "Order cancelled successfully" };
*/
if (!result.success) {
if (result.error === 'order_not_found') {
throw new ApiError(result.message, 404)
}
if (result.error === 'status_not_found') {
throw new ApiError(result.message, 400)
}
if (result.error === 'already_cancelled') {
throw new ApiError(result.message, 400)
}
if (result.error === 'already_delivered') {
throw new ApiError(result.message, 400)
}
throw new ApiError(result.message, 400)
}
if (result.orderId) {
await publishCancellation(result.orderId, 'admin', reason)
}
return { success: true, message: result.message }
}),
});
// {"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";
export async function deleteOrderById(orderId: number): Promise<void> {
await deleteOrderByIdInDb(orderId)
/*
// Old implementation - direct DB queries:
await db.transaction(async (tx) => {
await tx.delete(orderItems).where(eq(orderItems.orderId, orderId));
await tx.delete(orderStatus).where(eq(orderStatus.orderId, orderId));
@ -1014,5 +950,5 @@ export async function deleteOrderById(orderId: number): Promise<void> {
await tx.delete(complaints).where(eq(complaints.orderId, orderId));
await tx.delete(orders).where(eq(orders.id, orderId));
});
*/
}

View file

@ -1,24 +1,47 @@
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod';
import { db } from '@/src/db/db_index'
import { productInfo, units, specialDeals, productSlots, productTags, productReviews, users, productGroupInfo, productGroupMembership } from '@/src/db/schema'
import { eq, and, inArray, desc, sql } from 'drizzle-orm';
import { z } from 'zod'
import { ApiError } from '@/src/lib/api-error'
import { imageUploadS3, generateSignedUrlsFromS3Urls, getOriginalUrlFromSignedUrl, claimUploadUrl } from '@/src/lib/s3-client'
import { deleteS3Image } from '@/src/lib/delete-image'
import type { SpecialDeal } from '@/src/db/types'
import { generateSignedUrlsFromS3Urls, claimUploadUrl } from '@/src/lib/s3-client'
import { scheduleStoreInitialization } from '@/src/stores/store-initializer'
import {
getAllProducts as getAllProductsInDb,
getProductById as getProductByIdInDb,
deleteProduct as deleteProductInDb,
toggleProductOutOfStock as toggleProductOutOfStockInDb,
updateSlotProducts as updateSlotProductsInDb,
getSlotProductIds as getSlotProductIdsInDb,
getSlotsProductIds as getSlotsProductIdsInDb,
getProductReviews as getProductReviewsInDb,
respondToReview as respondToReviewInDb,
getAllProductGroups as getAllProductGroupsInDb,
createProductGroup as createProductGroupInDb,
updateProductGroup as updateProductGroupInDb,
deleteProductGroup as deleteProductGroupInDb,
updateProductPrices as updateProductPricesInDb,
} from '@/src/dbService'
import type {
AdminProductGroupsResult,
AdminProductGroupResponse,
AdminProductReviewsResult,
AdminProductReviewResponse,
AdminProductListResponse,
AdminProductResponse,
AdminDeleteProductResult,
AdminToggleOutOfStockResult,
AdminUpdateSlotProductsResult,
AdminSlotProductIdsResult,
AdminSlotsProductIdsResult,
AdminUpdateProductPricesResult,
} from '@packages/shared'
type CreateDeal = {
quantity: number;
price: number;
validTill: string;
};
export const productRouter = router({
getProducts: protectedProcedure
.query(async ({ ctx }) => {
.query(async (): Promise<AdminProductListResponse> => {
const products = await getAllProductsInDb()
/*
// Old implementation - direct DB query:
const products = await db.query.productInfo.findMany({
orderBy: productInfo.name,
with: {
@ -26,28 +49,32 @@ export const productRouter = router({
store: true,
},
});
*/
// Generate signed URLs for all product images
const productsWithSignedUrls = await Promise.all(
products.map(async (product) => ({
...product,
images: await generateSignedUrlsFromS3Urls((product.images as string[]) || []),
}))
);
)
return {
products: productsWithSignedUrls,
count: productsWithSignedUrls.length,
};
}
}),
getProductById: protectedProcedure
.input(z.object({
id: z.number(),
}))
.query(async ({ input, ctx }) => {
.query(async ({ input }): Promise<AdminProductResponse> => {
const { id } = input;
const product = await getProductByIdInDb(id)
/*
// Old implementation - direct DB queries:
const product = await db.query.productInfo.findFirst({
where: eq(productInfo.id, id),
with: {
@ -84,15 +111,33 @@ export const productRouter = router({
return {
product: productWithSignedUrls,
};
*/
if (!product) {
throw new ApiError('Product not found', 404)
}
const productWithSignedUrls = {
...product,
images: await generateSignedUrlsFromS3Urls((product.images as string[]) || []),
}
return {
product: productWithSignedUrls,
}
}),
deleteProduct: protectedProcedure
.input(z.object({
id: z.number(),
}))
.mutation(async ({ input, ctx }) => {
.mutation(async ({ input }): Promise<AdminDeleteProductResult> => {
const { id } = input;
const deletedProduct = await deleteProductInDb(id)
/*
// Old implementation - direct DB query:
const [deletedProduct] = await db
.delete(productInfo)
.where(eq(productInfo.id, id))
@ -101,22 +146,31 @@ export const productRouter = router({
if (!deletedProduct) {
throw new ApiError("Product not found", 404);
}
*/
if (!deletedProduct) {
throw new ApiError('Product not found', 404)
}
// Reinitialize stores to reflect changes
scheduleStoreInitialization()
return {
message: "Product deleted successfully",
};
message: 'Product deleted successfully',
}
}),
toggleOutOfStock: protectedProcedure
.input(z.object({
id: z.number(),
}))
.mutation(async ({ input, ctx }) => {
.mutation(async ({ input }): Promise<AdminToggleOutOfStockResult> => {
const { id } = input;
const updatedProduct = await toggleProductOutOfStockInDb(id)
/*
// Old implementation - direct DB queries:
const product = await db.query.productInfo.findFirst({
where: eq(productInfo.id, id),
});
@ -132,14 +186,18 @@ export const productRouter = router({
})
.where(eq(productInfo.id, id))
.returning();
*/
if (!updatedProduct) {
throw new ApiError('Product not found', 404)
}
// Reinitialize stores to reflect changes
scheduleStoreInitialization()
return {
product: updatedProduct,
message: `Product marked as ${updatedProduct.isOutOfStock ? 'out of stock' : 'in stock'}`,
};
}
}),
updateSlotProducts: protectedProcedure
@ -147,13 +205,17 @@ export const productRouter = router({
slotId: z.string(),
productIds: z.array(z.string()),
}))
.mutation(async ({ input, ctx }) => {
.mutation(async ({ input }): Promise<AdminUpdateSlotProductsResult> => {
const { slotId, productIds } = input;
if (!Array.isArray(productIds)) {
throw new ApiError("productIds must be an array", 400);
}
const result = await updateSlotProductsInDb(slotId, productIds)
/*
// Old implementation - direct DB queries:
// Get current associations
const currentAssociations = await db.query.productSlots.findMany({
where: eq(productSlots.slotId, parseInt(slotId)),
@ -197,15 +259,28 @@ export const productRouter = router({
added: productsToAdd.length,
removed: productsToRemove.length,
};
*/
scheduleStoreInitialization()
return {
message: 'Slot products updated successfully',
added: result.added,
removed: result.removed,
}
}),
getSlotProductIds: protectedProcedure
.input(z.object({
slotId: z.string(),
}))
.query(async ({ input, ctx }) => {
.query(async ({ input }): Promise<AdminSlotProductIdsResult> => {
const { slotId } = input;
const productIds = await getSlotProductIdsInDb(slotId)
/*
// Old implementation - direct DB queries:
const associations = await db.query.productSlots.findMany({
where: eq(productSlots.slotId, parseInt(slotId)),
columns: {
@ -218,19 +293,28 @@ export const productRouter = router({
return {
productIds,
};
*/
return {
productIds,
}
}),
getSlotsProductIds: protectedProcedure
.input(z.object({
slotIds: z.array(z.number()),
}))
.query(async ({ input, ctx }) => {
.query(async ({ input }): Promise<AdminSlotsProductIdsResult> => {
const { slotIds } = input;
if (!Array.isArray(slotIds)) {
throw new ApiError("slotIds must be an array", 400);
}
const result = await getSlotsProductIdsInDb(slotIds)
/*
// Old implementation - direct DB queries:
if (slotIds.length === 0) {
return {};
}
@ -261,6 +345,9 @@ export const productRouter = router({
});
return result;
*/
return result
}),
getProductReviews: protectedProcedure
@ -269,9 +356,13 @@ export const productRouter = router({
limit: z.number().int().min(1).max(50).optional().default(10),
offset: z.number().int().min(0).optional().default(0),
}))
.query(async ({ input }) => {
.query(async ({ input }): Promise<AdminProductReviewsResult> => {
const { productId, limit, offset } = input;
const { reviews, totalCount } = await getProductReviewsInDb(productId, limit, offset)
/*
// Old implementation - direct DB queries:
const reviews = await db
.select({
id: productReviews.id,
@ -309,6 +400,19 @@ export const productRouter = router({
const hasMore = offset + limit < totalCount;
return { reviews: reviewsWithSignedUrls, hasMore };
*/
const reviewsWithSignedUrls = await Promise.all(
reviews.map(async (review) => ({
...review,
signedImageUrls: await generateSignedUrlsFromS3Urls((review.imageUrls as string[]) || []),
signedAdminImageUrls: await generateSignedUrlsFromS3Urls((review.adminResponseImages as string[]) || []),
}))
)
const hasMore = offset + limit < totalCount
return { reviews: reviewsWithSignedUrls, hasMore }
}),
respondToReview: protectedProcedure
@ -318,9 +422,13 @@ export const productRouter = router({
adminResponseImages: z.array(z.string()).optional().default([]),
uploadUrls: z.array(z.string()).optional().default([]),
}))
.mutation(async ({ input }) => {
.mutation(async ({ input }): Promise<AdminProductReviewResponse> => {
const { reviewId, adminResponse, adminResponseImages, uploadUrls } = input;
const updatedReview = await respondToReviewInDb(reviewId, adminResponse, adminResponseImages)
/*
// Old implementation - direct DB queries:
const [updatedReview] = await db
.update(productReviews)
.set({
@ -341,10 +449,25 @@ export const productRouter = router({
}
return { success: true, review: updatedReview };
*/
if (!updatedReview) {
throw new ApiError('Review not found', 404)
}
if (uploadUrls && uploadUrls.length > 0) {
await Promise.all(uploadUrls.map(url => claimUploadUrl(url)))
}
return { success: true, review: updatedReview }
}),
getGroups: protectedProcedure
.query(async ({ ctx }) => {
.query(async (): Promise<AdminProductGroupsResult> => {
const groups = await getAllProductGroupsInDb()
/*
// Old implementation - direct DB queries:
const groups = await db.query.productGroupInfo.findMany({
with: {
memberships: {
@ -355,14 +478,18 @@ export const productRouter = router({
},
orderBy: desc(productGroupInfo.createdAt),
});
*/
return {
groups: groups.map(group => ({
...group,
products: group.memberships.map(m => m.product),
products: group.memberships.map(m => ({
...m.product,
images: (m.product.images as string[]) || null,
})),
productCount: group.memberships.length,
})),
};
}
}),
createGroup: protectedProcedure
@ -371,9 +498,13 @@ export const productRouter = router({
description: z.string().optional(),
product_ids: z.array(z.number()).default([]),
}))
.mutation(async ({ input, ctx }) => {
.mutation(async ({ input }): Promise<AdminProductGroupResponse> => {
const { group_name, description, product_ids } = input;
const newGroup = await createProductGroupInDb(group_name, description, product_ids)
/*
// Old implementation - direct DB queries:
const [newGroup] = await db
.insert(productGroupInfo)
.values({
@ -398,6 +529,14 @@ export const productRouter = router({
group: newGroup,
message: 'Group created successfully',
};
*/
scheduleStoreInitialization()
return {
group: newGroup,
message: 'Group created successfully',
}
}),
updateGroup: protectedProcedure
@ -407,9 +546,13 @@ export const productRouter = router({
description: z.string().optional(),
product_ids: z.array(z.number()).optional(),
}))
.mutation(async ({ input, ctx }) => {
.mutation(async ({ input }): Promise<AdminProductGroupResponse> => {
const { id, group_name, description, product_ids } = input;
const updatedGroup = await updateProductGroupInDb(id, group_name, description, product_ids)
/*
// Old implementation - direct DB queries:
const updateData: any = {};
if (group_name !== undefined) updateData.groupName = group_name;
if (description !== undefined) updateData.description = description;
@ -446,15 +589,31 @@ export const productRouter = router({
group: updatedGroup,
message: 'Group updated successfully',
};
*/
if (!updatedGroup) {
throw new ApiError('Group not found', 404)
}
scheduleStoreInitialization()
return {
group: updatedGroup,
message: 'Group updated successfully',
}
}),
deleteGroup: protectedProcedure
.input(z.object({
id: z.number(),
}))
.mutation(async ({ input, ctx }) => {
.mutation(async ({ input }): Promise<AdminDeleteProductResult> => {
const { id } = input;
const deletedGroup = await deleteProductGroupInDb(id)
/*
// Old implementation - direct DB queries:
// Delete memberships first
await db.delete(productGroupMembership).where(eq(productGroupMembership.groupId, id));
@ -474,6 +633,17 @@ export const productRouter = router({
return {
message: 'Group deleted successfully',
};
*/
if (!deletedGroup) {
throw new ApiError('Group not found', 404)
}
scheduleStoreInitialization()
return {
message: 'Group deleted successfully',
}
}),
updateProductPrices: protectedProcedure
@ -486,9 +656,17 @@ export const productRouter = router({
isFlashAvailable: z.boolean().optional(),
})),
}))
.mutation(async ({ input, ctx }) => {
const { updates } = input;
.mutation(async ({ input }): Promise<AdminUpdateProductPricesResult> => {
const { updates } = input;
if (updates.length === 0) {
throw new ApiError('No updates provided', 400)
}
const result = await updateProductPricesInDb(updates)
/*
// Old implementation - direct DB queries:
if (updates.length === 0) {
throw new ApiError('No updates provided', 400);
}
@ -531,5 +709,17 @@ export const productRouter = router({
message: `Updated prices for ${updates.length} product(s)`,
updatedCount: updates.length,
};
}),
*/
if (result.invalidIds.length > 0) {
throw new ApiError(`Invalid product IDs: ${result.invalidIds.join(', ')}`, 400)
}
scheduleStoreInitialization()
return {
message: `Updated prices for ${result.updatedCount} product(s)`,
updatedCount: result.updatedCount,
}
}),
});

View file

@ -1,14 +1,38 @@
import { router, protectedProcedure } from "@/src/trpc/trpc-index"
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { db } from "@/src/db/db_index"
import { deliverySlotInfo, productSlots, productInfo, vendorSnippets, productGroupInfo } from "@/src/db/schema"
import { eq, inArray, and, desc } from "drizzle-orm";
import { ApiError } from "@/src/lib/api-error"
import { appUrl } from "@/src/lib/env-exporter"
import redisClient from "@/src/lib/redis-client"
import { getSlotSequenceKey } from "@/src/lib/redisKeyGetters"
import { scheduleStoreInitialization } from '@/src/stores/store-initializer'
import {
getActiveSlotsWithProducts as getActiveSlotsWithProductsInDb,
getActiveSlots as getActiveSlotsInDb,
getSlotsAfterDate as getSlotsAfterDateInDb,
getSlotByIdWithRelations as getSlotByIdWithRelationsInDb,
createSlotWithRelations as createSlotWithRelationsInDb,
updateSlotWithRelations as updateSlotWithRelationsInDb,
deleteSlotById as deleteSlotByIdInDb,
updateSlotCapacity as updateSlotCapacityInDb,
getSlotDeliverySequence as getSlotDeliverySequenceInDb,
updateSlotDeliverySequence as updateSlotDeliverySequenceInDb,
updateSlotProducts as updateSlotProductsInDb,
getSlotsProductIds as getSlotsProductIdsInDb,
} from '@/src/dbService'
import type {
AdminDeliverySequenceResult,
AdminSlotResult,
AdminSlotsResult,
AdminSlotsListResult,
AdminSlotCreateResult,
AdminSlotUpdateResult,
AdminSlotDeleteResult,
AdminUpdateDeliverySequenceResult,
AdminUpdateSlotCapacityResult,
AdminSlotsProductIdsResult,
AdminUpdateSlotProductsResult,
} from '@packages/shared'
interface CachedDeliverySequence {
@ -64,11 +88,15 @@ const updateDeliverySequenceSchema = z.object({
export const slotsRouter = router({
// Exact replica of GET /av/slots
getAll: protectedProcedure.query(async ({ ctx }) => {
getAll: protectedProcedure.query(async ({ ctx }): Promise<AdminSlotsResult> => {
if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
}
const slots = await getActiveSlotsWithProductsInDb()
/*
// Old implementation - direct DB queries:
const slots = await db.query.deliverySlotInfo
.findMany({
where: eq(deliverySlotInfo.isActive, true),
@ -94,17 +122,18 @@ export const slotsRouter = router({
products: slot.productSlots.map((ps) => ps.product),
}))
);
*/
return {
slots,
count: slots.length,
};
}
}),
// Exact replica of POST /av/products/slots/product-ids
getSlotsProductIds: protectedProcedure
.input(z.object({ slotIds: z.array(z.number()) }))
.query(async ({ input, ctx }) => {
.query(async ({ input, ctx }): Promise<AdminSlotsProductIdsResult> => {
if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
}
@ -118,6 +147,10 @@ export const slotsRouter = router({
});
}
const result = await getSlotsProductIdsInDb(slotIds)
/*
// Old implementation - direct DB queries:
if (slotIds.length === 0) {
return {};
}
@ -148,6 +181,9 @@ export const slotsRouter = router({
});
return result;
*/
return result
}),
// Exact replica of PUT /av/products/slots/:slotId/products
@ -158,7 +194,7 @@ export const slotsRouter = router({
productIds: z.array(z.number()),
})
)
.mutation(async ({ input, ctx }) => {
.mutation(async ({ input, ctx }): Promise<AdminUpdateSlotProductsResult> => {
if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
}
@ -172,6 +208,10 @@ export const slotsRouter = router({
});
}
const result = await updateSlotProductsInDb(String(slotId), productIds.map(String))
/*
// Old implementation - direct DB queries:
// Get current associations
const currentAssociations = await db.query.productSlots.findMany({
where: eq(productSlots.slotId, slotId),
@ -223,11 +263,20 @@ export const slotsRouter = router({
added: productsToAdd.length,
removed: productsToRemove.length,
};
*/
scheduleStoreInitialization()
return {
message: result.message,
added: result.added,
removed: result.removed,
}
}),
createSlot: protectedProcedure
.input(createSlotSchema)
.mutation(async ({ input, ctx }) => {
.mutation(async ({ input, ctx }): Promise<AdminSlotCreateResult> => {
if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
}
@ -239,6 +288,17 @@ export const slotsRouter = router({
throw new ApiError("Delivery time and orders close time are required", 400);
}
const result = await createSlotWithRelationsInDb({
deliveryTime,
freezeTime,
isActive,
productIds,
vendorSnippets: snippets,
groupIds,
})
/*
// Old implementation - direct DB queries:
const result = await db.transaction(async (tx) => {
// Create slot
const [newSlot] = await tx
@ -297,76 +357,84 @@ export const slotsRouter = router({
message: "Slot created successfully",
};
});
*/
// Reinitialize stores to reflect changes (outside transaction)
scheduleStoreInitialization()
return result;
return result
}),
getSlots: protectedProcedure.query(async ({ ctx }) => {
getSlots: protectedProcedure.query(async ({ ctx }): Promise<AdminSlotsListResult> => {
if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
}
const slots = await getActiveSlotsInDb()
/*
// Old implementation - direct DB queries:
const slots = await db.query.deliverySlotInfo.findMany({
where: eq(deliverySlotInfo.isActive, true),
});
*/
return {
slots,
count: slots.length,
};
}
}),
getSlotById: protectedProcedure
.input(getSlotByIdSchema)
.query(async ({ input, ctx }) => {
.query(async ({ input, ctx }): Promise<AdminSlotResult> => {
if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
}
const { id } = input;
const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, id),
with: {
productSlots: {
with: {
product: {
columns: {
id: true,
name: true,
images: true,
},
},
},
},
vendorSnippets: true,
},
});
const slot = await getSlotByIdWithRelationsInDb(id)
/*
// Old implementation - direct DB queries:
const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, id),
with: {
productSlots: {
with: {
product: {
columns: {
id: true,
name: true,
images: true,
},
},
},
},
vendorSnippets: true,
},
});
*/
if (!slot) {
throw new ApiError("Slot not found", 404);
throw new ApiError('Slot not found', 404)
}
return {
slot: {
...slot,
deliverySequence: slot.deliverySequence as number[],
groupIds: slot.groupIds as number[],
products: slot.productSlots.map((ps) => ps.product),
vendorSnippets: slot.vendorSnippets?.map(snippet => ({
vendorSnippets: slot.vendorSnippets.map(snippet => ({
...snippet,
accessUrl: `${appUrl}/vendor-order-list?id=${snippet.snippetCode}`
accessUrl: `${appUrl}/vendor-order-list?id=${snippet.snippetCode}`,
})),
},
};
}
}),
updateSlot: protectedProcedure
.input(updateSlotSchema)
.mutation(async ({ input, ctx }) => {
.mutation(async ({ input, ctx }): Promise<AdminSlotUpdateResult> => {
if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
}
@ -377,6 +445,18 @@ export const slotsRouter = router({
throw new ApiError("Delivery time and orders close time are required", 400);
}
const result = await updateSlotWithRelationsInDb({
id,
deliveryTime,
freezeTime,
isActive,
productIds,
vendorSnippets: snippets,
groupIds,
})
/*
// Old implementation - direct DB queries:
// Filter groupIds to only include valid (existing) groups
let validGroupIds = groupIds;
if (groupIds && groupIds.length > 0) {
@ -456,11 +536,16 @@ export const slotsRouter = router({
message: "Slot updated successfully",
};
});
*/
if (!result) {
throw new ApiError('Slot not found', 404)
}
// Reinitialize stores to reflect changes (outside transaction)
scheduleStoreInitialization()
return result;
return result
}
catch(e) {
console.log(e)
@ -470,13 +555,17 @@ export const slotsRouter = router({
deleteSlot: protectedProcedure
.input(deleteSlotSchema)
.mutation(async ({ input, ctx }) => {
.mutation(async ({ input, ctx }): Promise<AdminSlotDeleteResult> => {
if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
}
const { id } = input;
const deletedSlot = await deleteSlotByIdInDb(id)
/*
// Old implementation - direct DB queries:
const [deletedSlot] = await db
.update(deliverySlotInfo)
.set({ isActive: false })
@ -486,18 +575,23 @@ export const slotsRouter = router({
if (!deletedSlot) {
throw new ApiError("Slot not found", 404);
}
*/
if (!deletedSlot) {
throw new ApiError('Slot not found', 404)
}
// Reinitialize stores to reflect changes
scheduleStoreInitialization()
return {
message: "Slot deleted successfully",
};
message: 'Slot deleted successfully',
}
}),
getDeliverySequence: protectedProcedure
.input(getDeliverySequenceSchema)
.query(async ({ input, ctx }) => {
.query(async ({ input, ctx }): Promise<AdminDeliverySequenceResult> => {
const { id } = input;
const slotId = parseInt(id);
@ -507,7 +601,7 @@ export const slotsRouter = router({
const cached = await redisClient.get(cacheKey);
if (cached) {
const parsed = JSON.parse(cached);
const validated = cachedSequenceSchema.parse(parsed) as CachedDeliverySequence;
const validated = cachedSequenceSchema.parse(parsed);
console.log('sending cached response')
return { deliverySequence: validated };
@ -518,6 +612,10 @@ export const slotsRouter = router({
}
// Fallback to DB
const slot = await getSlotDeliverySequenceInDb(slotId)
/*
// Old implementation - direct DB queries:
const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, slotId),
});
@ -526,6 +624,13 @@ export const slotsRouter = router({
throw new ApiError("Slot not found", 404);
}
const sequence = cachedSequenceSchema.parse(slot.deliverySequence || {});
*/
if (!slot) {
throw new ApiError('Slot not found', 404)
}
const sequence = (slot.deliverySequence || {}) as CachedDeliverySequence;
// Cache the validated result
@ -536,18 +641,22 @@ export const slotsRouter = router({
console.warn('Redis cache write failed:', cacheError);
}
return { deliverySequence: sequence };
return { deliverySequence: sequence }
}),
updateDeliverySequence: protectedProcedure
.input(updateDeliverySequenceSchema)
.mutation(async ({ input, ctx }) => {
.mutation(async ({ input, ctx }): Promise<AdminUpdateDeliverySequenceResult> => {
if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
}
const { id, deliverySequence } = input;
const updatedSlot = await updateSlotDeliverySequenceInDb(id, deliverySequence)
/*
// Old implementation - direct DB queries:
const [updatedSlot] = await db
.update(deliverySlotInfo)
.set({ deliverySequence })
@ -560,6 +669,11 @@ export const slotsRouter = router({
if (!updatedSlot) {
throw new ApiError("Slot not found", 404);
}
*/
if (!updatedSlot) {
throw new ApiError('Slot not found', 404)
}
// Cache the updated sequence
const cacheKey = getSlotSequenceKey(id);
@ -572,8 +686,8 @@ export const slotsRouter = router({
return {
slot: updatedSlot,
message: "Delivery sequence updated successfully",
};
message: 'Delivery sequence updated successfully',
}
}),
updateSlotCapacity: protectedProcedure
@ -581,13 +695,17 @@ export const slotsRouter = router({
slotId: z.number(),
isCapacityFull: z.boolean(),
}))
.mutation(async ({ input, ctx }) => {
.mutation(async ({ input, ctx }): Promise<AdminUpdateSlotCapacityResult> => {
if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
}
const { slotId, isCapacityFull } = input;
const result = await updateSlotCapacityInDb(slotId, isCapacityFull)
/*
// Old implementation - direct DB queries:
const [updatedSlot] = await db
.update(deliverySlotInfo)
.set({ isCapacityFull })
@ -606,5 +724,14 @@ export const slotsRouter = router({
slot: updatedSlot,
message: `Slot ${isCapacityFull ? 'marked as full capacity' : 'capacity reset'}`,
};
*/
if (!result) {
throw new ApiError('Slot not found', 404)
}
scheduleStoreInitialization()
return result
}),
});

View file

@ -8,7 +8,7 @@ import {
getAllStaff,
getAllUsers,
getUserWithDetails,
updateUserSuspension,
upsertUserSuspension,
checkStaffUserExists,
checkStaffRoleExists,
createStaffUser,
@ -130,7 +130,7 @@ export const staffUserRouter = router({
.mutation(async ({ input }) => {
const { userId, isSuspended } = input;
await updateUserSuspension(userId, isSuspended);
await upsertUserSuspension(userId, isSuspended);
return { success: true };
}),

View file

@ -1,10 +1,33 @@
import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod';
import dayjs from 'dayjs';
import { db } from '@/src/db/db_index'
import { vendorSnippets, deliverySlotInfo, productInfo, orders, orderItems, users, orderStatus } from '@/src/db/schema'
import { eq, and, inArray, isNotNull, gt, sql, asc, ne } from 'drizzle-orm';
import { z } from 'zod'
import dayjs from 'dayjs'
import { appUrl } from '@/src/lib/env-exporter'
import {
checkVendorSnippetExists as checkVendorSnippetExistsInDb,
getVendorSnippetById as getVendorSnippetByIdInDb,
getVendorSnippetByCode as getVendorSnippetByCodeInDb,
getAllVendorSnippets as getAllVendorSnippetsInDb,
createVendorSnippet as createVendorSnippetInDb,
updateVendorSnippet as updateVendorSnippetInDb,
deleteVendorSnippet as deleteVendorSnippetInDb,
getProductsByIds as getProductsByIdsInDb,
getVendorSlotById as getVendorSlotByIdInDb,
getVendorOrdersBySlotId as getVendorOrdersBySlotIdInDb,
getVendorOrders as getVendorOrdersInDb,
updateVendorOrderItemPackaging as updateVendorOrderItemPackagingInDb,
getSlotsAfterDate as getSlotsAfterDateInDb,
} from '@/src/dbService'
import type {
AdminVendorSnippet,
AdminVendorSnippetWithProducts,
AdminVendorSnippetWithSlot,
AdminVendorSnippetDeleteResult,
AdminVendorSnippetOrdersResult,
AdminVendorSnippetOrdersWithSlotResult,
AdminVendorOrderSummary,
AdminUpcomingSlotsResult,
AdminVendorUpdatePackagingResult,
} from '@packages/shared'
const createSnippetSchema = z.object({
snippetCode: z.string().min(1, "Snippet code is required"),
@ -26,7 +49,7 @@ const updateSnippetSchema = z.object({
export const vendorSnippetsRouter = router({
create: protectedProcedure
.input(createSnippetSchema)
.mutation(async ({ input, ctx }) => {
.mutation(async ({ input, ctx }): Promise<AdminVendorSnippet> => {
const { snippetCode, slotId, productIds, validTill, isPermanent } = input;
// Get staff user ID from auth middleware
@ -35,6 +58,33 @@ export const vendorSnippetsRouter = router({
throw new Error("Unauthorized");
}
if(slotId) {
const slot = await getVendorSlotByIdInDb(slotId)
if (!slot) {
throw new Error("Invalid slot ID")
}
}
const products = await getProductsByIdsInDb(productIds)
if (products.length !== productIds.length) {
throw new Error("One or more invalid product IDs")
}
const existingSnippet = await checkVendorSnippetExistsInDb(snippetCode)
if (existingSnippet) {
throw new Error("Snippet code already exists")
}
const result = await createVendorSnippetInDb({
snippetCode,
slotId,
productIds,
isPermanent,
validTill: validTill ? new Date(validTill) : undefined,
})
/*
// Old implementation - direct DB queries:
// Validate slot exists
if(slotId) {
const slot = await db.query.deliverySlotInfo.findFirst({
@ -70,13 +120,32 @@ export const vendorSnippetsRouter = router({
}).returning();
return result[0];
*/
return result
}),
getAll: protectedProcedure
.query(async () => {
.query(async (): Promise<AdminVendorSnippetWithProducts[]> => {
console.log('from the vendor snipptes methods')
try {
const result = await getAllVendorSnippetsInDb()
const snippetsWithProducts = await Promise.all(
result.map(async (snippet) => {
const products = await getProductsByIdsInDb(snippet.productIds)
return {
...snippet,
accessUrl: `${appUrl}/vendor-order-list?id=${snippet.snippetCode}`,
products,
}
})
)
/*
// Old implementation - direct DB queries:
const result = await db.query.vendorSnippets.findMany({
with: {
slot: true,
@ -100,18 +169,25 @@ export const vendorSnippetsRouter = router({
);
return snippetsWithProducts;
*/
return snippetsWithProducts
}
catch(e) {
console.log(e)
}
return [];
return []
}),
getById: protectedProcedure
.input(z.object({ id: z.number().int().positive() }))
.query(async ({ input }) => {
.query(async ({ input }): Promise<AdminVendorSnippetWithSlot> => {
const { id } = input;
const result = await getVendorSnippetByIdInDb(id)
/*
// Old implementation - direct DB queries:
const result = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.id, id),
with: {
@ -124,14 +200,57 @@ export const vendorSnippetsRouter = router({
}
return result;
*/
if (!result) {
throw new Error('Vendor snippet not found')
}
return result
}),
update: protectedProcedure
.input(updateSnippetSchema)
.mutation(async ({ input }) => {
.mutation(async ({ input }): Promise<AdminVendorSnippet> => {
const { id, updates } = input;
// Check if snippet exists
const existingSnippet = await getVendorSnippetByIdInDb(id)
if (!existingSnippet) {
throw new Error('Vendor snippet not found')
}
if (updates.slotId) {
const slot = await getVendorSlotByIdInDb(updates.slotId)
if (!slot) {
throw new Error('Invalid slot ID')
}
}
if (updates.productIds) {
const products = await getProductsByIdsInDb(updates.productIds)
if (products.length !== updates.productIds.length) {
throw new Error('One or more invalid product IDs')
}
}
if (updates.snippetCode && updates.snippetCode !== existingSnippet.snippetCode) {
const duplicateSnippet = await checkVendorSnippetExistsInDb(updates.snippetCode)
if (duplicateSnippet) {
throw new Error('Snippet code already exists')
}
}
const updateData = {
...updates,
validTill: updates.validTill !== undefined
? (updates.validTill ? new Date(updates.validTill) : null)
: undefined,
}
const result = await updateVendorSnippetInDb(id, updateData)
/*
// Old implementation - direct DB queries:
const existingSnippet = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.id, id),
});
@ -184,13 +303,24 @@ export const vendorSnippetsRouter = router({
}
return result[0];
*/
if (!result) {
throw new Error('Failed to update vendor snippet')
}
return result
}),
delete: protectedProcedure
.input(z.object({ id: z.number().int().positive() }))
.mutation(async ({ input }) => {
.mutation(async ({ input }): Promise<AdminVendorSnippetDeleteResult> => {
const { id } = input;
const result = await deleteVendorSnippetInDb(id)
/*
// Old implementation - direct DB queries:
const result = await db.delete(vendorSnippets)
.where(eq(vendorSnippets.id, id))
.returning();
@ -200,15 +330,26 @@ export const vendorSnippetsRouter = router({
}
return { message: "Vendor snippet deleted successfully" };
*/
if (!result) {
throw new Error('Vendor snippet not found')
}
return { message: 'Vendor snippet deleted successfully' }
}),
getOrdersBySnippet: publicProcedure
.input(z.object({
snippetCode: z.string().min(1, "Snippet code is required")
}))
.query(async ({ input }) => {
.query(async ({ input }): Promise<AdminVendorSnippetOrdersResult> => {
const { snippetCode } = input;
const snippet = await getVendorSnippetByCodeInDb(snippetCode)
/*
// Old implementation - direct DB queries:
// Find the snippet
const snippet = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.snippetCode, snippetCode),
@ -242,6 +383,21 @@ export const vendorSnippetsRouter = router({
},
orderBy: (orders, { desc }) => [desc(orders.createdAt)],
});
*/
if (!snippet) {
throw new Error('Vendor snippet not found')
}
if (snippet.validTill && new Date(snippet.validTill) < new Date()) {
throw new Error('Vendor snippet has expired')
}
if (!snippet.slotId) {
throw new Error('Vendor snippet not associated with a slot')
}
const matchingOrders = await getVendorOrdersBySlotIdInDb(snippet.slotId)
// Filter orders that contain at least one of the snippet's products
const filteredOrders = matchingOrders.filter(order => {
@ -273,11 +429,11 @@ export const vendorSnippetsRouter = router({
const orderTotal = products.reduce((sum, p) => sum + p.subtotal, 0);
return {
orderId: `ORD${order.id}`,
orderDate: order.createdAt.toISOString(),
customerName: order.user.name,
totalAmount: orderTotal,
return {
orderId: `ORD${order.id}`,
orderDate: order.createdAt.toISOString(),
customerName: order.user.name || '',
totalAmount: orderTotal,
slotInfo: order.slot ? {
time: order.slot.deliveryTime.toISOString(),
sequence: order.slot.deliverySequence,
@ -300,11 +456,15 @@ export const vendorSnippetsRouter = router({
createdAt: snippet.createdAt.toISOString(),
isPermanent: snippet.isPermanent,
},
};
}
}),
getVendorOrders: protectedProcedure
.query(async () => {
.query(async (): Promise<AdminVendorOrderSummary[]> => {
const vendorOrders = await getVendorOrdersInDb()
/*
// Old implementation - direct DB queries:
const vendorOrders = await db.query.orders.findMany({
with: {
user: true,
@ -320,10 +480,11 @@ export const vendorSnippetsRouter = router({
},
orderBy: (orders, { desc }) => [desc(orders.createdAt)],
});
*/
return vendorOrders.map(order => ({
id: order.id,
status: 'pending', // Default status since orders table may not have status field
status: 'pending',
orderDate: order.createdAt.toISOString(),
totalQuantity: order.orderItems.reduce((sum, item) => sum + parseFloat(item.quantity || '0'), 0),
products: order.orderItems.map(item => ({
@ -331,12 +492,16 @@ export const vendorSnippetsRouter = router({
quantity: parseFloat(item.quantity || '0'),
unit: item.product.unit?.shortNotation || 'unit',
})),
}));
}))
}),
getUpcomingSlots: publicProcedure
.query(async () => {
.query(async (): Promise<AdminUpcomingSlotsResult> => {
const threeHoursAgo = dayjs().subtract(3, 'hour').toDate();
const slots = await getSlotsAfterDateInDb(threeHoursAgo)
/*
// Old implementation - direct DB queries:
const slots = await db.query.deliverySlotInfo.findMany({
where: and(
eq(deliverySlotInfo.isActive, true),
@ -344,6 +509,7 @@ export const vendorSnippetsRouter = router({
),
orderBy: asc(deliverySlotInfo.deliveryTime),
});
*/
return {
success: true,
@ -353,7 +519,7 @@ export const vendorSnippetsRouter = router({
freezeTime: slot.freezeTime.toISOString(),
deliverySequence: slot.deliverySequence,
})),
};
}
}),
getOrdersBySnippetAndSlot: publicProcedure
@ -361,9 +527,14 @@ export const vendorSnippetsRouter = router({
snippetCode: z.string().min(1, "Snippet code is required"),
slotId: z.number().int().positive("Valid slot ID is required"),
}))
.query(async ({ input }) => {
.query(async ({ input }): Promise<AdminVendorSnippetOrdersWithSlotResult> => {
const { snippetCode, slotId } = input;
const snippet = await getVendorSnippetByCodeInDb(snippetCode)
const slot = await getVendorSlotByIdInDb(slotId)
/*
// Old implementation - direct DB queries:
// Find the snippet
const snippet = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.snippetCode, snippetCode),
@ -401,6 +572,17 @@ export const vendorSnippetsRouter = router({
},
orderBy: (orders, { desc }) => [desc(orders.createdAt)],
});
*/
if (!snippet) {
throw new Error('Vendor snippet not found')
}
if (!slot) {
throw new Error('Slot not found')
}
const matchingOrders = await getVendorOrdersBySlotIdInDb(slotId)
// Filter orders that contain at least one of the snippet's products
const filteredOrders = matchingOrders.filter(order => {
@ -435,7 +617,7 @@ export const vendorSnippetsRouter = router({
return {
orderId: `ORD${order.id}`,
orderDate: order.createdAt.toISOString(),
customerName: order.user.name,
customerName: order.user.name || '',
totalAmount: orderTotal,
slotInfo: order.slot ? {
time: order.slot.deliveryTime.toISOString(),
@ -465,7 +647,7 @@ export const vendorSnippetsRouter = router({
freezeTime: slot.freezeTime.toISOString(),
deliverySequence: slot.deliverySequence,
},
};
}
}),
updateOrderItemPackaging: publicProcedure
@ -473,7 +655,7 @@ export const vendorSnippetsRouter = router({
orderItemId: z.number().int().positive("Valid order item ID required"),
is_packaged: z.boolean()
}))
.mutation(async ({ input, ctx }) => {
.mutation(async ({ input, ctx }): Promise<AdminVendorUpdatePackagingResult> => {
const { orderItemId, is_packaged } = input;
// Get staff user ID from auth middleware
@ -482,6 +664,10 @@ export const vendorSnippetsRouter = router({
// throw new Error("Unauthorized");
// }
const result = await updateVendorOrderItemPackagingInDb(orderItemId, is_packaged)
/*
// Old implementation - direct DB queries:
// Check if order item exists and get related data
const orderItem = await db.query.orderItems.findFirst({
where: eq(orderItems.id, orderItemId),
@ -527,5 +713,12 @@ export const vendorSnippetsRouter = router({
orderItemId,
is_packaged
};
*/
if (!result.success) {
throw new Error(result.message)
}
return result
}),
});
});

View file

@ -7,15 +7,165 @@ export { db } from './src/db/db_index';
// Re-export schema
export * from './src/db/schema';
// Re-export helper methods
export * from './src/helper_methods/banner';
export * from './src/helper_methods/complaint';
export * from './src/helper_methods/const';
export * from './src/helper_methods/coupon';
export * from './src/helper_methods/store';
export * from './src/helper_methods/staff-user';
export * from './src/helper_methods/user';
export * from './src/helper_methods/vendor-snippets';
export * from './src/helper_methods/product';
export * from './src/helper_methods/slots';
export * from './src/helper_methods/order';
// Admin API helpers - explicitly namespaced exports to avoid duplicates
export {
// Banner
getBanners,
getBannerById,
createBanner,
updateBanner,
deleteBanner,
} from './src/admin-apis/banner';
export {
// Complaint
getComplaints,
resolveComplaint,
} from './src/admin-apis/complaint';
export {
// Constants
getAllConstants,
upsertConstants,
} from './src/admin-apis/const';
export {
// Coupon
getAllCoupons,
getCouponById,
invalidateCoupon,
validateCoupon,
getReservedCoupons,
getUsersForCoupon,
createCouponWithRelations,
updateCouponWithRelations,
generateCancellationCoupon,
createReservedCouponWithProducts,
createCouponForUser,
checkUsersExist,
checkCouponExists,
checkReservedCouponExists,
getOrderWithUser,
} from './src/admin-apis/coupon';
export {
// Order
updateOrderNotes,
getOrderDetails,
updateOrderPackaged,
updateOrderDelivered,
updateOrderItemPackaging,
removeDeliveryCharge,
getSlotOrders,
updateAddressCoords,
getAllOrders,
rebalanceSlots,
cancelOrder,
deleteOrderById,
} from './src/admin-apis/order';
export {
// Product
getAllProducts,
getProductById,
deleteProduct,
createProduct,
updateProduct,
toggleProductOutOfStock,
updateSlotProducts,
getSlotProductIds,
getSlotsProductIds,
getAllUnits,
getAllProductTags,
getProductReviews,
respondToReview,
getAllProductGroups,
createProductGroup,
updateProductGroup,
deleteProductGroup,
addProductToGroup,
removeProductFromGroup,
updateProductPrices,
} from './src/admin-apis/product';
export {
// Slots
getActiveSlotsWithProducts,
getActiveSlots,
getSlotsAfterDate,
getSlotByIdWithRelations,
createSlotWithRelations,
updateSlotWithRelations,
deleteSlotById,
updateSlotCapacity,
getSlotDeliverySequence,
updateSlotDeliverySequence,
} from './src/admin-apis/slots';
export {
// Staff User
getStaffUserByName,
getAllStaff,
getAllUsers,
getUserWithDetails,
updateUserSuspensionStatus,
checkStaffUserExists,
checkStaffRoleExists,
createStaffUser,
getAllRoles,
} from './src/admin-apis/staff-user';
export {
// Store
getAllStores,
getStoreById,
createStore,
updateStore,
deleteStore,
} from './src/admin-apis/store';
export {
// User
createUserByMobile,
getUserByMobile,
getUnresolvedComplaintsCount,
getAllUsersWithFilters,
getOrderCountsByUserIds,
getLastOrdersByUserIds,
getSuspensionStatusesByUserIds,
getUserBasicInfo,
getUserSuspensionStatus,
getUserOrders,
getOrderStatusesByOrderIds,
getItemCountsByOrderIds,
upsertUserSuspension,
searchUsers,
getAllNotifCreds,
getAllUnloggedTokens,
getNotifTokensByUserIds,
getUserIncidentsWithRelations,
createUserIncident,
} from './src/admin-apis/user';
export {
// Vendor Snippets
checkVendorSnippetExists,
getVendorSnippetById,
getVendorSnippetByCode,
getAllVendorSnippets,
createVendorSnippet,
updateVendorSnippet,
deleteVendorSnippet,
getProductsByIds,
getVendorSlotById,
getVendorOrdersBySlotId,
getOrderItemsByOrderIds,
getOrderStatusByOrderIds,
updateVendorOrderItemPackaging,
getVendorOrders,
} from './src/admin-apis/vendor-snippets';
// Note: User API helpers are available in their respective files
// but not exported from main index to avoid naming conflicts
// Import them directly from the file paths if needed:
// import { getAllProducts } from '@packages/db_helper_postgres/src/user-apis/product'

View file

@ -0,0 +1,112 @@
import { db } from '../db/db_index';
import { homeBanners } from '../db/schema';
import { eq, desc } from 'drizzle-orm';
export interface Banner {
id: number;
name: string;
imageUrl: string;
description: string | null;
productIds: number[] | null;
redirectUrl: string | null;
serialNum: number | null;
isActive: boolean;
createdAt: Date;
lastUpdated: Date;
}
export async function getBanners(): Promise<Banner[]> {
const banners = await db.query.homeBanners.findMany({
orderBy: desc(homeBanners.createdAt),
});
return banners.map((banner) => ({
id: banner.id,
name: banner.name,
imageUrl: banner.imageUrl,
description: banner.description,
productIds: banner.productIds || [],
redirectUrl: banner.redirectUrl,
serialNum: banner.serialNum,
isActive: banner.isActive,
createdAt: banner.createdAt,
lastUpdated: banner.lastUpdated,
}));
}
export async function getBannerById(id: number): Promise<Banner | null> {
const banner = await db.query.homeBanners.findFirst({
where: eq(homeBanners.id, id),
});
if (!banner) return null;
return {
id: banner.id,
name: banner.name,
imageUrl: banner.imageUrl,
description: banner.description,
productIds: banner.productIds || [],
redirectUrl: banner.redirectUrl,
serialNum: banner.serialNum,
isActive: banner.isActive,
createdAt: banner.createdAt,
lastUpdated: banner.lastUpdated,
};
}
export type CreateBannerInput = Omit<Banner, 'id' | 'createdAt' | 'lastUpdated'>;
export async function createBanner(input: CreateBannerInput): Promise<Banner> {
const [banner] = await db.insert(homeBanners).values({
name: input.name,
imageUrl: input.imageUrl,
description: input.description,
productIds: input.productIds || [],
redirectUrl: input.redirectUrl,
serialNum: input.serialNum,
isActive: input.isActive,
}).returning();
return {
id: banner.id,
name: banner.name,
imageUrl: banner.imageUrl,
description: banner.description,
productIds: banner.productIds || [],
redirectUrl: banner.redirectUrl,
serialNum: banner.serialNum,
isActive: banner.isActive,
createdAt: banner.createdAt,
lastUpdated: banner.lastUpdated,
};
}
export type UpdateBannerInput = Partial<Omit<Banner, 'id' | 'createdAt'>>;
export async function updateBanner(id: number, input: UpdateBannerInput): Promise<Banner> {
const [banner] = await db.update(homeBanners)
.set({
...input,
lastUpdated: new Date(),
})
.where(eq(homeBanners.id, id))
.returning();
return {
id: banner.id,
name: banner.name,
imageUrl: banner.imageUrl,
description: banner.description,
productIds: banner.productIds || [],
redirectUrl: banner.redirectUrl,
serialNum: banner.serialNum,
isActive: banner.isActive,
createdAt: banner.createdAt,
lastUpdated: banner.lastUpdated,
};
}
export async function deleteBanner(id: number): Promise<void> {
await db.delete(homeBanners).where(eq(homeBanners.id, id));
}

View file

@ -0,0 +1,74 @@
import { db } from '../db/db_index';
import { complaints, users } from '../db/schema';
import { eq, desc, lt } from 'drizzle-orm';
export interface Complaint {
id: number;
complaintBody: string;
userId: number;
orderId: number | null;
isResolved: boolean;
response: string | null;
createdAt: Date;
images: string[] | null;
}
export interface ComplaintWithUser extends Complaint {
userName: string | null;
userMobile: string | null;
}
export async function getComplaints(
cursor?: number,
limit: number = 20
): Promise<{ complaints: ComplaintWithUser[]; hasMore: boolean }> {
let whereCondition = cursor ? lt(complaints.id, cursor) : undefined;
const complaintsData = await db
.select({
id: complaints.id,
complaintBody: complaints.complaintBody,
userId: complaints.userId,
orderId: complaints.orderId,
isResolved: complaints.isResolved,
response: complaints.response,
createdAt: complaints.createdAt,
images: complaints.images,
userName: users.name,
userMobile: users.mobile,
})
.from(complaints)
.leftJoin(users, eq(complaints.userId, users.id))
.where(whereCondition)
.orderBy(desc(complaints.id))
.limit(limit + 1);
const hasMore = complaintsData.length > limit;
const complaintsToReturn = hasMore ? complaintsData.slice(0, limit) : complaintsData;
return {
complaints: complaintsToReturn.map((c) => ({
id: c.id,
complaintBody: c.complaintBody,
userId: c.userId,
orderId: c.orderId,
isResolved: c.isResolved,
response: c.response,
createdAt: c.createdAt,
images: c.images as string[],
userName: c.userName,
userMobile: c.userMobile,
})),
hasMore,
};
}
export async function resolveComplaint(
id: number,
response?: string
): Promise<void> {
await db
.update(complaints)
.set({ isResolved: true, response })
.where(eq(complaints.id, id));
}

View file

@ -0,0 +1,29 @@
import { db } from '../db/db_index';
import { keyValStore } from '../db/schema';
export interface Constant {
key: string;
value: any;
}
export async function getAllConstants(): Promise<Constant[]> {
const constants = await db.select().from(keyValStore);
return constants.map(c => ({
key: c.key,
value: c.value,
}));
}
export async function upsertConstants(constants: Constant[]): Promise<void> {
await db.transaction(async (tx) => {
for (const { key, value } of constants) {
await tx.insert(keyValStore)
.values({ key, value })
.onConflictDoUpdate({
target: keyValStore.key,
set: { value },
});
}
});
}

View file

@ -0,0 +1,496 @@
import { db } from '../db/db_index';
import { coupons, reservedCoupons, users, orders, orderStatus, couponApplicableUsers, couponApplicableProducts } from '../db/schema';
import { eq, and, like, or, inArray, lt, desc } from 'drizzle-orm';
export interface Coupon {
id: number;
couponCode: string;
isUserBased: boolean;
discountPercent: string | null;
flatDiscount: string | null;
minOrder: string | null;
productIds: number[] | null;
maxValue: string | null;
isApplyForAll: boolean;
validTill: Date | null;
maxLimitForUser: number | null;
exclusiveApply: boolean;
isInvalidated: boolean;
createdAt: Date;
createdBy: number;
}
export async function getAllCoupons(
cursor?: number,
limit: number = 50,
search?: string
): Promise<{ coupons: any[]; hasMore: boolean }> {
let whereCondition = undefined;
const conditions = [];
if (cursor) {
conditions.push(lt(coupons.id, cursor));
}
if (search && search.trim()) {
conditions.push(like(coupons.couponCode, `%${search}%`));
}
if (conditions.length > 0) {
whereCondition = and(...conditions);
}
const result = await db.query.coupons.findMany({
where: whereCondition,
with: {
creator: true,
applicableUsers: {
with: {
user: true,
},
},
applicableProducts: {
with: {
product: true,
},
},
},
orderBy: (coupons, { desc }) => [desc(coupons.createdAt)],
limit: limit + 1,
});
const hasMore = result.length > limit;
const couponsList = hasMore ? result.slice(0, limit) : result;
return { coupons: couponsList, hasMore };
}
export async function getCouponById(id: number): Promise<any | null> {
return await db.query.coupons.findFirst({
where: eq(coupons.id, id),
with: {
creator: true,
applicableUsers: {
with: {
user: true,
},
},
applicableProducts: {
with: {
product: true,
},
},
},
});
}
export interface CreateCouponInput {
couponCode: string;
isUserBased: boolean;
discountPercent?: string;
flatDiscount?: string;
minOrder?: string;
productIds?: number[] | null;
maxValue?: string;
isApplyForAll: boolean;
validTill?: Date;
maxLimitForUser?: number;
exclusiveApply: boolean;
createdBy: number;
}
export async function createCouponWithRelations(
input: CreateCouponInput,
applicableUsers?: number[],
applicableProducts?: number[]
): Promise<Coupon> {
return await db.transaction(async (tx) => {
const [coupon] = await tx.insert(coupons).values({
couponCode: input.couponCode,
isUserBased: input.isUserBased,
discountPercent: input.discountPercent,
flatDiscount: input.flatDiscount,
minOrder: input.minOrder,
productIds: input.productIds,
createdBy: input.createdBy,
maxValue: input.maxValue,
isApplyForAll: input.isApplyForAll,
validTill: input.validTill,
maxLimitForUser: input.maxLimitForUser,
exclusiveApply: input.exclusiveApply,
}).returning();
if (applicableUsers && applicableUsers.length > 0) {
await tx.insert(couponApplicableUsers).values(
applicableUsers.map(userId => ({
couponId: coupon.id,
userId,
}))
);
}
if (applicableProducts && applicableProducts.length > 0) {
await tx.insert(couponApplicableProducts).values(
applicableProducts.map(productId => ({
couponId: coupon.id,
productId,
}))
);
}
return coupon as Coupon;
});
}
export interface UpdateCouponInput {
couponCode?: string;
isUserBased?: boolean;
discountPercent?: string;
flatDiscount?: string;
minOrder?: string;
productIds?: number[] | null;
maxValue?: string;
isApplyForAll?: boolean;
validTill?: Date | null;
maxLimitForUser?: number;
exclusiveApply?: boolean;
isInvalidated?: boolean;
}
export async function updateCouponWithRelations(
id: number,
input: UpdateCouponInput,
applicableUsers?: number[],
applicableProducts?: number[]
): Promise<Coupon> {
return await db.transaction(async (tx) => {
const [coupon] = await tx.update(coupons)
.set({
...input,
})
.where(eq(coupons.id, id))
.returning();
if (applicableUsers !== undefined) {
await tx.delete(couponApplicableUsers).where(eq(couponApplicableUsers.couponId, id));
if (applicableUsers.length > 0) {
await tx.insert(couponApplicableUsers).values(
applicableUsers.map(userId => ({
couponId: id,
userId,
}))
);
}
}
if (applicableProducts !== undefined) {
await tx.delete(couponApplicableProducts).where(eq(couponApplicableProducts.couponId, id));
if (applicableProducts.length > 0) {
await tx.insert(couponApplicableProducts).values(
applicableProducts.map(productId => ({
couponId: id,
productId,
}))
);
}
}
return coupon as Coupon;
});
}
export async function invalidateCoupon(id: number): Promise<Coupon> {
const result = await db.update(coupons)
.set({ isInvalidated: true })
.where(eq(coupons.id, id))
.returning();
return result[0] as Coupon;
}
export interface CouponValidationResult {
valid: boolean;
message?: string;
discountAmount?: number;
coupon?: Partial<Coupon>;
}
export async function validateCoupon(
code: string,
userId: number,
orderAmount: number
): Promise<CouponValidationResult> {
const coupon = await db.query.coupons.findFirst({
where: and(
eq(coupons.couponCode, code.toUpperCase()),
eq(coupons.isInvalidated, false)
),
});
if (!coupon) {
return { valid: false, message: "Coupon not found or invalidated" };
}
if (coupon.validTill && new Date(coupon.validTill) < new Date()) {
return { valid: false, message: "Coupon has expired" };
}
if (!coupon.isApplyForAll && !coupon.isUserBased) {
return { valid: false, message: "Coupon is not available for use" };
}
const minOrderValue = coupon.minOrder ? parseFloat(coupon.minOrder) : 0;
if (minOrderValue > 0 && orderAmount < minOrderValue) {
return { valid: false, message: `Minimum order amount is ${minOrderValue}` };
}
let discountAmount = 0;
if (coupon.discountPercent) {
const percent = parseFloat(coupon.discountPercent);
discountAmount = (orderAmount * percent) / 100;
} else if (coupon.flatDiscount) {
discountAmount = parseFloat(coupon.flatDiscount);
}
const maxValueLimit = coupon.maxValue ? parseFloat(coupon.maxValue) : 0;
if (maxValueLimit > 0 && discountAmount > maxValueLimit) {
discountAmount = maxValueLimit;
}
return {
valid: true,
discountAmount,
coupon: {
id: coupon.id,
discountPercent: coupon.discountPercent,
flatDiscount: coupon.flatDiscount,
maxValue: coupon.maxValue,
}
};
}
export async function getReservedCoupons(
cursor?: number,
limit: number = 50,
search?: string
): Promise<{ coupons: any[]; hasMore: boolean }> {
let whereCondition = undefined;
const conditions = [];
if (cursor) {
conditions.push(lt(reservedCoupons.id, cursor));
}
if (search && search.trim()) {
conditions.push(or(
like(reservedCoupons.secretCode, `%${search}%`),
like(reservedCoupons.couponCode, `%${search}%`)
));
}
if (conditions.length > 0) {
whereCondition = and(...conditions);
}
const result = await db.query.reservedCoupons.findMany({
where: whereCondition,
with: {
redeemedUser: true,
creator: true,
},
orderBy: (reservedCoupons, { desc }) => [desc(reservedCoupons.createdAt)],
limit: limit + 1,
});
const hasMore = result.length > limit;
const couponsList = hasMore ? result.slice(0, limit) : result;
return { coupons: couponsList, hasMore };
}
export async function createReservedCouponWithProducts(
input: any,
applicableProducts?: number[]
): Promise<any> {
return await db.transaction(async (tx) => {
const [coupon] = await tx.insert(reservedCoupons).values({
secretCode: input.secretCode,
couponCode: input.couponCode,
discountPercent: input.discountPercent,
flatDiscount: input.flatDiscount,
minOrder: input.minOrder,
productIds: input.productIds,
maxValue: input.maxValue,
validTill: input.validTill,
maxLimitForUser: input.maxLimitForUser,
exclusiveApply: input.exclusiveApply,
createdBy: input.createdBy,
}).returning();
if (applicableProducts && applicableProducts.length > 0) {
await tx.insert(couponApplicableProducts).values(
applicableProducts.map(productId => ({
couponId: coupon.id,
productId,
}))
);
}
return coupon;
});
}
export async function checkUsersExist(userIds: number[]): Promise<boolean> {
const existingUsers = await db.query.users.findMany({
where: inArray(users.id, userIds),
columns: { id: true },
});
return existingUsers.length === userIds.length;
}
export async function checkCouponExists(couponCode: string): Promise<boolean> {
const existing = await db.query.coupons.findFirst({
where: eq(coupons.couponCode, couponCode),
});
return !!existing;
}
export async function checkReservedCouponExists(secretCode: string): Promise<boolean> {
const existing = await db.query.reservedCoupons.findFirst({
where: eq(reservedCoupons.secretCode, secretCode),
});
return !!existing;
}
export async function generateCancellationCoupon(
orderId: number,
staffUserId: number,
userId: number,
orderAmount: number,
couponCode: string
): Promise<Coupon> {
return await db.transaction(async (tx) => {
const expiryDate = new Date();
expiryDate.setDate(expiryDate.getDate() + 30);
const [coupon] = await tx.insert(coupons).values({
couponCode,
isUserBased: true,
flatDiscount: orderAmount.toString(),
minOrder: orderAmount.toString(),
maxValue: orderAmount.toString(),
validTill: expiryDate,
maxLimitForUser: 1,
createdBy: staffUserId,
isApplyForAll: false,
}).returning();
await tx.insert(couponApplicableUsers).values({
couponId: coupon.id,
userId,
});
await tx.update(orderStatus)
.set({ refundCouponId: coupon.id })
.where(eq(orderStatus.orderId, orderId));
return coupon as Coupon;
});
}
export async function getOrderWithUser(orderId: number): Promise<any | null> {
return await db.query.orders.findFirst({
where: eq(orders.id, orderId),
with: {
user: true,
},
});
}
export async function createCouponForUser(
mobile: string,
couponCode: string,
staffUserId: number
): Promise<{ coupon: Coupon; user: { id: number; mobile: string; name: string | null } }> {
return await db.transaction(async (tx) => {
let user = await tx.query.users.findFirst({
where: eq(users.mobile, mobile),
});
if (!user) {
const [newUser] = await tx.insert(users).values({
name: null,
email: null,
mobile,
}).returning();
user = newUser;
}
const [coupon] = await tx.insert(coupons).values({
couponCode,
isUserBased: true,
discountPercent: "20",
minOrder: "1000",
maxValue: "500",
maxLimitForUser: 1,
isApplyForAll: false,
exclusiveApply: false,
createdBy: staffUserId,
validTill: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000),
}).returning();
await tx.insert(couponApplicableUsers).values({
couponId: coupon.id,
userId: user.id,
});
return {
coupon: coupon as Coupon,
user: {
id: user.id,
mobile: user.mobile as string,
name: user.name,
},
};
});
}
export interface UserMiniInfo {
id: number;
name: string;
mobile: string | null;
}
export async function getUsersForCoupon(
search?: string,
limit: number = 20,
offset: number = 0
): Promise<{ users: UserMiniInfo[] }> {
let whereCondition = undefined;
if (search && search.trim()) {
whereCondition = or(
like(users.name, `%${search}%`),
like(users.mobile, `%${search}%`)
);
}
const userList = await db.query.users.findMany({
where: whereCondition,
columns: {
id: true,
name: true,
mobile: true,
},
limit: limit,
offset: offset,
orderBy: (users, { asc }) => [asc(users.name)],
});
return {
users: userList.map(user => ({
id: user.id,
name: user.name || 'Unknown',
mobile: user.mobile,
}))
};
}

View file

@ -0,0 +1,710 @@
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))
})
}

View file

@ -0,0 +1,558 @@
import { db } from '../db/db_index'
import {
productInfo,
units,
specialDeals,
productSlots,
productTags,
productReviews,
productGroupInfo,
productGroupMembership,
productTagInfo,
users,
storeInfo,
} from '../db/schema'
import { and, desc, eq, inArray, sql } from 'drizzle-orm'
import type { InferInsertModel, InferSelectModel } from 'drizzle-orm'
import type {
AdminProduct,
AdminProductGroupInfo,
AdminProductTagWithProducts,
AdminProductReview,
AdminProductWithDetails,
AdminProductWithRelations,
AdminSpecialDeal,
AdminUnit,
AdminUpdateSlotProductsResult,
Store,
} from '@packages/shared'
type ProductRow = InferSelectModel<typeof productInfo>
type UnitRow = InferSelectModel<typeof units>
type StoreRow = InferSelectModel<typeof storeInfo>
type SpecialDealRow = InferSelectModel<typeof specialDeals>
type ProductTagInfoRow = InferSelectModel<typeof productTagInfo>
type ProductTagRow = InferSelectModel<typeof productTags>
type ProductGroupRow = InferSelectModel<typeof productGroupInfo>
type ProductGroupMembershipRow = InferSelectModel<typeof productGroupMembership>
type ProductReviewRow = InferSelectModel<typeof productReviews>
const getStringArray = (value: unknown): string[] | null => {
if (!Array.isArray(value)) return null
return value.map((item) => String(item))
}
const mapUnit = (unit: UnitRow): AdminUnit => ({
id: unit.id,
shortNotation: unit.shortNotation,
fullName: unit.fullName,
})
const mapStore = (store: StoreRow): Store => ({
id: store.id,
name: store.name,
description: store.description,
imageUrl: store.imageUrl,
owner: store.owner,
createdAt: store.createdAt,
// updatedAt: store.createdAt,
})
const mapProduct = (product: ProductRow): AdminProduct => ({
id: product.id,
name: product.name,
shortDescription: product.shortDescription ?? null,
longDescription: product.longDescription ?? null,
unitId: product.unitId,
price: product.price.toString(),
marketPrice: product.marketPrice ? product.marketPrice.toString() : null,
images: getStringArray(product.images),
isOutOfStock: product.isOutOfStock,
isSuspended: product.isSuspended,
isFlashAvailable: product.isFlashAvailable,
flashPrice: product.flashPrice ? product.flashPrice.toString() : null,
createdAt: product.createdAt,
incrementStep: product.incrementStep,
productQuantity: product.productQuantity,
storeId: product.storeId,
})
const mapSpecialDeal = (deal: SpecialDealRow): AdminSpecialDeal => ({
id: deal.id,
productId: deal.productId,
quantity: deal.quantity.toString(),
price: deal.price.toString(),
validTill: deal.validTill,
})
const mapTagInfo = (tag: ProductTagInfoRow) => ({
id: tag.id,
tagName: tag.tagName,
tagDescription: tag.tagDescription ?? null,
imageUrl: tag.imageUrl ?? null,
isDashboardTag: tag.isDashboardTag,
relatedStores: tag.relatedStores,
createdAt: tag.createdAt,
})
export async function getAllProducts(): Promise<AdminProductWithRelations[]> {
const products = await db.query.productInfo.findMany({
orderBy: productInfo.name,
with: {
unit: true,
store: true,
},
})
return products.map((product) => ({
...mapProduct(product),
unit: mapUnit(product.unit),
store: product.store ? mapStore(product.store) : null,
}))
}
export async function getProductById(id: number): Promise<AdminProductWithDetails | null> {
const product = await db.query.productInfo.findFirst({
where: eq(productInfo.id, id),
with: {
unit: true,
},
})
if (!product) {
return null
}
const deals = await db.query.specialDeals.findMany({
where: eq(specialDeals.productId, id),
orderBy: specialDeals.quantity,
})
const productTagsData = await db.query.productTags.findMany({
where: eq(productTags.productId, id),
with: {
tag: true,
},
})
return {
...mapProduct(product),
unit: mapUnit(product.unit),
deals: deals.map(mapSpecialDeal),
tags: productTagsData.map((tag) => mapTagInfo(tag.tag)),
}
}
export async function deleteProduct(id: number): Promise<AdminProduct | null> {
const [deletedProduct] = await db
.delete(productInfo)
.where(eq(productInfo.id, id))
.returning()
if (!deletedProduct) {
return null
}
return mapProduct(deletedProduct)
}
type ProductInfoInsert = InferInsertModel<typeof productInfo>
type ProductInfoUpdate = Partial<ProductInfoInsert>
export async function createProduct(input: ProductInfoInsert): Promise<AdminProduct> {
const [product] = await db.insert(productInfo).values(input).returning()
return mapProduct(product)
}
export async function updateProduct(id: number, updates: ProductInfoUpdate): Promise<AdminProduct | null> {
const [product] = await db.update(productInfo)
.set(updates)
.where(eq(productInfo.id, id))
.returning()
if (!product) {
return null
}
return mapProduct(product)
}
export async function toggleProductOutOfStock(id: number): Promise<AdminProduct | null> {
const product = await db.query.productInfo.findFirst({
where: eq(productInfo.id, id),
})
if (!product) {
return null
}
const [updatedProduct] = await db
.update(productInfo)
.set({
isOutOfStock: !product.isOutOfStock,
})
.where(eq(productInfo.id, id))
.returning()
if (!updatedProduct) {
return null
}
return mapProduct(updatedProduct)
}
export async function updateSlotProducts(slotId: string, productIds: string[]): Promise<AdminUpdateSlotProductsResult> {
const currentAssociations = await db.query.productSlots.findMany({
where: eq(productSlots.slotId, parseInt(slotId)),
columns: {
productId: true,
},
})
const currentProductIds = currentAssociations.map((assoc) => assoc.productId)
const newProductIds = productIds.map((id) => parseInt(id))
const productsToAdd = newProductIds.filter((id) => !currentProductIds.includes(id))
const productsToRemove = currentProductIds.filter((id) => !newProductIds.includes(id))
if (productsToRemove.length > 0) {
await db.delete(productSlots).where(
and(
eq(productSlots.slotId, parseInt(slotId)),
inArray(productSlots.productId, productsToRemove)
)
)
}
if (productsToAdd.length > 0) {
const newAssociations = productsToAdd.map((productId) => ({
productId,
slotId: parseInt(slotId),
}))
await db.insert(productSlots).values(newAssociations)
}
return {
message: 'Slot products updated successfully',
added: productsToAdd.length,
removed: productsToRemove.length,
}
}
export async function getSlotProductIds(slotId: string): Promise<number[]> {
const associations = await db.query.productSlots.findMany({
where: eq(productSlots.slotId, parseInt(slotId)),
columns: {
productId: true,
},
})
return associations.map((assoc) => assoc.productId)
}
export async function getAllUnits(): Promise<AdminUnit[]> {
const allUnits = await db.query.units.findMany({
orderBy: units.shortNotation,
})
return allUnits.map(mapUnit)
}
export async function getAllProductTags(): Promise<AdminProductTagWithProducts[]> {
const tags = await db.query.productTagInfo.findMany({
with: {
products: {
with: {
product: true,
},
},
},
})
return tags.map((tag) => ({
...mapTagInfo(tag),
products: tag.products.map((assignment) => ({
productId: assignment.productId,
tagId: assignment.tagId,
assignedAt: assignment.assignedAt,
product: mapProduct(assignment.product),
})),
}))
}
export async function getSlotsProductIds(slotIds: number[]): Promise<Record<number, number[]>> {
if (slotIds.length === 0) {
return {}
}
const associations = await db.query.productSlots.findMany({
where: inArray(productSlots.slotId, slotIds),
columns: {
slotId: true,
productId: true,
},
})
const result: Record<number, number[]> = {}
for (const assoc of associations) {
if (!result[assoc.slotId]) {
result[assoc.slotId] = []
}
result[assoc.slotId].push(assoc.productId)
}
slotIds.forEach((slotId) => {
if (!result[slotId]) {
result[slotId] = []
}
})
return result
}
export async function getProductReviews(productId: number, limit: number, offset: number) {
const reviews = await db
.select({
id: productReviews.id,
reviewBody: productReviews.reviewBody,
ratings: productReviews.ratings,
imageUrls: productReviews.imageUrls,
reviewTime: productReviews.reviewTime,
adminResponse: productReviews.adminResponse,
adminResponseImages: productReviews.adminResponseImages,
userName: users.name,
})
.from(productReviews)
.innerJoin(users, eq(productReviews.userId, users.id))
.where(eq(productReviews.productId, productId))
.orderBy(desc(productReviews.reviewTime))
.limit(limit)
.offset(offset)
const totalCountResult = await db
.select({ count: sql`count(*)` })
.from(productReviews)
.where(eq(productReviews.productId, productId))
const totalCount = Number(totalCountResult[0].count)
const mappedReviews: AdminProductReview[] = reviews.map((review) => ({
id: review.id,
reviewBody: review.reviewBody,
ratings: review.ratings,
imageUrls: review.imageUrls,
reviewTime: review.reviewTime,
adminResponse: review.adminResponse ?? null,
adminResponseImages: review.adminResponseImages,
userName: review.userName ?? null,
}))
return {
reviews: mappedReviews,
totalCount,
}
}
export async function respondToReview(
reviewId: number,
adminResponse: string | undefined,
adminResponseImages: string[]
): Promise<AdminProductReview | null> {
const [updatedReview] = await db
.update(productReviews)
.set({
adminResponse,
adminResponseImages,
})
.where(eq(productReviews.id, reviewId))
.returning()
if (!updatedReview) {
return null
}
return {
id: updatedReview.id,
reviewBody: updatedReview.reviewBody,
ratings: updatedReview.ratings,
imageUrls: updatedReview.imageUrls,
reviewTime: updatedReview.reviewTime,
adminResponse: updatedReview.adminResponse ?? null,
adminResponseImages: updatedReview.adminResponseImages,
userName: null,
}
}
export async function getAllProductGroups() {
const groups = await db.query.productGroupInfo.findMany({
with: {
memberships: {
with: {
product: true,
},
},
},
orderBy: desc(productGroupInfo.createdAt),
})
return groups.map((group) => ({
id: group.id,
groupName: group.groupName,
description: group.description ?? null,
createdAt: group.createdAt,
products: group.memberships.map((membership) => mapProduct(membership.product)),
productCount: group.memberships.length,
memberships: group.memberships
}))
}
export async function createProductGroup(
groupName: string,
description: string | undefined,
productIds: number[]
): Promise<AdminProductGroupInfo> {
const [newGroup] = await db
.insert(productGroupInfo)
.values({
groupName,
description,
})
.returning()
if (productIds.length > 0) {
const memberships = productIds.map((productId) => ({
productId,
groupId: newGroup.id,
}))
await db.insert(productGroupMembership).values(memberships)
}
return {
id: newGroup.id,
groupName: newGroup.groupName,
description: newGroup.description ?? null,
createdAt: newGroup.createdAt,
}
}
export async function updateProductGroup(
id: number,
groupName: string | undefined,
description: string | undefined,
productIds: number[] | undefined
): Promise<AdminProductGroupInfo | null> {
const updateData: Partial<{
groupName: string
description: string | null
}> = {}
if (groupName !== undefined) updateData.groupName = groupName
if (description !== undefined) updateData.description = description
const [updatedGroup] = await db
.update(productGroupInfo)
.set(updateData)
.where(eq(productGroupInfo.id, id))
.returning()
if (!updatedGroup) {
return null
}
if (productIds !== undefined) {
await db.delete(productGroupMembership).where(eq(productGroupMembership.groupId, id))
if (productIds.length > 0) {
const memberships = productIds.map((productId) => ({
productId,
groupId: id,
}))
await db.insert(productGroupMembership).values(memberships)
}
}
return {
id: updatedGroup.id,
groupName: updatedGroup.groupName,
description: updatedGroup.description ?? null,
createdAt: updatedGroup.createdAt,
}
}
export async function deleteProductGroup(id: number): Promise<AdminProductGroupInfo | null> {
await db.delete(productGroupMembership).where(eq(productGroupMembership.groupId, id))
const [deletedGroup] = await db
.delete(productGroupInfo)
.where(eq(productGroupInfo.id, id))
.returning()
if (!deletedGroup) {
return null
}
return {
id: deletedGroup.id,
groupName: deletedGroup.groupName,
description: deletedGroup.description ?? null,
createdAt: deletedGroup.createdAt,
}
}
export async function addProductToGroup(groupId: number, productId: number): Promise<void> {
await db.insert(productGroupMembership).values({ groupId, productId })
}
export async function removeProductFromGroup(groupId: number, productId: number): Promise<void> {
await db.delete(productGroupMembership)
.where(and(
eq(productGroupMembership.groupId, groupId),
eq(productGroupMembership.productId, productId)
))
}
export async function updateProductPrices(updates: Array<{
productId: number
price?: number
marketPrice?: number | null
flashPrice?: number | null
isFlashAvailable?: boolean
}>) {
if (updates.length === 0) {
return { updatedCount: 0, invalidIds: [] }
}
const productIds = updates.map((update) => update.productId)
const existingProducts = await db.query.productInfo.findMany({
where: inArray(productInfo.id, productIds),
columns: { id: true },
})
const existingIds = new Set(existingProducts.map((product) => product.id))
const invalidIds = productIds.filter((id) => !existingIds.has(id))
if (invalidIds.length > 0) {
return { updatedCount: 0, invalidIds }
}
const updatePromises = updates.map((update) => {
const { productId, price, marketPrice, flashPrice, isFlashAvailable } = update
const updateData: Partial<Pick<ProductInfoInsert, 'price' | 'marketPrice' | 'flashPrice' | 'isFlashAvailable'>> = {}
if (price !== undefined) updateData.price = price.toString()
if (marketPrice !== undefined) updateData.marketPrice = marketPrice === null ? null : marketPrice.toString()
if (flashPrice !== undefined) updateData.flashPrice = flashPrice === null ? null : flashPrice.toString()
if (isFlashAvailable !== undefined) updateData.isFlashAvailable = isFlashAvailable
return db
.update(productInfo)
.set(updateData)
.where(eq(productInfo.id, productId))
})
await Promise.all(updatePromises)
return { updatedCount: updates.length, invalidIds: [] }
}

View file

@ -0,0 +1,351 @@
import { db } from '../db/db_index'
import {
deliverySlotInfo,
productSlots,
productInfo,
vendorSnippets,
productGroupInfo,
} from '../db/schema'
import { and, asc, desc, eq, gt, inArray } from 'drizzle-orm'
import type {
AdminDeliverySlot,
AdminSlotWithProducts,
AdminSlotWithProductsAndSnippetsBase,
AdminSlotCreateResult,
AdminSlotUpdateResult,
AdminVendorSnippet,
AdminSlotProductSummary,
AdminUpdateSlotCapacityResult,
} from '@packages/shared'
type SlotSnippetInput = {
name: string
productIds: number[]
validTill?: string
}
const getStringArray = (value: unknown): string[] | null => {
if (!Array.isArray(value)) return null
return value.map((item) => String(item))
}
const getNumberArray = (value: unknown): number[] => {
if (!Array.isArray(value)) return []
return value.map((item) => Number(item))
}
const mapDeliverySlot = (slot: typeof deliverySlotInfo.$inferSelect): AdminDeliverySlot => ({
id: slot.id,
deliveryTime: slot.deliveryTime,
freezeTime: slot.freezeTime,
isActive: slot.isActive,
isFlash: slot.isFlash,
isCapacityFull: slot.isCapacityFull,
deliverySequence: slot.deliverySequence,
groupIds: slot.groupIds,
})
const mapSlotProductSummary = (product: { id: number; name: string; images: unknown }): AdminSlotProductSummary => ({
id: product.id,
name: product.name,
images: getStringArray(product.images),
})
const mapVendorSnippet = (snippet: typeof vendorSnippets.$inferSelect): AdminVendorSnippet => ({
id: snippet.id,
snippetCode: snippet.snippetCode,
slotId: snippet.slotId ?? null,
productIds: snippet.productIds,
isPermanent: snippet.isPermanent,
validTill: snippet.validTill ?? null,
createdAt: snippet.createdAt,
})
export async function getActiveSlotsWithProducts(): Promise<AdminSlotWithProducts[]> {
const slots = await db.query.deliverySlotInfo
.findMany({
where: eq(deliverySlotInfo.isActive, true),
orderBy: desc(deliverySlotInfo.deliveryTime),
with: {
productSlots: {
with: {
product: {
columns: {
id: true,
name: true,
images: true,
},
},
},
},
},
})
return slots.map((slot) => ({
...mapDeliverySlot(slot),
deliverySequence: getNumberArray(slot.deliverySequence),
products: slot.productSlots.map((ps) => mapSlotProductSummary(ps.product)),
}))
}
export async function getActiveSlots(): Promise<AdminDeliverySlot[]> {
const slots = await db.query.deliverySlotInfo.findMany({
where: eq(deliverySlotInfo.isActive, true),
})
return slots.map(mapDeliverySlot)
}
export async function getSlotsAfterDate(afterDate: Date): Promise<AdminDeliverySlot[]> {
const slots = await db.query.deliverySlotInfo.findMany({
where: and(
eq(deliverySlotInfo.isActive, true),
gt(deliverySlotInfo.deliveryTime, afterDate)
),
orderBy: asc(deliverySlotInfo.deliveryTime),
})
return slots.map(mapDeliverySlot)
}
export async function getSlotByIdWithRelations(id: number): Promise<AdminSlotWithProductsAndSnippetsBase | null> {
const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, id),
with: {
productSlots: {
with: {
product: {
columns: {
id: true,
name: true,
images: true,
},
},
},
},
vendorSnippets: true,
},
})
if (!slot) {
return null
}
return {
...mapDeliverySlot(slot),
deliverySequence: getNumberArray(slot.deliverySequence),
groupIds: getNumberArray(slot.groupIds),
products: slot.productSlots.map((ps) => mapSlotProductSummary(ps.product)),
vendorSnippets: slot.vendorSnippets.map(mapVendorSnippet),
}
}
export async function createSlotWithRelations(input: {
deliveryTime: string
freezeTime: string
isActive?: boolean
productIds?: number[]
vendorSnippets?: SlotSnippetInput[]
groupIds?: number[]
}): Promise<AdminSlotCreateResult> {
const { deliveryTime, freezeTime, isActive, productIds, vendorSnippets: snippets, groupIds } = input
const result = await db.transaction(async (tx) => {
const [newSlot] = await tx
.insert(deliverySlotInfo)
.values({
deliveryTime: new Date(deliveryTime),
freezeTime: new Date(freezeTime),
isActive: isActive !== undefined ? isActive : true,
groupIds: groupIds !== undefined ? groupIds : [],
})
.returning()
if (productIds && productIds.length > 0) {
const associations = productIds.map((productId) => ({
productId,
slotId: newSlot.id,
}))
await tx.insert(productSlots).values(associations)
}
let createdSnippets: AdminVendorSnippet[] = []
if (snippets && snippets.length > 0) {
for (const snippet of snippets) {
const products = await tx.query.productInfo.findMany({
where: inArray(productInfo.id, snippet.productIds),
})
if (products.length !== snippet.productIds.length) {
throw new Error(`One or more invalid product IDs in snippet "${snippet.name}"`)
}
const existingSnippet = await tx.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.snippetCode, snippet.name),
})
if (existingSnippet) {
throw new Error(`Snippet name "${snippet.name}" already exists`)
}
const [createdSnippet] = await tx.insert(vendorSnippets).values({
snippetCode: snippet.name,
slotId: newSlot.id,
productIds: snippet.productIds,
validTill: snippet.validTill ? new Date(snippet.validTill) : undefined,
}).returning()
createdSnippets.push(mapVendorSnippet(createdSnippet))
}
}
return {
slot: mapDeliverySlot(newSlot),
createdSnippets,
message: 'Slot created successfully',
}
})
return result
}
export async function updateSlotWithRelations(input: {
id: number
deliveryTime: string
freezeTime: string
isActive?: boolean
productIds?: number[]
vendorSnippets?: SlotSnippetInput[]
groupIds?: number[]
}): Promise<AdminSlotUpdateResult | null> {
const { id, deliveryTime, freezeTime, isActive, productIds, vendorSnippets: snippets, groupIds } = input
let validGroupIds = groupIds
if (groupIds && groupIds.length > 0) {
const existingGroups = await db.query.productGroupInfo.findMany({
where: inArray(productGroupInfo.id, groupIds),
columns: { id: true },
})
validGroupIds = existingGroups.map((group) => group.id)
}
const result = await db.transaction(async (tx) => {
const [updatedSlot] = await tx
.update(deliverySlotInfo)
.set({
deliveryTime: new Date(deliveryTime),
freezeTime: new Date(freezeTime),
isActive: isActive !== undefined ? isActive : true,
groupIds: validGroupIds !== undefined ? validGroupIds : [],
})
.where(eq(deliverySlotInfo.id, id))
.returning()
if (!updatedSlot) {
return null
}
if (productIds !== undefined) {
await tx.delete(productSlots).where(eq(productSlots.slotId, id))
if (productIds.length > 0) {
const associations = productIds.map((productId) => ({
productId,
slotId: id,
}))
await tx.insert(productSlots).values(associations)
}
}
let createdSnippets: AdminVendorSnippet[] = []
if (snippets && snippets.length > 0) {
for (const snippet of snippets) {
const products = await tx.query.productInfo.findMany({
where: inArray(productInfo.id, snippet.productIds),
})
if (products.length !== snippet.productIds.length) {
throw new Error(`One or more invalid product IDs in snippet "${snippet.name}"`)
}
const existingSnippet = await tx.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.snippetCode, snippet.name),
})
if (existingSnippet) {
throw new Error(`Snippet name "${snippet.name}" already exists`)
}
const [createdSnippet] = await tx.insert(vendorSnippets).values({
snippetCode: snippet.name,
slotId: id,
productIds: snippet.productIds,
validTill: snippet.validTill ? new Date(snippet.validTill) : undefined,
}).returning()
createdSnippets.push(mapVendorSnippet(createdSnippet))
}
}
return {
slot: mapDeliverySlot(updatedSlot),
createdSnippets,
message: 'Slot updated successfully',
}
})
return result
}
export async function deleteSlotById(id: number): Promise<AdminDeliverySlot | null> {
const [deletedSlot] = await db
.update(deliverySlotInfo)
.set({ isActive: false })
.where(eq(deliverySlotInfo.id, id))
.returning()
if (!deletedSlot) {
return null
}
return mapDeliverySlot(deletedSlot)
}
export async function getSlotDeliverySequence(slotId: number): Promise<AdminDeliverySlot | null> {
const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, slotId),
})
if (!slot) {
return null
}
return mapDeliverySlot(slot)
}
export async function updateSlotDeliverySequence(slotId: number, sequence: unknown) {
const [updatedSlot] = await db
.update(deliverySlotInfo)
.set({ deliverySequence: sequence })
.where(eq(deliverySlotInfo.id, slotId))
.returning({
id: deliverySlotInfo.id,
deliverySequence: deliverySlotInfo.deliverySequence,
})
return updatedSlot || null
}
export async function updateSlotCapacity(slotId: number, isCapacityFull: boolean): Promise<AdminUpdateSlotCapacityResult | null> {
const [updatedSlot] = await db
.update(deliverySlotInfo)
.set({ isCapacityFull })
.where(eq(deliverySlotInfo.id, slotId))
.returning()
if (!updatedSlot) {
return null
}
return {
success: true,
slot: mapDeliverySlot(updatedSlot),
message: `Slot ${isCapacityFull ? 'marked as full capacity' : 'capacity reset'}`,
}
}

View file

@ -0,0 +1,146 @@
import { db } from '../db/db_index';
import { staffUsers, staffRoles, users, userDetails, orders } from '../db/schema';
import { eq, or, ilike, and, lt, desc } from 'drizzle-orm';
export interface StaffUser {
id: number;
name: string;
password: string;
staffRoleId: number | null;
createdAt: Date;
}
export async function getStaffUserByName(name: string): Promise<StaffUser | null> {
const staff = await db.query.staffUsers.findFirst({
where: eq(staffUsers.name, name),
});
return staff || null;
}
export async function getAllStaff(): Promise<any[]> {
const staff = await db.query.staffUsers.findMany({
columns: {
id: true,
name: true,
},
with: {
role: {
with: {
rolePermissions: {
with: {
permission: true,
},
},
},
},
},
});
return staff;
}
export async function getAllUsers(
cursor?: number,
limit: number = 20,
search?: string
): Promise<{ users: any[]; hasMore: boolean }> {
let whereCondition = undefined;
if (search) {
whereCondition = or(
ilike(users.name, `%${search}%`),
ilike(users.email, `%${search}%`),
ilike(users.mobile, `%${search}%`)
);
}
if (cursor) {
const cursorCondition = lt(users.id, cursor);
whereCondition = whereCondition ? and(whereCondition, cursorCondition) : cursorCondition;
}
const allUsers = await db.query.users.findMany({
where: whereCondition,
with: {
userDetails: true,
},
orderBy: desc(users.id),
limit: limit + 1,
});
const hasMore = allUsers.length > limit;
const usersToReturn = hasMore ? allUsers.slice(0, limit) : allUsers;
return { users: usersToReturn, hasMore };
}
export async function getUserWithDetails(userId: number): Promise<any | null> {
const user = await db.query.users.findFirst({
where: eq(users.id, userId),
with: {
userDetails: true,
orders: {
orderBy: desc(orders.createdAt),
limit: 1,
},
},
});
return user || null;
}
export async function updateUserSuspensionStatus(userId: number, isSuspended: boolean): Promise<void> {
await db
.insert(userDetails)
.values({ userId, isSuspended })
.onConflictDoUpdate({
target: userDetails.userId,
set: { isSuspended },
});
}
export async function checkStaffUserExists(name: string): Promise<boolean> {
const existingUser = await db.query.staffUsers.findFirst({
where: eq(staffUsers.name, name),
});
return !!existingUser;
}
export async function checkStaffRoleExists(roleId: number): Promise<boolean> {
const role = await db.query.staffRoles.findFirst({
where: eq(staffRoles.id, roleId),
});
return !!role;
}
export async function createStaffUser(
name: string,
password: string,
roleId: number
): Promise<StaffUser> {
const [newUser] = await db.insert(staffUsers).values({
name: name.trim(),
password,
staffRoleId: roleId,
}).returning();
return {
id: newUser.id,
name: newUser.name,
password: newUser.password,
staffRoleId: newUser.staffRoleId,
createdAt: newUser.createdAt,
};
}
export async function getAllRoles(): Promise<any[]> {
const roles = await db.query.staffRoles.findMany({
columns: {
id: true,
roleName: true,
},
});
return roles;
}

View file

@ -0,0 +1,145 @@
import { db } from '../db/db_index';
import { storeInfo, productInfo } from '../db/schema';
import { eq, inArray } from 'drizzle-orm';
export interface Store {
id: number;
name: string;
description: string | null;
imageUrl: string | null;
owner: number;
createdAt: Date;
// updatedAt: Date;
}
export async function getAllStores(): Promise<any[]> {
const stores = await db.query.storeInfo.findMany({
with: {
owner: true,
},
});
return stores;
}
export async function getStoreById(id: number): Promise<any | null> {
const store = await db.query.storeInfo.findFirst({
where: eq(storeInfo.id, id),
with: {
owner: true,
},
});
return store || null;
}
export interface CreateStoreInput {
name: string;
description?: string;
imageUrl?: string;
owner: number;
}
export async function createStore(
input: CreateStoreInput,
products?: number[]
): Promise<Store> {
const [newStore] = await db
.insert(storeInfo)
.values({
name: input.name,
description: input.description,
imageUrl: input.imageUrl,
owner: input.owner,
})
.returning();
if (products && products.length > 0) {
await db
.update(productInfo)
.set({ storeId: newStore.id })
.where(inArray(productInfo.id, products));
}
return {
id: newStore.id,
name: newStore.name,
description: newStore.description,
imageUrl: newStore.imageUrl,
owner: newStore.owner,
createdAt: newStore.createdAt,
// updatedAt: newStore.updatedAt,
};
}
export interface UpdateStoreInput {
name?: string;
description?: string;
imageUrl?: string;
owner?: number;
}
export async function updateStore(
id: number,
input: UpdateStoreInput,
products?: number[]
): Promise<Store> {
const [updatedStore] = await db
.update(storeInfo)
.set({
...input,
// updatedAt: new Date(),
})
.where(eq(storeInfo.id, id))
.returning();
if (!updatedStore) {
throw new Error("Store not found");
}
if (products !== undefined) {
await db
.update(productInfo)
.set({ storeId: null })
.where(eq(productInfo.storeId, id));
if (products.length > 0) {
await db
.update(productInfo)
.set({ storeId: id })
.where(inArray(productInfo.id, products));
}
}
return {
id: updatedStore.id,
name: updatedStore.name,
description: updatedStore.description,
imageUrl: updatedStore.imageUrl,
owner: updatedStore.owner,
createdAt: updatedStore.createdAt,
// updatedAt: updatedStore.updatedAt,
};
}
export async function deleteStore(id: number): Promise<{ message: string }> {
return await db.transaction(async (tx) => {
await tx
.update(productInfo)
.set({ storeId: null })
.where(eq(productInfo.storeId, id));
const [deletedStore] = await tx
.delete(storeInfo)
.where(eq(storeInfo.id, id))
.returning();
if (!deletedStore) {
throw new Error("Store not found");
}
return {
message: "Store deleted successfully",
};
});
}

View file

@ -0,0 +1,270 @@
import { db } from '../db/db_index';
import { users, userDetails, orders, orderItems, complaints, notifCreds, unloggedUserTokens, userIncidents, orderStatus } from '../db/schema';
import { eq, sql, desc, asc, count, max, inArray } from 'drizzle-orm';
export async function createUserByMobile(mobile: string): Promise<any> {
const [newUser] = await db
.insert(users)
.values({
name: null,
email: null,
mobile,
})
.returning();
return newUser;
}
export async function getUserByMobile(mobile: string): Promise<any | null> {
const [existingUser] = await db
.select()
.from(users)
.where(eq(users.mobile, mobile))
.limit(1);
return existingUser || null;
}
export async function getUnresolvedComplaintsCount(): Promise<number> {
const result = await db
.select({ count: count(complaints.id) })
.from(complaints)
.where(eq(complaints.isResolved, false));
return result[0]?.count || 0;
}
export async function getAllUsersWithFilters(
limit: number,
cursor?: number,
search?: string
): Promise<{ users: any[]; hasMore: boolean }> {
const whereConditions = [];
if (search && search.trim()) {
whereConditions.push(sql`${users.mobile} ILIKE ${`%${search.trim()}%`}`);
}
if (cursor) {
whereConditions.push(sql`${users.id} > ${cursor}`);
}
const usersList = await db
.select({
id: users.id,
name: users.name,
mobile: users.mobile,
createdAt: users.createdAt,
})
.from(users)
.where(whereConditions.length > 0 ? sql.join(whereConditions, sql` AND `) : undefined)
.orderBy(asc(users.id))
.limit(limit + 1);
const hasMore = usersList.length > limit;
const usersToReturn = hasMore ? usersList.slice(0, limit) : usersList;
return { users: usersToReturn, hasMore };
}
export async function getOrderCountsByUserIds(userIds: number[]): Promise<{ userId: number; totalOrders: number }[]> {
if (userIds.length === 0) return [];
return await db
.select({
userId: orders.userId,
totalOrders: count(orders.id),
})
.from(orders)
.where(sql`${orders.userId} IN (${sql.join(userIds, sql`, `)})`)
.groupBy(orders.userId);
}
export async function getLastOrdersByUserIds(userIds: number[]): Promise<{ userId: number; lastOrderDate: Date | null }[]> {
if (userIds.length === 0) return [];
return await db
.select({
userId: orders.userId,
lastOrderDate: max(orders.createdAt),
})
.from(orders)
.where(sql`${orders.userId} IN (${sql.join(userIds, sql`, `)})`)
.groupBy(orders.userId);
}
export async function getSuspensionStatusesByUserIds(userIds: number[]): Promise<{ userId: number; isSuspended: boolean }[]> {
if (userIds.length === 0) return [];
return await db
.select({
userId: userDetails.userId,
isSuspended: userDetails.isSuspended,
})
.from(userDetails)
.where(sql`${userDetails.userId} IN (${sql.join(userIds, sql`, `)})`);
}
export async function getUserBasicInfo(userId: number): Promise<any | null> {
const user = await db
.select({
id: users.id,
name: users.name,
mobile: users.mobile,
createdAt: users.createdAt,
})
.from(users)
.where(eq(users.id, userId))
.limit(1);
return user[0] || null;
}
export async function getUserSuspensionStatus(userId: number): Promise<boolean> {
const userDetail = await db
.select({
isSuspended: userDetails.isSuspended,
})
.from(userDetails)
.where(eq(userDetails.userId, userId))
.limit(1);
return userDetail[0]?.isSuspended ?? false;
}
export async function getUserOrders(userId: number): Promise<any[]> {
return await db
.select({
id: orders.id,
readableId: orders.readableId,
totalAmount: orders.totalAmount,
createdAt: orders.createdAt,
isFlashDelivery: orders.isFlashDelivery,
})
.from(orders)
.where(eq(orders.userId, userId))
.orderBy(desc(orders.createdAt));
}
export async function getOrderStatusesByOrderIds(orderIds: number[]): Promise<{ orderId: number; isDelivered: boolean; isCancelled: boolean }[]> {
if (orderIds.length === 0) return [];
return await db
.select({
orderId: orderStatus.orderId,
isDelivered: orderStatus.isDelivered,
isCancelled: orderStatus.isCancelled,
})
.from(orderStatus)
.where(sql`${orderStatus.orderId} IN (${sql.join(orderIds, sql`, `)})`);
}
export async function getItemCountsByOrderIds(orderIds: number[]): Promise<{ orderId: number; itemCount: number }[]> {
if (orderIds.length === 0) return [];
return await db
.select({
orderId: orderItems.orderId,
itemCount: count(orderItems.id),
})
.from(orderItems)
.where(sql`${orderItems.orderId} IN (${sql.join(orderIds, sql`, `)})`)
.groupBy(orderItems.orderId);
}
export async function upsertUserSuspension(userId: number, isSuspended: boolean): Promise<void> {
const existingDetail = await db
.select({ id: userDetails.id })
.from(userDetails)
.where(eq(userDetails.userId, userId))
.limit(1);
if (existingDetail.length > 0) {
await db
.update(userDetails)
.set({ isSuspended })
.where(eq(userDetails.userId, userId));
} else {
await db
.insert(userDetails)
.values({
userId,
isSuspended,
});
}
}
export async function searchUsers(search?: string): Promise<any[]> {
if (search && search.trim()) {
return await db
.select({
id: users.id,
name: users.name,
mobile: users.mobile,
})
.from(users)
.where(sql`${users.mobile} ILIKE ${`%${search.trim()}%`} OR ${users.name} ILIKE ${`%${search.trim()}%`}`);
} else {
return await db
.select({
id: users.id,
name: users.name,
mobile: users.mobile,
})
.from(users);
}
}
export async function getAllNotifCreds(): Promise<{ userId: number, token: string }[]> {
return await db
.select({ userId: notifCreds.userId, token: notifCreds.token })
.from(notifCreds);
}
export async function getAllUnloggedTokens(): Promise<{ token: string }[]> {
return await db
.select({ token: unloggedUserTokens.token })
.from(unloggedUserTokens);
}
export async function getNotifTokensByUserIds(userIds: number[]): Promise<{ token: string }[]> {
return await db
.select({ token: notifCreds.token })
.from(notifCreds)
.where(inArray(notifCreds.userId, userIds));
}
export async function getUserIncidentsWithRelations(userId: number): Promise<any[]> {
return await db.query.userIncidents.findMany({
where: eq(userIncidents.userId, userId),
with: {
order: {
with: {
orderStatus: true,
},
},
addedBy: true,
},
orderBy: desc(userIncidents.dateAdded),
});
}
export async function createUserIncident(
userId: number,
orderId: number | undefined,
adminComment: string | undefined,
adminUserId: number,
negativityScore: number | undefined
): Promise<any> {
const [incident] = await db.insert(userIncidents)
.values({
userId,
orderId,
adminComment,
addedBy: adminUserId,
negativityScore,
})
.returning();
return incident;
}

View file

@ -0,0 +1,250 @@
import { db } from '../db/db_index'
import { vendorSnippets, deliverySlotInfo, productInfo, orders, orderItems, orderStatus } from '../db/schema'
import { desc, eq, inArray } from 'drizzle-orm'
import type { InferSelectModel } from 'drizzle-orm'
import type {
AdminDeliverySlot,
AdminVendorSnippet,
AdminVendorSnippetWithSlot,
AdminVendorSnippetProduct,
AdminVendorUpdatePackagingResult,
} from '@packages/shared'
type VendorSnippetRow = InferSelectModel<typeof vendorSnippets>
type DeliverySlotRow = InferSelectModel<typeof deliverySlotInfo>
type ProductRow = InferSelectModel<typeof productInfo>
const mapVendorSnippet = (snippet: VendorSnippetRow): AdminVendorSnippet => ({
id: snippet.id,
snippetCode: snippet.snippetCode,
slotId: snippet.slotId ?? null,
productIds: snippet.productIds,
isPermanent: snippet.isPermanent,
validTill: snippet.validTill ?? null,
createdAt: snippet.createdAt,
})
const mapDeliverySlot = (slot: DeliverySlotRow): AdminDeliverySlot => ({
id: slot.id,
deliveryTime: slot.deliveryTime,
freezeTime: slot.freezeTime,
isActive: slot.isActive,
isFlash: slot.isFlash,
isCapacityFull: slot.isCapacityFull,
deliverySequence: slot.deliverySequence,
groupIds: slot.groupIds,
})
const mapProductSummary = (product:{id:number, name: string}): AdminVendorSnippetProduct => ({
id: product.id,
name: product.name,
})
export async function checkVendorSnippetExists(snippetCode: string): Promise<boolean> {
const existingSnippet = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.snippetCode, snippetCode),
})
return !!existingSnippet
}
export async function getVendorSnippetById(id: number): Promise<AdminVendorSnippetWithSlot | null> {
const snippet = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.id, id),
with: {
slot: true,
},
})
if (!snippet) {
return null
}
return {
...mapVendorSnippet(snippet),
slot: snippet.slot ? mapDeliverySlot(snippet.slot) : null,
}
}
export async function getVendorSnippetByCode(snippetCode: string): Promise<AdminVendorSnippet | null> {
const snippet = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.snippetCode, snippetCode),
})
return snippet ? mapVendorSnippet(snippet) : null
}
export async function getAllVendorSnippets(): Promise<AdminVendorSnippetWithSlot[]> {
const snippets = await db.query.vendorSnippets.findMany({
with: {
slot: true,
},
orderBy: (vendorSnippets, { desc }) => [desc(vendorSnippets.createdAt)],
})
return snippets.map((snippet) => ({
...mapVendorSnippet(snippet),
slot: snippet.slot ? mapDeliverySlot(snippet.slot) : null,
}))
}
export async function createVendorSnippet(input: {
snippetCode: string
slotId?: number
productIds: number[]
isPermanent: boolean
validTill?: Date
}): Promise<AdminVendorSnippet> {
const [result] = await db.insert(vendorSnippets).values({
snippetCode: input.snippetCode,
slotId: input.slotId,
productIds: input.productIds,
isPermanent: input.isPermanent,
validTill: input.validTill,
}).returning()
return mapVendorSnippet(result)
}
export async function updateVendorSnippet(id: number, updates: {
snippetCode?: string
slotId?: number | null
productIds?: number[]
isPermanent?: boolean
validTill?: Date | null
}): Promise<AdminVendorSnippet | null> {
const [result] = await db.update(vendorSnippets)
.set(updates)
.where(eq(vendorSnippets.id, id))
.returning()
return result ? mapVendorSnippet(result) : null
}
export async function deleteVendorSnippet(id: number): Promise<AdminVendorSnippet | null> {
const [result] = await db.delete(vendorSnippets)
.where(eq(vendorSnippets.id, id))
.returning()
return result ? mapVendorSnippet(result) : null
}
export async function getProductsByIds(productIds: number[]): Promise<AdminVendorSnippetProduct[]> {
const products = await db.query.productInfo.findMany({
where: inArray(productInfo.id, productIds),
columns: { id: true, name: true },
})
const prods = products.map(mapProductSummary)
return prods;
}
export async function getVendorSlotById(slotId: number): Promise<AdminDeliverySlot | null> {
const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, slotId),
})
return slot ? mapDeliverySlot(slot) : null
}
export async function getVendorOrdersBySlotId(slotId: number) {
return await db.query.orders.findMany({
where: eq(orders.slotId, slotId),
with: {
orderItems: {
with: {
product: {
with: {
unit: true,
},
},
},
},
orderStatus: true,
user: true,
slot: true,
},
orderBy: (orders, { desc }) => [desc(orders.createdAt)],
})
}
export async function getVendorOrders() {
return await db.query.orders.findMany({
with: {
user: true,
orderItems: {
with: {
product: {
with: {
unit: true,
},
},
},
},
},
orderBy: (orders, { desc }) => [desc(orders.createdAt)],
})
}
export async function getOrderItemsByOrderIds(orderIds: number[]) {
return await db.query.orderItems.findMany({
where: inArray(orderItems.orderId, orderIds),
with: {
product: {
with: {
unit: true,
},
},
},
})
}
export async function getOrderStatusByOrderIds(orderIds: number[]) {
return await db.query.orderStatus.findMany({
where: inArray(orderStatus.orderId, orderIds),
})
}
export async function updateVendorOrderItemPackaging(
orderItemId: number,
isPackaged: boolean
): Promise<AdminVendorUpdatePackagingResult> {
const orderItem = await db.query.orderItems.findFirst({
where: eq(orderItems.id, orderItemId),
with: {
order: {
with: {
slot: true,
},
},
},
})
if (!orderItem) {
return { success: false, message: 'Order item not found' }
}
if (!orderItem.order.slotId) {
return { success: false, message: 'Order item not associated with a vendor slot' }
}
const snippetExists = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.slotId, orderItem.order.slotId),
})
if (!snippetExists) {
return { success: false, message: "No vendor snippet found for this order's slot" }
}
const [updatedItem] = await db.update(orderItems)
.set({
is_packaged: isPackaged,
})
.where(eq(orderItems.id, orderItemId))
.returning({ id: orderItems.id })
if (!updatedItem) {
return { success: false, message: 'Failed to update packaging status' }
}
return { success: true, orderItemId, is_packaged: isPackaged }
}

View file

@ -0,0 +1,19 @@
// Common utility functions that can be used by both admin and user APIs
export function formatDate(date: Date): string {
return date.toISOString();
}
export function generateCode(prefix: string, length: number = 6): string {
const timestamp = Date.now().toString().slice(-length);
const random = Math.random().toString(36).substring(2, 8).toUpperCase();
return `${prefix}${timestamp}${random}`;
}
export function calculateDiscount(amount: number, percent: number, maxDiscount?: number): number {
let discount = (amount * percent) / 100;
if (maxDiscount && discount > maxDiscount) {
discount = maxDiscount;
}
return discount;
}

View file

@ -0,0 +1,23 @@
import { db } from '../db/db_index';
import { addresses, addressAreas, addressZones } from '../db/schema';
import { eq, desc } from 'drizzle-orm';
export async function getZones(): Promise<any[]> {
const zones = await db.select().from(addressZones).orderBy(desc(addressZones.addedAt));
return zones;
}
export async function getAreas(): Promise<any[]> {
const areas = await db.select().from(addressAreas).orderBy(desc(addressAreas.createdAt));
return areas;
}
export async function createZone(zoneName: string): Promise<any> {
const [zone] = await db.insert(addressZones).values({ zoneName }).returning();
return zone;
}
export async function createArea(placeName: string, zoneId: number | null): Promise<any> {
const [area] = await db.insert(addressAreas).values({ placeName, zoneId }).returning();
return area;
}

View file

@ -0,0 +1,14 @@
import { db } from '../db/db_index';
import { users } from '../db/schema';
import { eq } from 'drizzle-orm';
export async function getUserByMobile(mobile: string): Promise<any | null> {
return await db.query.users.findFirst({
where: eq(users.mobile, mobile),
});
}
export async function createUser(userData: any): Promise<any> {
const [user] = await db.insert(users).values(userData).returning();
return user;
}

View file

@ -0,0 +1,11 @@
import { db } from '../db/db_index';
import { homeBanners } from '../db/schema';
import { eq, and, desc, sql } from 'drizzle-orm';
export async function getActiveBanners(): Promise<any[]> {
const banners = await db.query.homeBanners.findMany({
where: eq(homeBanners.isActive, true),
orderBy: desc(homeBanners.createdAt),
});
return banners;
}

View file

@ -0,0 +1,41 @@
import { db } from '../db/db_index';
import { cartItems, productInfo } from '../db/schema';
import { eq, and } from 'drizzle-orm';
export async function getCartItems(userId: number): Promise<any[]> {
return await db.query.cartItems.findMany({
where: eq(cartItems.userId, userId),
with: {
product: {
with: {
unit: true,
},
},
},
});
}
export async function addToCart(userId: number, productId: number, quantity: number): Promise<any> {
const [item] = await db.insert(cartItems).values({
userId,
productId,
quantity,
}).returning();
return item;
}
export async function updateCartItem(itemId: number, quantity: number): Promise<any> {
const [item] = await db.update(cartItems)
.set({ quantity })
.where(eq(cartItems.id, itemId))
.returning();
return item;
}
export async function removeFromCart(itemId: number): Promise<void> {
await db.delete(cartItems).where(eq(cartItems.id, itemId));
}
export async function clearCart(userId: number): Promise<void> {
await db.delete(cartItems).where(eq(cartItems.userId, userId));
}

View file

@ -0,0 +1,21 @@
import { db } from '../db/db_index';
import { complaints } from '../db/schema';
import { eq, desc } from 'drizzle-orm';
export async function getUserComplaints(userId: number): Promise<any[]> {
return await db.query.complaints.findMany({
where: eq(complaints.userId, userId),
orderBy: desc(complaints.createdAt),
});
}
export async function createComplaint(userId: number, orderId: number | null, complaintBody: string, images?: string[]): Promise<any> {
const [complaint] = await db.insert(complaints).values({
userId,
orderId,
complaintBody,
images,
isResolved: false,
}).returning();
return complaint;
}

View file

@ -0,0 +1,43 @@
import { db } from '../db/db_index';
import { coupons, couponUsage } from '../db/schema';
import { eq, and } from 'drizzle-orm';
export async function validateUserCoupon(code: string, userId: number, orderAmount: number): Promise<any> {
const coupon = await db.query.coupons.findFirst({
where: eq(coupons.couponCode, code.toUpperCase()),
});
if (!coupon || coupon.isInvalidated) {
return { valid: false, message: 'Invalid coupon' };
}
if (coupon.validTill && new Date(coupon.validTill) < new Date()) {
return { valid: false, message: 'Coupon expired' };
}
let discountAmount = 0;
if (coupon.discountPercent) {
discountAmount = (orderAmount * parseFloat(coupon.discountPercent)) / 100;
} else if (coupon.flatDiscount) {
discountAmount = parseFloat(coupon.flatDiscount);
}
if (coupon.maxValue) {
const maxDiscount = parseFloat(coupon.maxValue);
if (discountAmount > maxDiscount) {
discountAmount = maxDiscount;
}
}
return {
valid: true,
discountAmount,
couponId: coupon.id,
};
}
export async function getUserCoupons(userId: number): Promise<any[]> {
return await db.query.coupons.findMany({
where: eq(coupons.userId, userId),
});
}

View file

@ -0,0 +1,59 @@
import { db } from '../db/db_index';
import { orders, orderItems, orderStatus } from '../db/schema';
import { eq, desc } from 'drizzle-orm';
export async function getUserOrders(userId: number): Promise<any[]> {
return await db.query.orders.findMany({
where: eq(orders.userId, userId),
with: {
orderItems: {
with: {
product: {
with: {
unit: true,
},
},
},
},
orderStatus: true,
slot: true,
},
orderBy: desc(orders.createdAt),
});
}
export async function getOrderById(orderId: number, userId: number): Promise<any | null> {
return await db.query.orders.findFirst({
where: eq(orders.id, orderId),
with: {
orderItems: {
with: {
product: true,
},
},
orderStatus: true,
address: true,
slot: true,
},
});
}
export async function createOrder(orderData: any, orderItemsData: any[]): Promise<any> {
return await db.transaction(async (tx) => {
const [order] = await tx.insert(orders).values(orderData).returning();
for (const item of orderItemsData) {
await tx.insert(orderItems).values({
...item,
orderId: order.id,
});
}
await tx.insert(orderStatus).values({
orderId: order.id,
paymentStatus: 'pending',
});
return order;
});
}

View file

@ -0,0 +1,22 @@
import { db } from '../db/db_index';
import { payments, orders, orderStatus } from '../db/schema';
import { eq } from 'drizzle-orm';
export async function createPayment(paymentData: any): Promise<any> {
const [payment] = await db.insert(payments).values(paymentData).returning();
return payment;
}
export async function updatePaymentStatus(paymentId: number, status: string): Promise<any> {
const [payment] = await db.update(payments)
.set({ paymentStatus: status })
.where(eq(payments.id, paymentId))
.returning();
return payment;
}
export async function getPaymentByOrderId(orderId: number): Promise<any | null> {
return await db.query.payments.findFirst({
where: eq(payments.orderId, orderId),
});
}

View file

@ -0,0 +1,41 @@
import { db } from '../db/db_index';
import { productInfo, productReviews } from '../db/schema';
import { eq, desc } from 'drizzle-orm';
export async function getAllProducts(): Promise<any[]> {
return await db.query.productInfo.findMany({
with: {
unit: true,
store: true,
specialDeals: true,
},
orderBy: productInfo.name,
});
}
export async function getProductById(id: number): Promise<any | null> {
return await db.query.productInfo.findFirst({
where: eq(productInfo.id, id),
with: {
unit: true,
store: true,
specialDeals: true,
productReviews: {
with: {
user: true,
},
orderBy: desc(productReviews.createdAt),
},
},
});
}
export async function createProductReview(userId: number, productId: number, rating: number, comment?: string): Promise<any> {
const [review] = await db.insert(productReviews).values({
userId,
productId,
rating,
comment,
}).returning();
return review;
}

View file

@ -0,0 +1,17 @@
import { db } from '../db/db_index';
import { deliverySlotInfo } from '../db/schema';
import { eq, gte, and } from 'drizzle-orm';
export async function getAvailableSlots(): Promise<any[]> {
const now = new Date();
return await db.query.deliverySlotInfo.findMany({
where: gte(deliverySlotInfo.freezeTime, now),
orderBy: deliverySlotInfo.deliveryTime,
});
}
export async function getSlotById(id: number): Promise<any | null> {
return await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, id),
});
}

View file

@ -0,0 +1,20 @@
import { db } from '../db/db_index';
import { storeInfo } from '../db/schema';
import { eq } from 'drizzle-orm';
export async function getAllStores(): Promise<any[]> {
return await db.query.storeInfo.findMany({
with: {
owner: true,
},
});
}
export async function getStoreById(id: number): Promise<any | null> {
return await db.query.storeInfo.findFirst({
where: eq(storeInfo.id, id),
with: {
owner: true,
},
});
}

View file

@ -0,0 +1,28 @@
import { db } from '../db/db_index';
import { productTags } from '../db/schema';
import { eq } from 'drizzle-orm';
export async function getAllTags(): Promise<any[]> {
return await db.query.productTags.findMany({
with: {
products: {
with: {
product: true,
},
},
},
});
}
export async function getTagById(id: number): Promise<any | null> {
return await db.query.productTags.findFirst({
where: eq(productTags.id, id),
with: {
products: {
with: {
product: true,
},
},
},
});
}

View file

@ -0,0 +1,44 @@
import { db } from '../db/db_index';
import { users, userDetails, addresses } from '../db/schema';
import { eq, desc } from 'drizzle-orm';
export async function getCurrentUser(userId: number): Promise<any | null> {
return await db.query.users.findFirst({
where: eq(users.id, userId),
with: {
userDetails: true,
},
});
}
export async function updateUser(userId: number, updates: any): Promise<any> {
const [user] = await db.update(users)
.set(updates)
.where(eq(users.id, userId))
.returning();
return user;
}
export async function getUserAddresses(userId: number): Promise<any[]> {
return await db.query.addresses.findMany({
where: eq(addresses.userId, userId),
orderBy: desc(addresses.isDefault),
});
}
export async function createAddress(addressData: any): Promise<any> {
const [address] = await db.insert(addresses).values(addressData).returning();
return address;
}
export async function updateAddress(addressId: number, updates: any): Promise<any> {
const [address] = await db.update(addresses)
.set(updates)
.where(eq(addresses.id, addressId))
.returning();
return address;
}
export async function deleteAddress(addressId: number): Promise<void> {
await db.delete(addresses).where(eq(addresses.id, addressId));
}

View file

@ -0,0 +1,713 @@
// Admin-related types
export interface Banner {
id: number;
name: string;
imageUrl: string;
description: string | null;
productIds: number[] | null;
redirectUrl: string | null;
serialNum: number | null;
isActive: boolean;
createdAt: Date;
lastUpdated: Date;
}
export interface Complaint {
id: number;
complaintBody: string;
userId: number;
orderId: number | null;
isResolved: boolean;
response: string | null;
createdAt: Date;
images: string[] | null;
}
export interface ComplaintWithUser extends Complaint {
userName: string | null;
userMobile: string | null;
}
export interface Constant {
key: string;
value: any;
}
export interface ConstantUpdateResult {
success: boolean;
updatedCount: number;
keys: string[];
}
export interface Coupon {
id: number;
couponCode: string;
isUserBased: boolean;
discountPercent: string | null;
flatDiscount: string | null;
minOrder: string | null;
productIds: number[] | null;
maxValue: string | null;
isApplyForAll: boolean;
validTill: Date | null;
maxLimitForUser: number | null;
exclusiveApply: boolean;
isInvalidated: boolean;
createdAt: Date;
createdBy: number;
}
export interface CouponValidationResult {
valid: boolean;
message?: string;
discountAmount?: number;
coupon?: Partial<Coupon>;
}
export interface UserMiniInfo {
id: number;
name: string;
mobile: string | null;
}
export interface Store {
id: number;
name: string;
description: string | null;
imageUrl: string | null;
owner: number;
createdAt: Date;
// updatedAt: Date;
}
export interface StaffUser {
id: number;
name: string;
password: string;
staffRoleId: number;
createdAt: Date;
}
export interface StaffRole {
id: number;
roleName: string;
}
export interface AdminOrderRow {
id: number;
userId: number;
addressId: number;
slotId: number | null;
isCod: boolean;
isOnlinePayment: boolean;
paymentInfoId: number | null;
totalAmount: string;
deliveryCharge: string;
readableId: number;
adminNotes: string | null;
userNotes: string | null;
orderGroupId: string | null;
orderGroupProportion: string | null;
isFlashDelivery: boolean;
createdAt: Date;
}
export type PaymentStatus = 'pending' | 'success' | 'cod' | 'failed'
export type RefundStatus = 'success' | 'pending' | 'failed' | 'none' | 'na' | 'processed'
export interface AdminOrderStatusRecord {
id: number;
orderTime: Date;
userId: number;
orderId: number;
isPackaged: boolean;
isDelivered: boolean;
isCancelled: boolean;
cancelReason: string | null;
isCancelledByAdmin: boolean | null;
paymentStatus: PaymentStatus;
cancellationUserNotes: string | null;
cancellationAdminNotes: string | null;
cancellationReviewed: boolean;
cancellationReviewedAt: Date | null;
refundCouponId: number | null;
}
export interface AdminRefundRecord {
id: number;
orderId: number;
refundAmount: string | null;
refundStatus: RefundStatus | null;
merchantRefundId: string | null;
refundProcessedAt: Date | null;
createdAt: Date;
}
export interface AdminOrderDetailsAddress {
name: string;
line1: string;
line2: string | null;
city: string;
state: string;
pincode: string;
phone: string;
}
export interface AdminOrderDetailsSlotInfo {
time: string;
sequence: unknown;
}
export interface AdminOrderDetailsItem {
id: number;
name: string;
quantity: string;
productSize: number;
price: string;
unit?: string;
amount: number;
isPackaged: boolean;
isPackageVerified: boolean;
}
export interface AdminOrderDetailsPayment {
status: string;
gateway: string;
merchantOrderId: string;
}
export interface AdminOrderDetailsCouponData {
couponCode: string;
couponDescription: string;
discountAmount: number;
}
export interface AdminOrderDetails {
id: number;
readableId: number;
userId: number;
customerName: string;
customerEmail: string | null;
customerMobile: string | null;
address: AdminOrderDetailsAddress;
slotInfo: AdminOrderDetailsSlotInfo | null;
isCod: boolean;
isOnlinePayment: boolean;
totalAmount: number;
deliveryCharge: number;
adminNotes: string | null;
userNotes: string | null;
createdAt: Date;
status: 'pending' | 'delivered' | 'cancelled';
isPackaged: boolean;
isDelivered: boolean;
items: AdminOrderDetailsItem[];
payment: AdminOrderDetailsPayment | null;
paymentInfo: AdminOrderDetailsPayment | null;
cancelReason: string | null;
cancellationReviewed: boolean;
isRefundDone: boolean;
refundStatus: RefundStatus | null;
refundAmount: number | null;
couponData: AdminOrderDetailsCouponData | null;
couponCode: string | null;
couponDescription: string | null;
discountAmount: number | null;
orderStatus: AdminOrderStatusRecord | null;
refundRecord: AdminRefundRecord | null;
isFlashDelivery: boolean;
}
export interface AdminOrderUpdateResult {
success: boolean;
userId: number | null;
}
export interface AdminOrderItemPackagingResult {
success: boolean;
updated: boolean;
}
export interface AdminOrderMessageResult {
success: boolean;
message: string;
}
export interface AdminOrderBasicResult {
success: boolean;
}
export interface AdminSlotOrderItem {
id: number;
name: string;
quantity: number;
price: number;
amount: number;
unit: string;
isPackaged: boolean;
isPackageVerified: boolean;
}
export interface AdminSlotOrder {
id: number;
readableId: number;
customerName: string;
address: string;
addressId: number;
latitude: number | null;
longitude: number | null;
totalAmount: number;
items: AdminSlotOrderItem[];
deliveryTime: string | null;
status: 'pending' | 'delivered' | 'cancelled';
isPackaged: boolean;
isDelivered: boolean;
isCod: boolean;
paymentMode: 'COD' | 'Online';
paymentStatus: PaymentStatus;
slotId: number | null;
adminNotes: string | null;
userNotes: string | null;
}
export interface AdminGetSlotOrdersResult {
success: boolean;
data: AdminSlotOrder[];
}
export interface AdminOrderListItemProduct {
id: number;
name: string;
quantity: number;
price: number;
amount: number;
unit: string;
productSize: number;
isPackaged: boolean;
isPackageVerified: boolean;
}
export interface AdminOrderListItem {
id: number;
orderId: string;
readableId: number;
customerName: string;
customerMobile: string | null;
address: string;
addressId: number;
latitude: number | null;
longitude: number | null;
totalAmount: number;
deliveryCharge: number;
items: AdminOrderListItemProduct[];
createdAt: Date;
deliveryTime: string | null;
status: 'pending' | 'delivered' | 'cancelled';
isPackaged: boolean;
isDelivered: boolean;
isCod: boolean;
isFlashDelivery: boolean;
userNotes: string | null;
adminNotes: string | null;
userNegativityScore: number;
}
export interface AdminOrderListItemWithUserId extends AdminOrderListItem {
userId: number;
}
export interface AdminGetAllOrdersResult {
orders: AdminOrderListItem[];
nextCursor?: number;
}
export interface AdminGetAllOrdersResultWithUserId {
orders: AdminOrderListItemWithUserId[];
nextCursor?: number;
}
export interface AdminRebalanceSlotsResult {
success: boolean;
updatedOrders: number[];
message: string;
}
export type AdminCancelOrderError =
| 'order_not_found'
| 'status_not_found'
| 'already_cancelled'
| 'already_delivered'
export interface AdminCancelOrderResult {
success: boolean;
message: string;
orderId?: number;
userId?: number;
error?: AdminCancelOrderError;
}
export interface AdminUnit {
id: number;
shortNotation: string;
fullName: string;
}
export interface AdminProduct {
id: number;
name: string;
shortDescription: string | null;
longDescription: string | null;
unitId: number;
price: string;
marketPrice: string | null;
images: string[] | null;
isOutOfStock: boolean;
isSuspended: boolean;
isFlashAvailable: boolean;
flashPrice: string | null;
createdAt: Date;
incrementStep: number;
productQuantity: number;
storeId: number | null;
}
export interface AdminProductWithRelations extends AdminProduct {
unit: AdminUnit;
store: Store | null;
}
export interface AdminProductTagInfo {
id: number;
tagName: string;
tagDescription: string | null;
imageUrl: string | null;
isDashboardTag: boolean;
relatedStores: unknown;
createdAt: Date;
}
export interface AdminProductTagAssignment {
productId: number;
tagId: number;
assignedAt: Date;
product: AdminProduct;
}
export interface AdminProductTagWithProducts extends AdminProductTagInfo {
products: AdminProductTagAssignment[];
}
export interface AdminSpecialDeal {
id: number;
productId: number;
quantity: string;
price: string;
validTill: Date;
}
export interface AdminProductWithDetails extends AdminProduct {
unit: AdminUnit;
deals: AdminSpecialDeal[];
tags: AdminProductTagInfo[];
}
export interface AdminProductListResponse {
products: AdminProductWithRelations[];
count: number;
}
export interface AdminProductResponse {
product: AdminProductWithDetails;
}
export interface AdminDeleteProductResult {
message: string;
}
export interface AdminToggleOutOfStockResult {
product: AdminProduct;
message: string;
}
export interface AdminUpdateSlotProductsResult {
message: string;
added: number;
removed: number;
}
export interface AdminSlotProductIdsResult {
productIds: number[];
}
export type AdminSlotsProductIdsResult = Record<number, number[]>;
export interface AdminProductReview {
id: number;
reviewBody: string;
ratings: number;
imageUrls: unknown;
reviewTime: Date;
adminResponse: string | null;
adminResponseImages: unknown;
userName: string | null;
}
export interface AdminProductReviewWithSignedUrls extends AdminProductReview {
signedImageUrls: string[];
signedAdminImageUrls: string[];
}
export interface AdminProductReviewsResult {
reviews: AdminProductReviewWithSignedUrls[];
hasMore: boolean;
}
export interface AdminProductReviewResponse {
success: boolean;
review: AdminProductReview;
}
export interface AdminProductGroup {
id: number;
groupName: string;
description: string | null;
createdAt: Date;
products: AdminProduct[];
productCount: number;
}
export interface AdminProductGroupsResult {
groups: AdminProductGroup[];
}
export interface AdminProductGroupResponse {
group: AdminProductGroupInfo;
message: string;
}
export interface AdminProductGroupInfo {
id: number;
groupName: string;
description: string | null;
createdAt: Date;
}
export interface AdminUpdateProductPricesResult {
message: string;
updatedCount: number;
}
export interface AdminDeliverySlot {
id: number;
deliveryTime: Date;
freezeTime: Date;
isActive: boolean;
isFlash: boolean;
isCapacityFull: boolean;
deliverySequence: unknown;
groupIds: unknown;
}
export interface AdminSlotProductSummary {
id: number;
name: string;
images: string[] | null;
}
export interface AdminVendorSnippet {
id: number;
snippetCode: string;
slotId: number | null;
productIds: number[];
isPermanent: boolean;
validTill: Date | null;
createdAt: Date;
}
export interface AdminVendorSnippetWithAccess extends AdminVendorSnippet {
accessUrl: string;
}
export interface AdminVendorSnippetWithSlot extends AdminVendorSnippet {
slot: AdminDeliverySlot | null;
}
export interface AdminVendorSnippetProduct {
id: number;
name: string;
}
export interface AdminVendorSnippetWithProducts extends AdminVendorSnippetWithSlot {
accessUrl: string;
products: AdminVendorSnippetProduct[];
}
export interface AdminSlotWithProducts extends AdminDeliverySlot {
deliverySequence: number[];
products: AdminSlotProductSummary[];
}
export interface AdminSlotWithProductsAndSnippets extends AdminSlotWithProducts {
groupIds: number[];
vendorSnippets: AdminVendorSnippetWithAccess[];
}
export interface AdminSlotWithProductsAndSnippetsBase extends AdminSlotWithProducts {
groupIds: number[];
vendorSnippets: AdminVendorSnippet[];
}
export interface AdminSlotsResult {
slots: AdminSlotWithProducts[];
count: number;
}
export interface AdminSlotsListResult {
slots: AdminDeliverySlot[];
count: number;
}
export interface AdminSlotResult {
slot: AdminSlotWithProductsAndSnippets;
}
export interface AdminSlotCreateResult {
slot: AdminDeliverySlot;
createdSnippets: AdminVendorSnippet[];
message: string;
}
export interface AdminSlotUpdateResult {
slot: AdminDeliverySlot;
createdSnippets: AdminVendorSnippet[];
message: string;
}
export interface AdminSlotDeleteResult {
message: string;
}
export type AdminDeliverySequence = Record<string, number[]>;
export interface AdminDeliverySequenceResult {
deliverySequence: AdminDeliverySequence;
}
export interface AdminUpdateDeliverySequenceResult {
slot: {
id: number;
deliverySequence: unknown;
};
message: string;
}
export interface AdminUpdateSlotCapacityResult {
success: boolean;
slot: AdminDeliverySlot;
message: string;
}
export interface AdminVendorSnippetCreateInput {
snippetCode: string;
slotId?: number;
productIds: number[];
validTill?: string;
isPermanent: boolean;
}
export interface AdminVendorSnippetUpdateInput {
snippetCode?: string;
slotId?: number;
productIds?: number[];
validTill?: string | null;
isPermanent?: boolean;
}
export interface AdminVendorSnippetDeleteResult {
message: string;
}
export interface AdminVendorSnippetOrderProduct {
orderItemId: number;
productId: number;
productName: string;
quantity: number;
productSize: number;
price: number;
unit: string;
subtotal: number;
is_packaged: boolean;
is_package_verified: boolean;
}
export interface AdminVendorSnippetOrderSummary {
orderId: string;
orderDate: string;
customerName: string;
totalAmount: number;
slotInfo: {
time: string;
sequence: unknown;
} | null;
products: AdminVendorSnippetOrderProduct[];
matchedProducts: number[];
snippetCode: string;
}
export interface AdminVendorSnippetOrdersResult {
success: boolean;
data: AdminVendorSnippetOrderSummary[];
snippet: {
id: number;
snippetCode: string;
slotId: number | null;
productIds: number[];
validTill?: string;
createdAt: string;
isPermanent: boolean;
};
}
export interface AdminVendorSnippetOrdersWithSlotResult extends AdminVendorSnippetOrdersResult {
selectedSlot: {
id: number;
deliveryTime: string;
freezeTime: string;
deliverySequence: unknown;
};
}
export interface AdminVendorOrderSummary {
id: number;
status: string;
orderDate: string;
totalQuantity: number;
products: {
name: string;
quantity: number;
unit: string;
}[];
}
export interface AdminUpcomingSlotsResult {
success: boolean;
data: {
id: number;
deliveryTime: string;
freezeTime: string;
deliverySequence: unknown;
}[];
}
export type AdminVendorUpdatePackagingResult =
| {
success: true;
orderItemId: number;
is_packaged: boolean;
}
| {
success: false;
message: string;
}

View file

@ -1,9 +1,5 @@
// Central types export file
// Re-export all types from the types folder
export type { Banner } from './banner.types';
export type { Complaint, ComplaintWithUser } from './complaint.types';
export type { Constant, ConstantUpdateResult } from './const.types';
export type { Coupon, ReservedCoupon, CouponValidationResult, UserMiniInfo } from './coupon.types';
export type { Store } from './store.types';
export type { StaffUser, StaffRole } from './staff-user.types';
export type * from './admin';
export type * from './user';

View file

@ -0,0 +1,82 @@
// User-related types
export interface User {
id: number;
name: string | null;
email: string | null;
mobile: string;
createdAt: Date;
}
export interface UserDetails {
id: number;
userId: number;
isSuspended: boolean;
profileImage: string | null;
}
export interface Address {
id: number;
userId: number;
addressLine1: string;
addressLine2: string | null;
city: string;
state: string;
pincode: string;
lat: number | null;
lng: number | null;
isDefault: boolean;
}
export interface Product {
id: number;
name: string;
description: string | null;
price: string;
productQuantity: string;
images: string[] | null;
isOutOfStock: boolean;
storeId: number | null;
unitId: number;
}
export interface CartItem {
id: number;
userId: number;
productId: number;
quantity: string;
}
export interface Order {
id: number;
userId: number;
addressId: number;
slotId: number | null;
totalAmount: string;
deliveryCharge: string;
isFlashDelivery: boolean;
adminNotes: string | null;
isPackaged: boolean;
isDelivered: boolean;
isCancelled: boolean;
createdAt: Date;
}
export interface OrderItem {
id: number;
orderId: number;
productId: number;
quantity: string;
price: string;
is_packaged: boolean;
is_package_verified: boolean;
}
export interface Payment {
id: number;
orderId: number;
paymentStatus: string;
paymentMethod: string;
amount: string;
createdAt: Date;
}