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 0000000..c99b4f2 Binary files /dev/null and b/apps/admin-ui/.expo/web/cache/production/images/favicon/favicon-24272cdaeff82cc5facdaccd982a6f05b60c4504704bbf94c19a6388659880bb-contain-transparent/favicon-48.png differ 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, +};