diff --git a/apps/admin-ui/components/FullOrderView.tsx b/apps/admin-ui/components/FullOrderView.tsx deleted file mode 100644 index 0671492..0000000 --- a/apps/admin-ui/components/FullOrderView.tsx +++ /dev/null @@ -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 = ({ orderId }) => { - const { data: order, isLoading, error } = trpc.admin.order.getFullOrder.useQuery({ orderId }); - - if (isLoading) { - return ( - - Loading order details... - - ); - } - - if (error || !order) { - return ( - - Failed to load order details - - ); - } - - const totalAmount = order.items.reduce((sum, item) => sum + item.amount, 0); - - return ( - - - Order #{order.readableId} - - {/* Customer Information */} - - Customer Details - - - Name: - {order.customerName} - - {order.customerEmail && ( - - Email: - {order.customerEmail} - - )} - - Mobile: - {order.customerMobile} - - - - - {/* Delivery Address */} - - Delivery Address - - {order.address.line1} - {order.address.line2 && {order.address.line2}} - - {order.address.city}, {order.address.state} - {order.address.pincode} - - Phone: {order.address.phone} - - - - {/* Order Details */} - - Order Details - - - Order Date: - - {new Date(order.createdAt).toLocaleDateString('en-IN', { - day: 'numeric', - month: 'short', - year: 'numeric', - hour: '2-digit', - minute: '2-digit' - })} - - - - Payment Method: - - {order.isCod ? 'Cash on Delivery' : 'Online Payment'} - - - {order.slotInfo && ( - - Delivery Slot: - - {new Date(order.slotInfo.time).toLocaleDateString('en-IN', { - day: 'numeric', - month: 'short', - hour: '2-digit', - minute: '2-digit' - })} - - - )} - - - - {/* Items */} - - Items ({order.items.length}) - {order.items.map((item, index) => ( - - - - {item.productName} - - - Qty: {item.quantity} {item.unit} × ₹{parseFloat(item.price.toString()).toFixed(2)} - - - ₹{item.amount.toFixed(2)} - - ))} - - - {/* Payment Information */} - {(order.payment || order.paymentInfo) && ( - - Payment Information - {order.payment && ( - - Payment Details: - - Status: - {order.payment.status} - - - Gateway: - {order.payment.gateway} - - - Order ID: - {order.payment.merchantOrderId} - - - )} - {order.paymentInfo && ( - - Payment Info: - - Status: - {order.paymentInfo.status} - - - Gateway: - {order.paymentInfo.gateway} - - - Order ID: - {order.paymentInfo.merchantOrderId} - - - )} - - )} - - {/* User Notes */} - {order.userNotes && ( - - Customer Notes - {order.userNotes} - - )} - - {/* Admin Notes */} - {order.adminNotes && ( - - Admin Notes - {order.adminNotes} - - )} - - {/* Total */} - - - Total Amount - ₹{parseFloat(order.totalAmount.toString()).toFixed(2)} - - - - - ); -}; \ No newline at end of file diff --git a/apps/backend/src/dbService.ts b/apps/backend/src/dbService.ts index caa8552..cd29594 100644 --- a/apps/backend/src/dbService.ts +++ b/apps/backend/src/dbService.ts @@ -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, @@ -121,20 +123,57 @@ export { 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 { + 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, +} from '@packages/shared'; + +export type { + // User types + User, + UserDetails, + Address, + Product, + CartItem, + Order, + OrderItem, + Payment, +} from '@packages/shared'; diff --git a/apps/backend/src/trpc/apis/admin-apis/apis/order.ts b/apps/backend/src/trpc/apis/admin-apis/apis/order.ts index 707f642..e7b947b 100644 --- a/apps/backend/src/trpc/apis/admin-apis/apis/order.ts +++ b/apps/backend/src/trpc/apis/admin-apis/apis/order.ts @@ -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 => { 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 => { 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 => { 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 => { 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 => { 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 => { 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 => { 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 => { 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 => { 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 => { 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 => { 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 { + 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 { await tx.delete(complaints).where(eq(complaints.orderId, orderId)); await tx.delete(orders).where(eq(orders.id, orderId)); }); + */ } - diff --git a/apps/backend/src/trpc/apis/admin-apis/apis/staff-user.ts b/apps/backend/src/trpc/apis/admin-apis/apis/staff-user.ts index aee10f1..0c777ac 100644 --- a/apps/backend/src/trpc/apis/admin-apis/apis/staff-user.ts +++ b/apps/backend/src/trpc/apis/admin-apis/apis/staff-user.ts @@ -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 }; }), diff --git a/packages/db_helper_postgres/index.ts b/packages/db_helper_postgres/index.ts index 6069737..18be345 100644 --- a/packages/db_helper_postgres/index.ts +++ b/packages/db_helper_postgres/index.ts @@ -7,15 +7,161 @@ 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, + createProduct, + updateProduct, + toggleProductOutOfStock, + getAllUnits, + getAllProductTags, + getProductReviews, + respondToReview, + getAllProductGroups, + createProductGroup, + updateProductGroup, + deleteProductGroup, + addProductToGroup, + removeProductFromGroup, +} from './src/admin-apis/product'; + +export { + // Slots + getAllSlots, + getSlotById, + createSlot, + updateSlot, + deleteSlot, + getSlotProducts, + addProductToSlot, + removeProductFromSlot, + clearSlotProducts, + 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, +} 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' diff --git a/packages/db_helper_postgres/src/admin-apis/banner.ts b/packages/db_helper_postgres/src/admin-apis/banner.ts new file mode 100644 index 0000000..e32c7f7 --- /dev/null +++ b/packages/db_helper_postgres/src/admin-apis/banner.ts @@ -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 { + 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 { + 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; + +export async function createBanner(input: CreateBannerInput): Promise { + 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>; + +export async function updateBanner(id: number, input: UpdateBannerInput): Promise { + 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 { + await db.delete(homeBanners).where(eq(homeBanners.id, id)); +} diff --git a/packages/db_helper_postgres/src/admin-apis/complaint.ts b/packages/db_helper_postgres/src/admin-apis/complaint.ts new file mode 100644 index 0000000..039e081 --- /dev/null +++ b/packages/db_helper_postgres/src/admin-apis/complaint.ts @@ -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 { + await db + .update(complaints) + .set({ isResolved: true, response }) + .where(eq(complaints.id, id)); +} diff --git a/packages/db_helper_postgres/src/admin-apis/const.ts b/packages/db_helper_postgres/src/admin-apis/const.ts new file mode 100644 index 0000000..9d3ba86 --- /dev/null +++ b/packages/db_helper_postgres/src/admin-apis/const.ts @@ -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 { + const constants = await db.select().from(keyValStore); + + return constants.map(c => ({ + key: c.key, + value: c.value, + })); +} + +export async function upsertConstants(constants: Constant[]): Promise { + await db.transaction(async (tx) => { + for (const { key, value } of constants) { + await tx.insert(keyValStore) + .values({ key, value }) + .onConflictDoUpdate({ + target: keyValStore.key, + set: { value }, + }); + } + }); +} diff --git a/packages/db_helper_postgres/src/admin-apis/coupon.ts b/packages/db_helper_postgres/src/admin-apis/coupon.ts new file mode 100644 index 0000000..1ab5fbf --- /dev/null +++ b/packages/db_helper_postgres/src/admin-apis/coupon.ts @@ -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 { + 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 { + 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 { + 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 { + 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; +} + +export async function validateCoupon( + code: string, + userId: number, + orderAmount: number +): Promise { + 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 { + 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 { + 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 { + const existing = await db.query.coupons.findFirst({ + where: eq(coupons.couponCode, couponCode), + }); + return !!existing; +} + +export async function checkReservedCouponExists(secretCode: string): Promise { + 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 { + 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 { + 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, + })) + }; +} diff --git a/packages/db_helper_postgres/src/admin-apis/order.ts b/packages/db_helper_postgres/src/admin-apis/order.ts new file mode 100644 index 0000000..8cc8cf0 --- /dev/null +++ b/packages/db_helper_postgres/src/admin-apis/order.ts @@ -0,0 +1,681 @@ +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' + +export async function updateOrderNotes(orderId: number, adminNotes: string | null): Promise { + 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 { + 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 { + 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 { + // 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 + ? (statusRecord as AdminOrderStatusRecord) + : 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 + ? (refund.refundStatus as 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 { + 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 { + 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 { + 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, + })) + + 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: (order.isCod ? 'COD' : 'Online') as 'COD' | 'Online', + paymentStatus: (statusRecord?.paymentStatus || 'pending') as PaymentStatus, + 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 { + 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 { + const { + cursor, + limit, + slotId, + packagedFilter, + deliveredFilter, + cancellationFilter, + flashDeliveryFilter, + } = input + + let whereCondition: SQL | 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 { + 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 { + 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 { + 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)) + }) +} diff --git a/packages/db_helper_postgres/src/admin-apis/product.ts b/packages/db_helper_postgres/src/admin-apis/product.ts new file mode 100644 index 0000000..13c4b51 --- /dev/null +++ b/packages/db_helper_postgres/src/admin-apis/product.ts @@ -0,0 +1,130 @@ +import { db } from '../db/db_index'; +import { productInfo, units, specialDeals, productSlots, productTags, productReviews, productGroupInfo, productGroupMembership } from '../db/schema'; +import { eq, and, inArray, desc, sql, asc } from 'drizzle-orm'; + +export async function getAllProducts(): Promise { + return await db.query.productInfo.findMany({ + orderBy: productInfo.name, + with: { + unit: true, + store: true, + }, + }); +} + +export async function getProductById(id: number): Promise { + return await db.query.productInfo.findFirst({ + where: eq(productInfo.id, id), + with: { + unit: true, + store: true, + productSlots: { + with: { + slot: true, + }, + }, + specialDeals: true, + productTags: { + with: { + tag: true, + }, + }, + }, + }); +} + +export async function createProduct(input: any): Promise { + const [product] = await db.insert(productInfo).values(input).returning(); + return product; +} + +export async function updateProduct(id: number, updates: any): Promise { + const [product] = await db.update(productInfo) + .set(updates) + .where(eq(productInfo.id, id)) + .returning(); + return product; +} + +export async function toggleProductOutOfStock(id: number, isOutOfStock: boolean): Promise { + const [product] = await db.update(productInfo) + .set({ isOutOfStock }) + .where(eq(productInfo.id, id)) + .returning(); + return product; +} + +export async function getAllUnits(): Promise { + return await db.query.units.findMany({ + orderBy: units.name, + }); +} + +export async function getAllProductTags(): Promise { + return await db.query.productTags.findMany({ + with: { + products: { + with: { + product: true, + }, + }, + }, + }); +} + +export async function getProductReviews(productId: number): Promise { + return await db.query.productReviews.findMany({ + where: eq(productReviews.productId, productId), + with: { + user: true, + }, + orderBy: desc(productReviews.createdAt), + }); +} + +export async function respondToReview(reviewId: number, adminResponse: string): Promise { + await db.update(productReviews) + .set({ adminResponse }) + .where(eq(productReviews.id, reviewId)); +} + +export async function getAllProductGroups(): Promise { + return await db.query.productGroupInfo.findMany({ + with: { + products: { + with: { + product: true, + }, + }, + }, + }); +} + +export async function createProductGroup(name: string): Promise { + const [group] = await db.insert(productGroupInfo).values({ name }).returning(); + return group; +} + +export async function updateProductGroup(id: number, name: string): Promise { + const [group] = await db.update(productGroupInfo) + .set({ name }) + .where(eq(productGroupInfo.id, id)) + .returning(); + return group; +} + +export async function deleteProductGroup(id: number): Promise { + await db.delete(productGroupInfo).where(eq(productGroupInfo.id, id)); +} + +export async function addProductToGroup(groupId: number, productId: number): Promise { + await db.insert(productGroupMembership).values({ groupId, productId }); +} + +export async function removeProductFromGroup(groupId: number, productId: number): Promise { + await db.delete(productGroupMembership) + .where(and( + eq(productGroupMembership.groupId, groupId), + eq(productGroupMembership.productId, productId) + )); +} diff --git a/packages/db_helper_postgres/src/admin-apis/slots.ts b/packages/db_helper_postgres/src/admin-apis/slots.ts new file mode 100644 index 0000000..d6a70cd --- /dev/null +++ b/packages/db_helper_postgres/src/admin-apis/slots.ts @@ -0,0 +1,95 @@ +import { db } from '../db/db_index'; +import { deliverySlotInfo, productSlots, productInfo } from '../db/schema'; +import { eq, and, inArray, desc } from 'drizzle-orm'; + +export async function getAllSlots(): Promise { + return await db.query.deliverySlotInfo.findMany({ + orderBy: desc(deliverySlotInfo.createdAt), + with: { + productSlots: { + with: { + product: true, + }, + }, + }, + }); +} + +export async function getSlotById(id: number): Promise { + return await db.query.deliverySlotInfo.findFirst({ + where: eq(deliverySlotInfo.id, id), + with: { + productSlots: { + with: { + product: true, + }, + }, + }, + }); +} + +export async function createSlot(input: any): Promise { + const [slot] = await db.insert(deliverySlotInfo).values(input).returning(); + return slot; +} + +export async function updateSlot(id: number, updates: any): Promise { + const [slot] = await db.update(deliverySlotInfo) + .set(updates) + .where(eq(deliverySlotInfo.id, id)) + .returning(); + return slot; +} + +export async function deleteSlot(id: number): Promise { + await db.delete(deliverySlotInfo).where(eq(deliverySlotInfo.id, id)); +} + +export async function getSlotProducts(slotId: number): Promise { + return await db.query.productSlots.findMany({ + where: eq(productSlots.slotId, slotId), + with: { + product: true, + }, + }); +} + +export async function addProductToSlot(slotId: number, productId: number): Promise { + await db.insert(productSlots).values({ slotId, productId }); +} + +export async function removeProductFromSlot(slotId: number, productId: number): Promise { + await db.delete(productSlots) + .where(and( + eq(productSlots.slotId, slotId), + eq(productSlots.productId, productId) + )); +} + +export async function clearSlotProducts(slotId: number): Promise { + await db.delete(productSlots).where(eq(productSlots.slotId, slotId)); +} + +export async function updateSlotCapacity(slotId: number, maxCapacity: number): Promise { + const [slot] = await db.update(deliverySlotInfo) + .set({ maxCapacity }) + .where(eq(deliverySlotInfo.id, slotId)) + .returning(); + return slot; +} + +export async function getSlotDeliverySequence(slotId: number): Promise { + const slot = await db.query.deliverySlotInfo.findFirst({ + where: eq(deliverySlotInfo.id, slotId), + columns: { + deliverySequence: true, + }, + }); + return slot?.deliverySequence || null; +} + +export async function updateSlotDeliverySequence(slotId: number, sequence: any): Promise { + await db.update(deliverySlotInfo) + .set({ deliverySequence: sequence }) + .where(eq(deliverySlotInfo.id, slotId)); +} diff --git a/packages/db_helper_postgres/src/admin-apis/staff-user.ts b/packages/db_helper_postgres/src/admin-apis/staff-user.ts new file mode 100644 index 0000000..2b51b9f --- /dev/null +++ b/packages/db_helper_postgres/src/admin-apis/staff-user.ts @@ -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; + createdAt: Date; +} + +export async function getStaffUserByName(name: string): Promise { + const staff = await db.query.staffUsers.findFirst({ + where: eq(staffUsers.name, name), + }); + + return staff || null; +} + +export async function getAllStaff(): Promise { + 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 { + 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 { + await db + .insert(userDetails) + .values({ userId, isSuspended }) + .onConflictDoUpdate({ + target: userDetails.userId, + set: { isSuspended }, + }); +} + +export async function checkStaffUserExists(name: string): Promise { + const existingUser = await db.query.staffUsers.findFirst({ + where: eq(staffUsers.name, name), + }); + return !!existingUser; +} + +export async function checkStaffRoleExists(roleId: number): Promise { + 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 { + 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 { + const roles = await db.query.staffRoles.findMany({ + columns: { + id: true, + roleName: true, + }, + }); + + return roles; +} diff --git a/packages/db_helper_postgres/src/admin-apis/store.ts b/packages/db_helper_postgres/src/admin-apis/store.ts new file mode 100644 index 0000000..d326f67 --- /dev/null +++ b/packages/db_helper_postgres/src/admin-apis/store.ts @@ -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 { + const stores = await db.query.storeInfo.findMany({ + with: { + owner: true, + }, + }); + + return stores; +} + +export async function getStoreById(id: number): Promise { + 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 { + 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 { + 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", + }; + }); +} diff --git a/packages/db_helper_postgres/src/admin-apis/user.ts b/packages/db_helper_postgres/src/admin-apis/user.ts new file mode 100644 index 0000000..2feb965 --- /dev/null +++ b/packages/db_helper_postgres/src/admin-apis/user.ts @@ -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 { + const [newUser] = await db + .insert(users) + .values({ + name: null, + email: null, + mobile, + }) + .returning(); + + return newUser; +} + +export async function getUserByMobile(mobile: string): Promise { + const [existingUser] = await db + .select() + .from(users) + .where(eq(users.mobile, mobile)) + .limit(1); + + return existingUser || null; +} + +export async function getUnresolvedComplaintsCount(): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + const [incident] = await db.insert(userIncidents) + .values({ + userId, + orderId, + adminComment, + addedBy: adminUserId, + negativityScore, + }) + .returning(); + + return incident; +} diff --git a/packages/db_helper_postgres/src/admin-apis/vendor-snippets.ts b/packages/db_helper_postgres/src/admin-apis/vendor-snippets.ts new file mode 100644 index 0000000..8cec6a6 --- /dev/null +++ b/packages/db_helper_postgres/src/admin-apis/vendor-snippets.ts @@ -0,0 +1,130 @@ +import { db } from '../db/db_index'; +import { vendorSnippets, deliverySlotInfo, productInfo, orders, orderItems, orderStatus } from '../db/schema'; +import { eq, and, inArray, gt, sql, asc } from 'drizzle-orm'; + +export async function checkVendorSnippetExists(snippetCode: string): Promise { + const existingSnippet = await db.query.vendorSnippets.findFirst({ + where: eq(vendorSnippets.snippetCode, snippetCode), + }); + return !!existingSnippet; +} + +export async function getVendorSnippetById(id: number): Promise { + return await db.query.vendorSnippets.findFirst({ + where: eq(vendorSnippets.id, id), + with: { + slot: true, + }, + }); +} + +export async function getVendorSnippetByCode(snippetCode: string): Promise { + return await db.query.vendorSnippets.findFirst({ + where: eq(vendorSnippets.snippetCode, snippetCode), + }); +} + +export async function getAllVendorSnippets(): Promise { + return await db.query.vendorSnippets.findMany({ + with: { + slot: true, + }, + orderBy: (vendorSnippets, { desc }) => [desc(vendorSnippets.createdAt)], + }); +} + +export interface CreateVendorSnippetInput { + snippetCode: string; + slotId?: number; + productIds: number[]; + isPermanent: boolean; + validTill?: Date; +} + +export async function createVendorSnippet(input: CreateVendorSnippetInput): Promise { + const [result] = await db.insert(vendorSnippets).values({ + snippetCode: input.snippetCode, + slotId: input.slotId, + productIds: input.productIds, + isPermanent: input.isPermanent, + validTill: input.validTill, + }).returning(); + + return result; +} + +export async function updateVendorSnippet(id: number, updates: any): Promise { + const [result] = await db.update(vendorSnippets) + .set(updates) + .where(eq(vendorSnippets.id, id)) + .returning(); + + return result; +} + +export async function deleteVendorSnippet(id: number): Promise { + await db.delete(vendorSnippets) + .where(eq(vendorSnippets.id, id)); +} + +export async function getProductsByIds(productIds: number[]): Promise { + return await db.query.productInfo.findMany({ + where: inArray(productInfo.id, productIds), + columns: { id: true, name: true }, + }); +} + +export async function getVendorSlotById(slotId: number): Promise { + return await db.query.deliverySlotInfo.findFirst({ + where: eq(deliverySlotInfo.id, slotId), + }); +} + +export async function getVendorOrdersBySlotId(slotId: number): Promise { + 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 getOrderItemsByOrderIds(orderIds: number[]): Promise { + return await db.query.orderItems.findMany({ + where: inArray(orderItems.orderId, orderIds), + with: { + product: { + with: { + unit: true, + }, + }, + }, + }); +} + +export async function getOrderStatusByOrderIds(orderIds: number[]): Promise { + return await db.query.orderStatus.findMany({ + where: inArray(orderStatus.orderId, orderIds), + }); +} + +export async function updateVendorOrderItemPackaging(orderItemId: number, isPackaged: boolean, isPackageVerified: boolean): Promise { + await db.update(orderItems) + .set({ + is_packaged: isPackaged, + is_package_verified: isPackageVerified, + }) + .where(eq(orderItems.id, orderItemId)); +} diff --git a/packages/db_helper_postgres/src/common-apis/utils.ts b/packages/db_helper_postgres/src/common-apis/utils.ts new file mode 100644 index 0000000..f41a32c --- /dev/null +++ b/packages/db_helper_postgres/src/common-apis/utils.ts @@ -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; +} diff --git a/packages/db_helper_postgres/src/user-apis/address.ts b/packages/db_helper_postgres/src/user-apis/address.ts new file mode 100644 index 0000000..9428831 --- /dev/null +++ b/packages/db_helper_postgres/src/user-apis/address.ts @@ -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 { + const zones = await db.select().from(addressZones).orderBy(desc(addressZones.addedAt)); + return zones; +} + +export async function getAreas(): Promise { + const areas = await db.select().from(addressAreas).orderBy(desc(addressAreas.createdAt)); + return areas; +} + +export async function createZone(zoneName: string): Promise { + const [zone] = await db.insert(addressZones).values({ zoneName }).returning(); + return zone; +} + +export async function createArea(placeName: string, zoneId: number | null): Promise { + const [area] = await db.insert(addressAreas).values({ placeName, zoneId }).returning(); + return area; +} diff --git a/packages/db_helper_postgres/src/user-apis/auth.ts b/packages/db_helper_postgres/src/user-apis/auth.ts new file mode 100644 index 0000000..ea56fa9 --- /dev/null +++ b/packages/db_helper_postgres/src/user-apis/auth.ts @@ -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 { + return await db.query.users.findFirst({ + where: eq(users.mobile, mobile), + }); +} + +export async function createUser(userData: any): Promise { + const [user] = await db.insert(users).values(userData).returning(); + return user; +} diff --git a/packages/db_helper_postgres/src/user-apis/banners.ts b/packages/db_helper_postgres/src/user-apis/banners.ts new file mode 100644 index 0000000..f062cbd --- /dev/null +++ b/packages/db_helper_postgres/src/user-apis/banners.ts @@ -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 { + const banners = await db.query.homeBanners.findMany({ + where: eq(homeBanners.isActive, true), + orderBy: desc(homeBanners.createdAt), + }); + return banners; +} diff --git a/packages/db_helper_postgres/src/user-apis/cart.ts b/packages/db_helper_postgres/src/user-apis/cart.ts new file mode 100644 index 0000000..a398987 --- /dev/null +++ b/packages/db_helper_postgres/src/user-apis/cart.ts @@ -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 { + 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 { + const [item] = await db.insert(cartItems).values({ + userId, + productId, + quantity, + }).returning(); + return item; +} + +export async function updateCartItem(itemId: number, quantity: number): Promise { + const [item] = await db.update(cartItems) + .set({ quantity }) + .where(eq(cartItems.id, itemId)) + .returning(); + return item; +} + +export async function removeFromCart(itemId: number): Promise { + await db.delete(cartItems).where(eq(cartItems.id, itemId)); +} + +export async function clearCart(userId: number): Promise { + await db.delete(cartItems).where(eq(cartItems.userId, userId)); +} diff --git a/packages/db_helper_postgres/src/user-apis/complaint.ts b/packages/db_helper_postgres/src/user-apis/complaint.ts new file mode 100644 index 0000000..314a273 --- /dev/null +++ b/packages/db_helper_postgres/src/user-apis/complaint.ts @@ -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 { + 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 { + const [complaint] = await db.insert(complaints).values({ + userId, + orderId, + complaintBody, + images, + isResolved: false, + }).returning(); + return complaint; +} diff --git a/packages/db_helper_postgres/src/user-apis/coupon.ts b/packages/db_helper_postgres/src/user-apis/coupon.ts new file mode 100644 index 0000000..cf167c6 --- /dev/null +++ b/packages/db_helper_postgres/src/user-apis/coupon.ts @@ -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 { + 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 { + return await db.query.coupons.findMany({ + where: eq(coupons.userId, userId), + }); +} diff --git a/packages/db_helper_postgres/src/user-apis/order.ts b/packages/db_helper_postgres/src/user-apis/order.ts new file mode 100644 index 0000000..a36bd7c --- /dev/null +++ b/packages/db_helper_postgres/src/user-apis/order.ts @@ -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 { + 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 { + 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 { + 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; + }); +} diff --git a/packages/db_helper_postgres/src/user-apis/payments.ts b/packages/db_helper_postgres/src/user-apis/payments.ts new file mode 100644 index 0000000..e09233a --- /dev/null +++ b/packages/db_helper_postgres/src/user-apis/payments.ts @@ -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 { + const [payment] = await db.insert(payments).values(paymentData).returning(); + return payment; +} + +export async function updatePaymentStatus(paymentId: number, status: string): Promise { + const [payment] = await db.update(payments) + .set({ paymentStatus: status }) + .where(eq(payments.id, paymentId)) + .returning(); + return payment; +} + +export async function getPaymentByOrderId(orderId: number): Promise { + return await db.query.payments.findFirst({ + where: eq(payments.orderId, orderId), + }); +} diff --git a/packages/db_helper_postgres/src/user-apis/product.ts b/packages/db_helper_postgres/src/user-apis/product.ts new file mode 100644 index 0000000..a2f4ecd --- /dev/null +++ b/packages/db_helper_postgres/src/user-apis/product.ts @@ -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 { + return await db.query.productInfo.findMany({ + with: { + unit: true, + store: true, + specialDeals: true, + }, + orderBy: productInfo.name, + }); +} + +export async function getProductById(id: number): Promise { + 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 { + const [review] = await db.insert(productReviews).values({ + userId, + productId, + rating, + comment, + }).returning(); + return review; +} diff --git a/packages/db_helper_postgres/src/user-apis/slots.ts b/packages/db_helper_postgres/src/user-apis/slots.ts new file mode 100644 index 0000000..f01a97f --- /dev/null +++ b/packages/db_helper_postgres/src/user-apis/slots.ts @@ -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 { + 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 { + return await db.query.deliverySlotInfo.findFirst({ + where: eq(deliverySlotInfo.id, id), + }); +} diff --git a/packages/db_helper_postgres/src/user-apis/stores.ts b/packages/db_helper_postgres/src/user-apis/stores.ts new file mode 100644 index 0000000..24feccf --- /dev/null +++ b/packages/db_helper_postgres/src/user-apis/stores.ts @@ -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 { + return await db.query.storeInfo.findMany({ + with: { + owner: true, + }, + }); +} + +export async function getStoreById(id: number): Promise { + return await db.query.storeInfo.findFirst({ + where: eq(storeInfo.id, id), + with: { + owner: true, + }, + }); +} diff --git a/packages/db_helper_postgres/src/user-apis/tags.ts b/packages/db_helper_postgres/src/user-apis/tags.ts new file mode 100644 index 0000000..bbd09eb --- /dev/null +++ b/packages/db_helper_postgres/src/user-apis/tags.ts @@ -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 { + return await db.query.productTags.findMany({ + with: { + products: { + with: { + product: true, + }, + }, + }, + }); +} + +export async function getTagById(id: number): Promise { + return await db.query.productTags.findFirst({ + where: eq(productTags.id, id), + with: { + products: { + with: { + product: true, + }, + }, + }, + }); +} diff --git a/packages/db_helper_postgres/src/user-apis/user.ts b/packages/db_helper_postgres/src/user-apis/user.ts new file mode 100644 index 0000000..defc0df --- /dev/null +++ b/packages/db_helper_postgres/src/user-apis/user.ts @@ -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 { + return await db.query.users.findFirst({ + where: eq(users.id, userId), + with: { + userDetails: true, + }, + }); +} + +export async function updateUser(userId: number, updates: any): Promise { + const [user] = await db.update(users) + .set(updates) + .where(eq(users.id, userId)) + .returning(); + return user; +} + +export async function getUserAddresses(userId: number): Promise { + return await db.query.addresses.findMany({ + where: eq(addresses.userId, userId), + orderBy: desc(addresses.isDefault), + }); +} + +export async function createAddress(addressData: any): Promise { + const [address] = await db.insert(addresses).values(addressData).returning(); + return address; +} + +export async function updateAddress(addressId: number, updates: any): Promise { + const [address] = await db.update(addresses) + .set(updates) + .where(eq(addresses.id, addressId)) + .returning(); + return address; +} + +export async function deleteAddress(addressId: number): Promise { + await db.delete(addresses).where(eq(addresses.id, addressId)); +} diff --git a/packages/shared/types/admin.ts b/packages/shared/types/admin.ts new file mode 100644 index 0000000..c79880d --- /dev/null +++ b/packages/shared/types/admin.ts @@ -0,0 +1,349 @@ +// 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; +} + +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; +} diff --git a/packages/shared/types/index.ts b/packages/shared/types/index.ts index 6f8996d..e1116f4 100644 --- a/packages/shared/types/index.ts +++ b/packages/shared/types/index.ts @@ -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'; diff --git a/packages/shared/types/user.ts b/packages/shared/types/user.ts new file mode 100644 index 0000000..0c257b7 --- /dev/null +++ b/packages/shared/types/user.ts @@ -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; +}