From d4afa75eaf721996126a45e6f5ed913705bf44a7 Mon Sep 17 00:00:00 2001 From: shafi54 <108669266+shafi-aviz@users.noreply.github.com> Date: Fri, 20 Feb 2026 11:40:59 +0530 Subject: [PATCH] enh --- .../favicon-48.png | Bin 0 -> 2919 bytes .../src/services/user/order-service.ts | 405 ++++++++++++++++++ .../src/services/user/product-service.ts | 138 ++++++ apps/backend/src/trpc/user-apis/slots.ts | 6 +- packages/ui/index.ts | 38 +- 5 files changed, 565 insertions(+), 22 deletions(-) create mode 100644 apps/admin-ui/.expo/web/cache/production/images/favicon/favicon-24272cdaeff82cc5facdaccd982a6f05b60c4504704bbf94c19a6388659880bb-contain-transparent/favicon-48.png create mode 100644 apps/backend/src/services/user/order-service.ts create mode 100644 apps/backend/src/services/user/product-service.ts diff --git a/apps/admin-ui/.expo/web/cache/production/images/favicon/favicon-24272cdaeff82cc5facdaccd982a6f05b60c4504704bbf94c19a6388659880bb-contain-transparent/favicon-48.png b/apps/admin-ui/.expo/web/cache/production/images/favicon/favicon-24272cdaeff82cc5facdaccd982a6f05b60c4504704bbf94c19a6388659880bb-contain-transparent/favicon-48.png new file mode 100644 index 0000000000000000000000000000000000000000..c99b4f2291c114216521d2da5eb3b8f7de2383ea GIT binary patch literal 2919 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 literal 0 HcmV?d00001 diff --git a/apps/backend/src/services/user/order-service.ts b/apps/backend/src/services/user/order-service.ts new file mode 100644 index 0000000..22a54c9 --- /dev/null +++ b/apps/backend/src/services/user/order-service.ts @@ -0,0 +1,405 @@ +import { db } from '../../db/db_index' +import { + orders, + orderItems, + orderStatus, + addresses, + productInfo, + paymentInfoTable, + coupons, + couponUsage, + payments, + cartItems, + refunds, + units, + userDetails, +} from '../../db/schema' +import { eq, and, inArray, desc, gte } from 'drizzle-orm' + +// ============ User/Auth Queries ============ + +/** + * Get user details by user ID + */ +export async function getUserDetails(userId: number) { + return db.query.userDetails.findFirst({ + where: eq(userDetails.userId, userId), + }) +} + +// ============ Address Queries ============ + +/** + * Get user address by ID + */ +export async function getUserAddress(userId: number, addressId: number) { + return db.query.addresses.findFirst({ + where: and(eq(addresses.userId, userId), eq(addresses.id, addressId)), + }) +} + +// ============ Product Queries ============ + +/** + * Get product by ID + */ +export async function getProductById(productId: number) { + return db.query.productInfo.findFirst({ + where: eq(productInfo.id, productId), + }) +} + +/** + * Get multiple products by IDs with unit info + */ +export async function getProductsByIdsWithUnits(productIds: number[]) { + return db + .select({ + id: productInfo.id, + name: productInfo.name, + shortDescription: productInfo.shortDescription, + price: productInfo.price, + images: productInfo.images, + isOutOfStock: productInfo.isOutOfStock, + unitShortNotation: units.shortNotation, + incrementStep: productInfo.incrementStep, + }) + .from(productInfo) + .innerJoin(units, eq(productInfo.unitId, units.id)) + .where(and(inArray(productInfo.id, productIds), eq(productInfo.isSuspended, false))) + .orderBy(desc(productInfo.createdAt)) +} + +// ============ Coupon Queries ============ + +/** + * Get coupon with usages for user + */ +export async function getCouponWithUsages(couponId: number, userId: number) { + return db.query.coupons.findFirst({ + where: eq(coupons.id, couponId), + with: { + usages: { where: eq(couponUsage.userId, userId) }, + }, + }) +} + +/** + * Insert coupon usage + */ +export async function insertCouponUsage(data: { + userId: number + couponId: number + orderId: number + orderItemId: number | null + usedAt: Date +}) { + return db.insert(couponUsage).values(data) +} + +/** + * Get coupon usages for order + */ +export async function getCouponUsagesForOrder(orderId: number) { + return db.query.couponUsage.findMany({ + where: eq(couponUsage.orderId, orderId), + with: { + coupon: true, + }, + }) +} + +// ============ Cart Queries ============ + +/** + * Delete cart items for user by product IDs + */ +export async function deleteCartItems(userId: number, productIds: number[]) { + return db.delete(cartItems).where( + and( + eq(cartItems.userId, userId), + inArray(cartItems.productId, productIds) + ) + ) +} + +// ============ Payment Info Queries ============ + +/** + * Create payment info + */ +export async function createPaymentInfo(data: { + status: string + gateway: string + merchantOrderId: string +}) { + return db.insert(paymentInfoTable).values(data).returning() +} + + + +// ============ Order Queries ============ + +/** + * Insert multiple orders + */ +export async function insertOrders(ordersData: any[]) { + return db.insert(orders).values(ordersData).returning() +} + +/** + * Insert multiple order items + */ +export async function insertOrderItems(itemsData: any[]) { + return db.insert(orderItems).values(itemsData) +} + +/** + * Insert multiple order statuses + */ +export async function insertOrderStatuses(statusesData: any[]) { + return db.insert(orderStatus).values(statusesData) +} + +/** + * Get user orders with all relations + */ +export async function getUserOrdersWithRelations(userId: number, limit: number, offset: number) { + return db.query.orders.findMany({ + where: eq(orders.userId, userId), + with: { + orderItems: { + with: { + product: true, + }, + }, + slot: true, + paymentInfo: true, + orderStatus: true, + refunds: true, + }, + orderBy: (orders, { desc }) => [desc(orders.createdAt)], + limit: limit, + offset: offset, + }) +} + +/** + * Count user orders + */ +export async function countUserOrders(userId: number) { + return db.$count(orders, eq(orders.userId, userId)) +} + +/** + * Get order by ID with all relations + */ +export async function getOrderByIdWithRelations(orderId: number) { + return db.query.orders.findFirst({ + where: eq(orders.id, orderId), + with: { + orderItems: { + with: { + product: true, + }, + }, + slot: true, + paymentInfo: true, + orderStatus: { + with: { + refundCoupon: true, + }, + }, + refunds: true, + }, + }) +} + +/** + * Get order by ID with order status + */ +export async function getOrderWithStatus(orderId: number) { + return db.query.orders.findFirst({ + where: eq(orders.id, orderId), + with: { + orderStatus: true, + }, + }) +} + +/** + * Update order status to cancelled + */ +export async function updateOrderStatusToCancelled( + statusId: number, + data: { + isCancelled: boolean + cancelReason: string + cancellationUserNotes: string + cancellationReviewed: boolean + } +) { + return db + .update(orderStatus) + .set(data) + .where(eq(orderStatus.id, statusId)) +} + +/** + * Insert refund record + */ +export async function insertRefund(data: { orderId: number; refundStatus: string }) { + return db.insert(refunds).values(data) +} + +/** + * Update order notes + */ +export async function updateOrderNotes(orderId: number, userNotes: string | null) { + return db + .update(orders) + .set({ userNotes }) + .where(eq(orders.id, orderId)) +} + +/** + * Get recent delivered orders for user + */ +export async function getRecentDeliveredOrders( + userId: number, + since: Date, + limit: number +) { + return db + .select({ id: orders.id }) + .from(orders) + .innerJoin(orderStatus, eq(orders.id, orderStatus.orderId)) + .where( + and( + eq(orders.userId, userId), + eq(orderStatus.isDelivered, true), + gte(orders.createdAt, since) + ) + ) + .orderBy(desc(orders.createdAt)) + .limit(limit) +} + +/** + * Get order items by order IDs + */ +export async function getOrderItemsByOrderIds(orderIds: number[]) { + return db + .select({ productId: orderItems.productId }) + .from(orderItems) + .where(inArray(orderItems.orderId, orderIds)) +} + +// ============ Transaction Helper ============ + +/** + * Execute function within a database transaction + */ +export async function withTransaction(fn: (tx: any) => Promise): Promise { + return db.transaction(fn) +} + +/** + * Cancel order with refund record in a transaction + */ +export async function cancelOrderWithRefund( + statusId: number, + orderId: number, + isCod: boolean, + reason: string +): Promise<{ orderId: number }> { + return db.transaction(async (tx) => { + // Update order status + await tx + .update(orderStatus) + .set({ + isCancelled: true, + cancelReason: reason, + cancellationUserNotes: reason, + cancellationReviewed: false, + }) + .where(eq(orderStatus.id, statusId)) + + // Insert refund record + const refundStatus = isCod ? "na" : "pending" + await tx.insert(refunds).values({ + orderId, + refundStatus, + }) + + return { orderId } + }) +} + +type Tx = Parameters[0]>[0] + +/** + * Create orders with payment info in a transaction + */ +export async function createOrdersWithPayment( + ordersData: any[], + paymentMethod: "online" | "cod", + totalWithDelivery: number, + razorpayOrderCreator?: (paymentInfoId: number, amount: string) => Promise, + paymentRecordInserter?: (paymentInfoId: number, razorpayOrder: any, tx: Tx) => Promise +): Promise { + return db.transaction(async (tx) => { + let sharedPaymentInfoId: number | null = null + if (paymentMethod === "online") { + const [paymentInfo] = await tx + .insert(paymentInfoTable) + .values({ + status: "pending", + gateway: "razorpay", + merchantOrderId: `multi_order_${Date.now()}`, + }) + .returning() + sharedPaymentInfoId = paymentInfo.id + } + + const ordersToInsert: Omit[] = ordersData.map( + (od) => ({ + ...od.order, + paymentInfoId: sharedPaymentInfoId, + }) + ) + + const insertedOrders = await tx.insert(orders).values(ordersToInsert).returning() + + const allOrderItems: Omit[] = [] + const allOrderStatuses: Omit[] = [] + + insertedOrders.forEach((order: typeof orders.$inferSelect, index: number) => { + const od = ordersData[index] + od.orderItems.forEach((item: any) => { + allOrderItems.push({ ...item, orderId: order.id as number }) + }) + allOrderStatuses.push({ + ...od.orderStatus, + orderId: order.id as number, + }) + }) + + await tx.insert(orderItems).values(allOrderItems) + await tx.insert(orderStatus).values(allOrderStatuses) + + if (paymentMethod === "online" && sharedPaymentInfoId && razorpayOrderCreator && paymentRecordInserter) { + const razorpayOrder = await razorpayOrderCreator( + sharedPaymentInfoId, + totalWithDelivery.toString() + ) + await paymentRecordInserter( + sharedPaymentInfoId, + razorpayOrder, + tx + ) + } + + return insertedOrders + }) +} diff --git a/apps/backend/src/services/user/product-service.ts b/apps/backend/src/services/user/product-service.ts new file mode 100644 index 0000000..77f62fa --- /dev/null +++ b/apps/backend/src/services/user/product-service.ts @@ -0,0 +1,138 @@ +import { db } from '../../db/db_index' +import { productInfo, units, productSlots, deliverySlotInfo, specialDeals, storeInfo, productReviews, users } from '../../db/schema' +import { eq, and, gt, sql, desc } from 'drizzle-orm' + +/** + * Get product basic info with unit + */ +export async function getProductWithUnit(productId: number) { + return db + .select({ + id: productInfo.id, + name: productInfo.name, + shortDescription: productInfo.shortDescription, + longDescription: productInfo.longDescription, + price: productInfo.price, + marketPrice: productInfo.marketPrice, + images: productInfo.images, + isOutOfStock: productInfo.isOutOfStock, + storeId: productInfo.storeId, + unitShortNotation: units.shortNotation, + incrementStep: productInfo.incrementStep, + productQuantity: productInfo.productQuantity, + isFlashAvailable: productInfo.isFlashAvailable, + flashPrice: productInfo.flashPrice, + }) + .from(productInfo) + .innerJoin(units, eq(productInfo.unitId, units.id)) + .where(eq(productInfo.id, productId)) + .limit(1) +} + +/** + * Get store info by ID + */ +export async function getStoreById(storeId: number) { + return db.query.storeInfo.findFirst({ + where: eq(storeInfo.id, storeId), + columns: { id: true, name: true, description: true }, + }) +} + +/** + * Get delivery slots for product + */ +export async function getProductDeliverySlots(productId: number) { + return db + .select({ + id: deliverySlotInfo.id, + deliveryTime: deliverySlotInfo.deliveryTime, + freezeTime: deliverySlotInfo.freezeTime, + }) + .from(productSlots) + .innerJoin(deliverySlotInfo, eq(productSlots.slotId, deliverySlotInfo.id)) + .where( + and( + eq(productSlots.productId, productId), + eq(deliverySlotInfo.isActive, true), + gt(deliverySlotInfo.deliveryTime, sql`NOW()`), + gt(deliverySlotInfo.freezeTime, sql`NOW()`) + ) + ) + .orderBy(deliverySlotInfo.deliveryTime) +} + +/** + * Get special deals for product + */ +export async function getProductSpecialDeals(productId: number) { + return db + .select({ + quantity: specialDeals.quantity, + price: specialDeals.price, + validTill: specialDeals.validTill, + }) + .from(specialDeals) + .where( + and( + eq(specialDeals.productId, productId), + gt(specialDeals.validTill, sql`NOW()`) + ) + ) + .orderBy(specialDeals.quantity) +} + +/** + * Get product reviews with user info + */ +export async function getProductReviews(productId: number, limit: number, offset: number) { + return db + .select({ + id: productReviews.id, + reviewBody: productReviews.reviewBody, + ratings: productReviews.ratings, + imageUrls: productReviews.imageUrls, + reviewTime: productReviews.reviewTime, + userName: users.name, + }) + .from(productReviews) + .innerJoin(users, eq(productReviews.userId, users.id)) + .where(eq(productReviews.productId, productId)) + .orderBy(desc(productReviews.reviewTime)) + .limit(limit) + .offset(offset) +} + +/** + * Count reviews for product + */ +export async function countProductReviews(productId: number) { + const result = await db + .select({ count: sql`count(*)` }) + .from(productReviews) + .where(eq(productReviews.productId, productId)) + + return Number(result[0].count) +} + +/** + * Check if product exists + */ +export async function checkProductExists(productId: number) { + return db.query.productInfo.findFirst({ + where: eq(productInfo.id, productId), + }) +} + +/** + * Insert new review + */ +export async function insertReview(data: { + userId: number + productId: number + reviewBody: string + ratings: number + imageUrls: string[] +}) { + return db.insert(productReviews).values(data).returning() +} diff --git a/apps/backend/src/trpc/user-apis/slots.ts b/apps/backend/src/trpc/user-apis/slots.ts index 6e8d2c7..a031a7f 100644 --- a/apps/backend/src/trpc/user-apis/slots.ts +++ b/apps/backend/src/trpc/user-apis/slots.ts @@ -47,9 +47,9 @@ export const slotsRouter = router({ getSlotsWithProducts: publicProcedure.query(async () => { const allSlots = await getAllSlotsFromCache(); const currentTime = new Date(); - const validSlots = allSlots.filter((slot) => - dayjs(slot.freezeTime).isAfter(currentTime) - ); + const validSlots = allSlots + .filter((slot) => dayjs(slot.freezeTime).isAfter(currentTime)) + .sort((a, b) => dayjs(a.deliveryTime).valueOf() - dayjs(b.deliveryTime).valueOf()); return { slots: validSlots, diff --git a/packages/ui/index.ts b/packages/ui/index.ts index 8dac1bf..e320050 100755 --- a/packages/ui/index.ts +++ b/packages/ui/index.ts @@ -63,10 +63,10 @@ const isDevMode = Constants.executionEnvironment !== "standalone"; // const BASE_API_URL = API_URL; // const BASE_API_URL = 'http://10.0.2.2:4000'; // const BASE_API_URL = 'http://192.168.100.101:4000'; -// const BASE_API_URL = 'http://192.168.1.3:4000'; -let BASE_API_URL = "https://mf.freshyo.in"; - // let BASE_API_URL = 'http://192.168.100.104:4000'; - // let BASE_API_URL = 'http://192.168.29.176:4000'; +const BASE_API_URL = 'http://192.168.1.3:4000'; +// let BASE_API_URL = "https://mf.freshyo.in"; +// let BASE_API_URL = 'http://192.168.100.104:4000'; +// let BASE_API_URL = 'http://192.168.29.176:4000'; // if(isDevMode) { // } @@ -87,11 +87,11 @@ export { Theme, MyTextInput, BottomDialog, - LoadingDialog, - MyText, - MyTouchableOpacity, - ConfirmationDialog, - RawBottomDialog, + LoadingDialog, + MyText, + MyTouchableOpacity, + ConfirmationDialog, + RawBottomDialog, DatePicker, BottomDropdown, ImageViewerURI, @@ -105,9 +105,9 @@ export { tw, SearchBar, DataTable, - Quantifier, - MiniQuantifier, - TabViewWrapper, + Quantifier, + MiniQuantifier, + TabViewWrapper, MyFlatList, useFocusCallback, useManualRefresh, @@ -118,10 +118,10 @@ export { useIsDevMode, usePagination, REFUND_STATUS, - DateTimePickerMod, - RefreshProvider, - useRefresh, - MyStatusBar, - updateStatusBarColor, - useStatusBarStore, - }; + DateTimePickerMod, + RefreshProvider, + useRefresh, + MyStatusBar, + updateStatusBarColor, + useStatusBarStore, +};