From fe057693431d36fb2cf045c106ff51b0f64e98e9 Mon Sep 17 00:00:00 2001 From: shafi54 <108669266+shafi-aviz@users.noreply.github.com> Date: Thu, 26 Mar 2026 00:34:31 +0530 Subject: [PATCH] enh --- apps/backend/src/dbService.ts | 141 ++++ .../src/trpc/apis/user-apis/apis/address.ts | 134 +++- .../src/trpc/apis/user-apis/apis/auth.ts | 235 +++---- .../src/trpc/apis/user-apis/apis/banners.ts | 21 +- .../src/trpc/apis/user-apis/apis/cart.ts | 128 ++-- .../src/trpc/apis/user-apis/apis/complaint.ts | 32 +- .../src/trpc/apis/user-apis/apis/coupon.ts | 117 ++-- .../src/trpc/apis/user-apis/apis/order.ts | 478 +++---------- .../src/trpc/apis/user-apis/apis/payments.ts | 99 ++- .../src/trpc/apis/user-apis/apis/product.ts | 191 ++---- .../src/trpc/apis/user-apis/apis/slots.ts | 40 +- .../src/trpc/apis/user-apis/apis/stores.ts | 143 ++-- .../src/trpc/apis/user-apis/apis/user.ts | 152 ++--- packages/db_helper_postgres/index.ts | 124 +++- .../src/user-apis/address.ts | 155 ++++- .../db_helper_postgres/src/user-apis/auth.ts | 138 +++- .../src/user-apis/banners.ts | 34 +- .../db_helper_postgres/src/user-apis/cart.ts | 112 ++- .../src/user-apis/complaint.ts | 46 +- .../src/user-apis/coupon.ts | 171 ++++- .../db_helper_postgres/src/user-apis/order.ts | 639 +++++++++++++++++- .../src/user-apis/payments.ts | 63 +- .../src/user-apis/product.ts | 206 +++++- .../db_helper_postgres/src/user-apis/slots.ts | 55 +- .../src/user-apis/stores.ts | 139 +++- .../db_helper_postgres/src/user-apis/tags.ts | 20 +- .../db_helper_postgres/src/user-apis/user.ts | 93 ++- packages/shared/types/user.ts | 563 +++++++++++++++ 28 files changed, 3189 insertions(+), 1280 deletions(-) diff --git a/apps/backend/src/dbService.ts b/apps/backend/src/dbService.ts index 76f8e04..8615e8e 100644 --- a/apps/backend/src/dbService.ts +++ b/apps/backend/src/dbService.ts @@ -125,6 +125,61 @@ export { updateSlotCapacity, getSlotDeliverySequence, updateSlotDeliverySequence, + // User address methods + getUserDefaultAddress, + getUserAddresses, + getUserAddressById, + clearUserDefaultAddress, + createUserAddress, + updateUserAddress, + deleteUserAddress, + hasOngoingOrdersForAddress, + getUserActiveBanners, + getUserCartItemsWithProducts, + getUserProductById, + getUserCartItemByUserProduct, + incrementUserCartItemQuantity, + insertUserCartItem, + updateUserCartItemQuantity, + deleteUserCartItem, + clearUserCart, + getUserComplaints, + createUserComplaint, + getUserStoreSummaries, + getUserStoreDetail, + getUserProductDetailById, + getUserProductReviews, + getUserProductByIdBasic, + createUserProductReview, + getUserActiveSlotsList, + getUserProductAvailability, + getUserPaymentOrderById, + getUserPaymentByOrderId, + getUserPaymentByMerchantOrderId, + updateUserPaymentSuccess, + updateUserOrderPaymentStatus, + markUserPaymentFailed, + getUserAuthByEmail, + getUserAuthByMobile, + getUserAuthById, + getUserAuthCreds, + getUserAuthDetails, + createUserAuthWithCreds, + createUserAuthWithMobile, + upsertUserAuthPassword, + deleteUserAuthAccount, + getUserActiveCouponsWithRelations, + getUserAllCouponsWithRelations, + getUserReservedCouponByCode, + redeemUserReservedCoupon, + getUserProfileById, + getUserProfileDetailById, + getUserWithCreds, + getUserNotifCred, + upsertUserNotifCred, + deleteUserUnloggedToken, + getUserUnloggedToken, + upsertUserUnloggedToken, // Order methods updateOrderNotes, updateOrderPackaged, @@ -137,6 +192,26 @@ export { rebalanceSlots, cancelOrder, deleteOrderById, + // User Order helpers + validateAndGetUserCoupon, + applyDiscountToUserOrder, + getUserAddressByIdAndUser, + getOrderProductById, + checkUserSuspended, + getUserSlotCapacityStatus, + placeUserOrderTransaction, + deleteUserCartItemsForOrder, + recordUserCouponUsage, + getUserOrdersWithRelations, + getUserOrderCount, + getUserOrderByIdWithRelations, + getUserCouponUsageForOrder, + getUserOrderBasic, + cancelUserOrderTransaction, + updateUserOrderNotes, + getUserRecentlyDeliveredOrderIds, + getUserProductIdsFromOrders, + getUserProductsForRecentOrders, } from 'postgresService' export async function getOrderDetails(orderId: number): Promise { @@ -220,6 +295,72 @@ export type { AdminVendorOrderSummary, AdminUpcomingSlotsResult, AdminVendorUpdatePackagingResult, + UserAddress, + UserAddressResponse, + UserAddressesResponse, + UserAddressDeleteResponse, + UserBanner, + UserBannersResponse, + UserCartProduct, + UserCartItem, + UserCartResponse, + UserComplaint, + UserComplaintsResponse, + UserRaiseComplaintResponse, + UserStoreSummary, + UserStoreSummaryData, + UserStoresResponse, + UserStoreSampleProduct, + UserStoreSampleProductData, + UserStoreDetail, + UserStoreDetailData, + UserStoreProduct, + UserStoreProductData, + UserTagSummary, + UserProductDetail, + UserProductDetailData, + UserProductReview, + UserProductReviewWithSignedUrls, + UserProductReviewsResponse, + UserCreateReviewResponse, + UserSlotProduct, + UserSlotWithProducts, + UserSlotData, + UserSlotAvailability, + UserDeliverySlot, + UserSlotsResponse, + UserSlotsWithProductsResponse, + UserSlotsListResponse, + UserPaymentOrderResponse, + UserPaymentVerifyResponse, + UserPaymentFailResponse, + UserAuthProfile, + UserAuthResponse, + UserAuthResult, + UserOtpVerifyResponse, + UserPasswordUpdateResponse, + UserProfileResponse, + UserDeleteAccountResponse, + UserCouponUsage, + UserCouponApplicableUser, + UserCouponApplicableProduct, + UserCoupon, + UserCouponWithRelations, + UserEligibleCouponsResponse, + UserCouponDisplay, + UserMyCouponsResponse, + UserRedeemCouponResponse, + UserSelfDataResponse, + UserProfileCompleteResponse, + UserSavePushTokenResponse, + UserOrderItemSummary, + UserOrderSummary, + UserOrdersResponse, + UserOrderDetail, + UserCancelOrderResponse, + UserUpdateNotesResponse, + UserRecentProduct, + UserRecentProductsResponse, } from '@packages/shared'; export type { diff --git a/apps/backend/src/trpc/apis/user-apis/apis/address.ts b/apps/backend/src/trpc/apis/user-apis/apis/address.ts index 0022c7e..56a6a95 100644 --- a/apps/backend/src/trpc/apis/user-apis/apis/address.ts +++ b/apps/backend/src/trpc/apis/user-apis/apis/address.ts @@ -1,30 +1,52 @@ -import { router, protectedProcedure } from '@/src/trpc/trpc-index'; -import { z } from 'zod'; -import { db } from '@/src/db/db_index'; -import { addresses, orders, orderStatus, deliverySlotInfo } from '@/src/db/schema'; -import { eq, and, gte } from 'drizzle-orm'; -import dayjs from 'dayjs'; -import { extractCoordsFromRedirectUrl } from '@/src/lib/license-util'; +import { router, protectedProcedure } from '@/src/trpc/trpc-index' +import { z } from 'zod' +import { extractCoordsFromRedirectUrl } from '@/src/lib/license-util' +import { + getUserDefaultAddress as getDefaultAddressInDb, + getUserAddresses as getUserAddressesInDb, + getUserAddressById as getUserAddressByIdInDb, + clearUserDefaultAddress as clearDefaultAddressInDb, + createUserAddress as createUserAddressInDb, + updateUserAddress as updateUserAddressInDb, + deleteUserAddress as deleteUserAddressInDb, + hasOngoingOrdersForAddress as hasOngoingOrdersForAddressInDb, +} from '@/src/dbService' +import type { + UserAddressResponse, + UserAddressesResponse, + UserAddressDeleteResponse, +} from '@packages/shared' export const addressRouter = router({ getDefaultAddress: protectedProcedure - .query(async ({ ctx }) => { + .query(async ({ ctx }): Promise => { const userId = ctx.user.userId; + const defaultAddress = await getDefaultAddressInDb(userId) + + /* + // Old implementation - direct DB queries: const [defaultAddress] = await db .select() .from(addresses) .where(and(eq(addresses.userId, userId), eq(addresses.isDefault, true))) .limit(1); + */ - return { success: true, data: defaultAddress || null }; + return { success: true, data: defaultAddress } }), getUserAddresses: protectedProcedure - .query(async ({ ctx }) => { + .query(async ({ ctx }): Promise => { const userId = ctx.user.userId; + const userAddresses = await getUserAddressesInDb(userId) + + /* + // Old implementation - direct DB queries: const userAddresses = await db.select().from(addresses).where(eq(addresses.userId, userId)); - return { success: true, data: userAddresses }; + */ + + return { success: true, data: userAddresses } }), createAddress: protectedProcedure @@ -41,7 +63,7 @@ export const addressRouter = router({ longitude: z.number().optional(), googleMapsUrl: z.string().optional(), })) - .mutation(async ({ input, ctx }) => { + .mutation(async ({ input, ctx }): Promise => { const userId = ctx.user.userId; const { name, phone, addressLine1, addressLine2, city, state, pincode, isDefault, googleMapsUrl } = input; @@ -61,6 +83,27 @@ export const addressRouter = router({ } // If setting as default, unset other defaults + if (isDefault) { + await clearDefaultAddressInDb(userId) + } + + const newAddress = await createUserAddressInDb({ + userId, + name, + phone, + addressLine1, + addressLine2, + city, + state, + pincode, + isDefault: isDefault || false, + latitude, + longitude, + googleMapsUrl, + }) + + /* + // Old implementation - direct DB queries: if (isDefault) { await db.update(addresses).set({ isDefault: false }).where(eq(addresses.userId, userId)); } @@ -79,8 +122,9 @@ export const addressRouter = router({ longitude, googleMapsUrl, }).returning(); + */ - return { success: true, data: newAddress }; + return { success: true, data: newAddress } }), updateAddress: protectedProcedure @@ -98,7 +142,7 @@ export const addressRouter = router({ longitude: z.number().optional(), googleMapsUrl: z.string().optional(), })) - .mutation(async ({ input, ctx }) => { + .mutation(async ({ input, ctx }): Promise => { const userId = ctx.user.userId; const { id, name, phone, addressLine1, addressLine2, city, state, pincode, isDefault, googleMapsUrl } = input; @@ -113,12 +157,34 @@ export const addressRouter = router({ } // Check if address exists and belongs to user - const existingAddress = await db.select().from(addresses).where(and(eq(addresses.id, id), eq(addresses.userId, userId))).limit(1); - if (existingAddress.length === 0) { - throw new Error('Address not found'); + const existingAddress = await getUserAddressByIdInDb(userId, id) + if (!existingAddress) { + throw new Error('Address not found') } // If setting as default, unset other defaults + if (isDefault) { + await clearDefaultAddressInDb(userId) + } + + const updatedAddress = await updateUserAddressInDb({ + userId, + addressId: id, + name, + phone, + addressLine1, + addressLine2, + city, + state, + pincode, + isDefault: isDefault || false, + googleMapsUrl, + latitude, + longitude, + }) + + /* + // Old implementation - direct DB queries: if (isDefault) { await db.update(addresses).set({ isDefault: false }).where(eq(addresses.userId, userId)); } @@ -143,25 +209,42 @@ export const addressRouter = router({ } const [updatedAddress] = await db.update(addresses).set(updateData).where(and(eq(addresses.id, id), eq(addresses.userId, userId))).returning(); + */ - return { success: true, data: updatedAddress }; + return { success: true, data: updatedAddress } }), deleteAddress: protectedProcedure .input(z.object({ id: z.number().int().positive(), })) - .mutation(async ({ input, ctx }) => { + .mutation(async ({ input, ctx }): Promise => { const userId = ctx.user.userId; const { id } = input; - // Check if address exists and belongs to user + const existingAddress = await getUserAddressByIdInDb(userId, id) + if (!existingAddress) { + throw new Error('Address not found or does not belong to user') + } + + const hasOngoingOrders = await hasOngoingOrdersForAddressInDb(id) + if (hasOngoingOrders) { + throw new Error('Address is attached to an ongoing order. Please cancel the order first.') + } + + if (existingAddress.isDefault) { + throw new Error('Cannot delete default address. Please set another address as default first.') + } + + const deleted = await deleteUserAddressInDb(userId, id) + + /* + // Old implementation - direct DB queries: const existingAddress = await db.select().from(addresses).where(and(eq(addresses.id, id), eq(addresses.userId, userId))).limit(1); if (existingAddress.length === 0) { throw new Error('Address not found or does not belong to user'); } - // Check if address is attached to any ongoing orders using joins const ongoingOrders = await db.select({ order: orders, status: orderStatus, @@ -181,14 +264,17 @@ export const addressRouter = router({ throw new Error('Address is attached to an ongoing order. Please cancel the order first.'); } - // Prevent deletion of default address if (existingAddress[0].isDefault) { throw new Error('Cannot delete default address. Please set another address as default first.'); } - // Delete the address await db.delete(addresses).where(and(eq(addresses.id, id), eq(addresses.userId, userId))); + */ - return { success: true, message: 'Address deleted successfully' }; + if (!deleted) { + throw new Error('Address not found or does not belong to user') + } + + return { success: true, message: 'Address deleted successfully' } }), }); diff --git a/apps/backend/src/trpc/apis/user-apis/apis/auth.ts b/apps/backend/src/trpc/apis/user-apis/apis/auth.ts index 56e9a3c..c028e3d 100644 --- a/apps/backend/src/trpc/apis/user-apis/apis/auth.ts +++ b/apps/backend/src/trpc/apis/user-apis/apis/auth.ts @@ -1,23 +1,33 @@ -import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index'; -import { z } from 'zod'; -import bcrypt from 'bcryptjs'; -import jwt from 'jsonwebtoken'; -import { eq } from 'drizzle-orm'; -import { db } from '@/src/db/db_index'; -import { - users, userCreds, userDetails, addresses, cartItems, complaints, - couponApplicableUsers, couponUsage, notifCreds, notifications, - orderItems, orderStatus, orders, payments, refunds, - productReviews, reservedCoupons -} from '@/src/db/schema'; -import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client'; -import { ApiError } from '@/src/lib/api-error'; -import catchAsync from '@/src/lib/catch-async'; -import { jwtSecret } from '@/src/lib/env-exporter'; -import { sendOtp, verifyOtpUtil, getOtpCreds } from '@/src/lib/otp-utils'; +import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index' +import { z } from 'zod' +import bcrypt from 'bcryptjs' +import jwt from 'jsonwebtoken' +import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client' +import { ApiError } from '@/src/lib/api-error' +import { jwtSecret } from '@/src/lib/env-exporter' +import { sendOtp, verifyOtpUtil, getOtpCreds } from '@/src/lib/otp-utils' +import { + getUserAuthByEmail as getUserAuthByEmailInDb, + getUserAuthByMobile as getUserAuthByMobileInDb, + getUserAuthById as getUserAuthByIdInDb, + getUserAuthCreds as getUserAuthCredsInDb, + getUserAuthDetails as getUserAuthDetailsInDb, + createUserAuthWithCreds as createUserAuthWithCredsInDb, + createUserAuthWithMobile as createUserAuthWithMobileInDb, + upsertUserAuthPassword as upsertUserAuthPasswordInDb, + deleteUserAuthAccount as deleteUserAuthAccountInDb, +} from '@/src/dbService' +import type { + UserAuthResult, + UserAuthResponse, + UserOtpVerifyResponse, + UserPasswordUpdateResponse, + UserProfileResponse, + UserDeleteAccountResponse, +} from '@packages/shared' interface LoginRequest { - identifier: string; // email or mobile + identifier: string; password: string; } @@ -28,22 +38,6 @@ interface RegisterRequest { password: string; } -interface AuthResponse { - token: string; - user: { - id: number; - name?: string | null; - email: string | null; - mobile: string | null; - createdAt: string; - profileImage: string | null; - bio?: string | null; - dateOfBirth?: string | null; - gender?: string | null; - occupation?: string | null; - }; -} - const generateToken = (userId: number): string => { const secret = jwtSecret; if (!secret) { @@ -61,7 +55,7 @@ export const authRouter = router({ identifier: z.string().min(1, 'Email/mobile is required'), password: z.string().min(1, 'Password is required'), })) - .mutation(async ({ input }) => { + .mutation(async ({ input }): Promise => { const { identifier, password }: LoginRequest = input; if (!identifier || !password) { @@ -69,22 +63,13 @@ export const authRouter = router({ } // Find user by email or mobile - const [user] = await db - .select() - .from(users) - .where(eq(users.email, identifier.toLowerCase())) - .limit(1); - - let foundUser = user; + const user = await getUserAuthByEmailInDb(identifier.toLowerCase()) + let foundUser = user || null if (!foundUser) { // Try mobile if email didn't work - const [userByMobile] = await db - .select() - .from(users) - .where(eq(users.mobile, identifier)) - .limit(1); - foundUser = userByMobile; + const userByMobile = await getUserAuthByMobileInDb(identifier) + foundUser = userByMobile || null } if (!foundUser) { @@ -92,22 +77,14 @@ export const authRouter = router({ } // Get user credentials - const [userCredentials] = await db - .select() - .from(userCreds) - .where(eq(userCreds.userId, foundUser.id)) - .limit(1); + const userCredentials = await getUserAuthCredsInDb(foundUser.id) if (!userCredentials) { throw new ApiError('Account setup incomplete. Please contact support.', 401); } // Get user details for profile image - const [userDetail] = await db - .select() - .from(userDetails) - .where(eq(userDetails.userId, foundUser.id)) - .limit(1); + const userDetail = await getUserAuthDetailsInDb(foundUser.id) // Generate signed URL for profile image if it exists const profileImageSignedUrl = userDetail?.profileImage @@ -122,7 +99,7 @@ export const authRouter = router({ const token = generateToken(foundUser.id); - const response: AuthResponse = { + const response: UserAuthResponse = { token, user: { id: foundUser.id, @@ -141,7 +118,7 @@ export const authRouter = router({ return { success: true, data: response, - }; + } }), register: publicProcedure @@ -151,7 +128,7 @@ export const authRouter = router({ mobile: z.string().min(1, 'Mobile is required'), password: z.string().min(1, 'Password is required'), })) - .mutation(async ({ input }) => { + .mutation(async ({ input }): Promise => { const { name, email, mobile, password }: RegisterRequest = input; if (!name || !email || !mobile || !password) { @@ -171,22 +148,14 @@ export const authRouter = router({ } // Check if email already exists - const [existingEmail] = await db - .select() - .from(users) - .where(eq(users.email, email.toLowerCase())) - .limit(1); + const existingEmail = await getUserAuthByEmailInDb(email.toLowerCase()) if (existingEmail) { throw new ApiError('Email already registered', 409); } // Check if mobile already exists - const [existingMobile] = await db - .select() - .from(users) - .where(eq(users.mobile, cleanMobile)) - .limit(1); + const existingMobile = await getUserAuthByMobileInDb(cleanMobile) if (existingMobile) { throw new ApiError('Mobile number already registered', 409); @@ -196,31 +165,16 @@ export const authRouter = router({ const hashedPassword = await bcrypt.hash(password, 12); // Create user and credentials in a transaction - const newUser = await db.transaction(async (tx) => { - // Create user - const [user] = await tx - .insert(users) - .values({ - name: name.trim(), - email: email.toLowerCase().trim(), - mobile: cleanMobile, - }) - .returning(); - - // Create user credentials - await tx - .insert(userCreds) - .values({ - userId: user.id, - userPassword: hashedPassword, - }); - - return user; - }); + const newUser = await createUserAuthWithCredsInDb({ + name: name.trim(), + email: email.toLowerCase().trim(), + mobile: cleanMobile, + hashedPassword, + }) const token = generateToken(newUser.id); - const response: AuthResponse = { + const response: UserAuthResponse = { token, user: { id: newUser.id, @@ -235,7 +189,7 @@ export const authRouter = router({ return { success: true, data: response, - }; + } }), sendOtp: publicProcedure @@ -252,7 +206,7 @@ export const authRouter = router({ mobile: z.string(), otp: z.string(), })) - .mutation(async ({ input }) => { + .mutation(async ({ input }): Promise => { const verificationId = getOtpCreds(input.mobile); if (!verificationId) { throw new ApiError("OTP not sent or expired", 400); @@ -264,45 +218,35 @@ export const authRouter = router({ } // Find user - let user = await db.query.users.findFirst({ - where: eq(users.mobile, input.mobile), - }); + let user = await getUserAuthByMobileInDb(input.mobile) // If user doesn't exist, create one - if (!user) { - const [newUser] = await db - .insert(users) - .values({ - name: null, - email: null, - mobile: input.mobile, - }) - .returning(); - user = newUser; - } + if (!user) { + user = await createUserAuthWithMobileInDb(input.mobile) + } // Generate JWT const token = generateToken(user.id); - return { - success: true, - token, - user: { - id: user.id, - name: user.name, - email: user.email, - mobile: user.mobile, - createdAt: user.createdAt.toISOString(), - profileImage: null, - }, - }; + return { + success: true, + token, + user: { + id: user.id, + name: user.name, + email: user.email, + mobile: user.mobile, + createdAt: user.createdAt.toISOString(), + profileImage: null, + }, + } }), updatePassword: protectedProcedure .input(z.object({ password: z.string().min(6, 'Password must be at least 6 characters'), })) - .mutation(async ({ input, ctx }) => { + .mutation(async ({ input, ctx }): Promise => { const userId = ctx.user.userId; if (!userId) { throw new ApiError('User not authenticated', 401); @@ -311,41 +255,38 @@ export const authRouter = router({ const hashedPassword = await bcrypt.hash(input.password, 10); // Insert if not exists, then update if exists + await upsertUserAuthPasswordInDb(userId, hashedPassword) + + /* + // Old implementation - direct DB queries: try { await db.insert(userCreds).values({ userId: userId, userPassword: hashedPassword, }); - // Insert succeeded - new credentials created } catch (error: any) { - // Insert failed - check if it's a unique constraint violation - if (error.code === '23505') { // PostgreSQL unique constraint violation - // Update existing credentials + if (error.code === '23505') { await db.update(userCreds).set({ userPassword: hashedPassword, }).where(eq(userCreds.userId, userId)); } else { - // Re-throw if it's a different error throw error; } } + */ - return { success: true, message: 'Password updated successfully' }; + return { success: true, message: 'Password updated successfully' } }), getProfile: protectedProcedure - .query(async ({ ctx }) => { + .query(async ({ ctx }): Promise => { const userId = ctx.user.userId; if (!userId) { throw new ApiError('User not authenticated', 401); } - const [user] = await db - .select() - .from(users) - .where(eq(users.id, userId)) - .limit(1); + const user = await getUserAuthByIdInDb(userId) if (!user) { throw new ApiError('User not found', 404); @@ -359,14 +300,14 @@ export const authRouter = router({ email: user.email, mobile: user.mobile, }, - }; + } }), deleteAccount: protectedProcedure .input(z.object({ mobile: z.string().min(10, 'Mobile number is required'), })) - .mutation(async ({ ctx, input }) => { + .mutation(async ({ ctx, input }): Promise => { const userId = ctx.user.userId; const { mobile } = input; @@ -375,10 +316,7 @@ export const authRouter = router({ } // Double-check: verify user exists and is the authenticated user - const existingUser = await db.query.users.findFirst({ - where: eq(users.id, userId), - columns: { id: true, mobile: true }, - }); + const existingUser = await getUserAuthByIdInDb(userId) if (!existingUser) { throw new ApiError('User not found', 404); @@ -399,8 +337,11 @@ export const authRouter = router({ } // Use transaction for atomic deletion + await deleteUserAuthAccountInDb(userId) + + /* + // Old implementation - direct DB queries: await db.transaction(async (tx) => { - // Phase 1: Direct references (safe to delete first) await tx.delete(notifCreds).where(eq(notifCreds.userId, userId)); await tx.delete(couponApplicableUsers).where(eq(couponApplicableUsers.userId, userId)); await tx.delete(couponUsage).where(eq(couponUsage.userId, userId)); @@ -408,13 +349,10 @@ export const authRouter = router({ await tx.delete(cartItems).where(eq(cartItems.userId, userId)); await tx.delete(notifications).where(eq(notifications.userId, userId)); await tx.delete(productReviews).where(eq(productReviews.userId, userId)); - - // Update reserved coupons (set redeemedBy to null) await tx.update(reservedCoupons) .set({ redeemedBy: null }) .where(eq(reservedCoupons.redeemedBy, userId)); - // Phase 2: Order dependencies const userOrders = await tx .select({ id: orders.id }) .from(orders) @@ -425,23 +363,18 @@ export const authRouter = router({ await tx.delete(orderStatus).where(eq(orderStatus.orderId, order.id)); await tx.delete(payments).where(eq(payments.orderId, order.id)); await tx.delete(refunds).where(eq(refunds.orderId, order.id)); - // Additional coupon usage entries linked to specific orders await tx.delete(couponUsage).where(eq(couponUsage.orderId, order.id)); await tx.delete(complaints).where(eq(complaints.orderId, order.id)); } - // Delete orders await tx.delete(orders).where(eq(orders.userId, userId)); - - // Phase 3: Addresses (now safe since orders are deleted) await tx.delete(addresses).where(eq(addresses.userId, userId)); - - // Phase 4: Core user data await tx.delete(userDetails).where(eq(userDetails.userId, userId)); await tx.delete(userCreds).where(eq(userCreds.userId, userId)); await tx.delete(users).where(eq(users.id, userId)); }); + */ - return { success: true, message: 'Account deleted successfully' }; + return { success: true, message: 'Account deleted successfully' } }), }); diff --git a/apps/backend/src/trpc/apis/user-apis/apis/banners.ts b/apps/backend/src/trpc/apis/user-apis/apis/banners.ts index 8e6a001..960e3b6 100644 --- a/apps/backend/src/trpc/apis/user-apis/apis/banners.ts +++ b/apps/backend/src/trpc/apis/user-apis/apis/banners.ts @@ -1,24 +1,27 @@ -import { db } from '@/src/db/db_index'; -import { homeBanners } from '@/src/db/schema'; -import { publicProcedure, router } from '@/src/trpc/trpc-index'; -import { scaffoldAssetUrl } from '@/src/lib/s3-client'; -import { isNotNull, asc } from 'drizzle-orm'; +import { publicProcedure, router } from '@/src/trpc/trpc-index' +import { scaffoldAssetUrl } from '@/src/lib/s3-client' +import { getUserActiveBanners as getUserActiveBannersInDb } from '@/src/dbService' +import type { UserBannersResponse } from '@packages/shared' -export async function scaffoldBanners() { +export async function scaffoldBanners(): Promise { + const banners = await getUserActiveBannersInDb() + + /* + // Old implementation - direct DB queries: const banners = await db.query.homeBanners.findMany({ where: isNotNull(homeBanners.serialNum), // Only show assigned banners orderBy: asc(homeBanners.serialNum), // Order by slot number 1-4 }); + */ - // Convert S3 keys to signed URLs for client const bannersWithSignedUrls = banners.map((banner) => ({ ...banner, imageUrl: banner.imageUrl ? scaffoldAssetUrl(banner.imageUrl) : banner.imageUrl, - })); + })) return { banners: bannersWithSignedUrls, - }; + } } export const bannerRouter = router({ diff --git a/apps/backend/src/trpc/apis/user-apis/apis/cart.ts b/apps/backend/src/trpc/apis/user-apis/apis/cart.ts index a2495bc..3d09b52 100644 --- a/apps/backend/src/trpc/apis/user-apis/apis/cart.ts +++ b/apps/backend/src/trpc/apis/user-apis/apis/cart.ts @@ -1,19 +1,25 @@ -import { router, protectedProcedure, publicProcedure } from '@/src/trpc/trpc-index'; -import { z } from 'zod'; -import { db } from '@/src/db/db_index'; -import { cartItems, productInfo, units, productSlots, deliverySlotInfo } from '@/src/db/schema'; -import { eq, and, sql, inArray, gt } from 'drizzle-orm'; -import { ApiError } from '@/src/lib/api-error'; -import { generateSignedUrlsFromS3Urls, scaffoldAssetUrl } from '@/src/lib/s3-client'; -import { getProductSlots, getMultipleProductsSlots } from '@/src/stores/slot-store'; +import { router, protectedProcedure, publicProcedure } from '@/src/trpc/trpc-index' +import { z } from 'zod' +import { ApiError } from '@/src/lib/api-error' +import { scaffoldAssetUrl } from '@/src/lib/s3-client' +import { getMultipleProductsSlots } from '@/src/stores/slot-store' +import { + getUserCartItemsWithProducts as getUserCartItemsWithProductsInDb, + getUserProductById as getUserProductByIdInDb, + getUserCartItemByUserProduct as getUserCartItemByUserProductInDb, + incrementUserCartItemQuantity as incrementUserCartItemQuantityInDb, + insertUserCartItem as insertUserCartItemInDb, + updateUserCartItemQuantity as updateUserCartItemQuantityInDb, + deleteUserCartItem as deleteUserCartItemInDb, + clearUserCart as clearUserCartInDb, +} from '@/src/dbService' +import type { UserCartResponse } from '@packages/shared' -interface CartResponse { - items: any[]; - totalItems: number; - totalAmount: number; -} +const getCartData = async (userId: number): Promise => { + const cartItemsWithProducts = await getUserCartItemsWithProductsInDb(userId) -const getCartData = async (userId: number): Promise => { + /* + // Old implementation - direct DB queries: const cartItemsWithProducts = await db .select({ cartId: cartItems.id, @@ -31,39 +37,28 @@ const getCartData = async (userId: number): Promise => { .innerJoin(productInfo, eq(cartItems.productId, productInfo.id)) .innerJoin(units, eq(productInfo.unitId, units.id)) .where(eq(cartItems.userId, userId)); + */ - // Generate signed URLs for images - const cartWithSignedUrls = await Promise.all( - cartItemsWithProducts.map(async (item) => ({ - id: item.cartId, - productId: item.productId, - quantity: parseFloat(item.quantity), - addedAt: item.addedAt, - product: { - id: item.productId, - name: item.productName, - price: item.productPrice, - productQuantity: item.productQuantity, - unit: item.unitShortNotation, - isOutOfStock: item.isOutOfStock, - images: scaffoldAssetUrl((item.productImages as string[]) || []), - }, - subtotal: parseFloat(item.productPrice.toString()) * parseFloat(item.quantity), - })) - ); + const cartWithSignedUrls = cartItemsWithProducts.map((item) => ({ + ...item, + product: { + ...item.product, + images: scaffoldAssetUrl(item.product.images || []), + }, + })) - const totalAmount = cartWithSignedUrls.reduce((sum, item) => sum + item.subtotal, 0); + const totalAmount = cartWithSignedUrls.reduce((sum, item) => sum + item.subtotal, 0) return { items: cartWithSignedUrls, totalItems: cartWithSignedUrls.length, totalAmount, - }; -}; + } +} export const cartRouter = router({ getCart: protectedProcedure - .query(async ({ ctx }): Promise => { + .query(async ({ ctx }): Promise => { const userId = ctx.user.userId; return await getCartData(userId); }), @@ -73,7 +68,7 @@ export const cartRouter = router({ productId: z.number().int().positive(), quantity: z.number().int().positive(), })) - .mutation(async ({ input, ctx }): Promise => { + .mutation(async ({ input, ctx }): Promise => { const userId = ctx.user.userId; const { productId, quantity } = input; @@ -83,6 +78,22 @@ export const cartRouter = router({ } // Check if product exists + const product = await getUserProductByIdInDb(productId) + + if (!product) { + throw new ApiError('Product not found', 404) + } + + const existingItem = await getUserCartItemByUserProductInDb(userId, productId) + + if (existingItem) { + await incrementUserCartItemQuantityInDb(existingItem.id, quantity) + } else { + await insertUserCartItemInDb(userId, productId, quantity) + } + + /* + // Old implementation - direct DB queries: const product = await db.query.productInfo.findFirst({ where: eq(productInfo.id, productId), }); @@ -91,29 +102,27 @@ export const cartRouter = router({ throw new ApiError("Product not found", 404); } - // Check if item already exists in cart const existingItem = await db.query.cartItems.findFirst({ where: and(eq(cartItems.userId, userId), eq(cartItems.productId, productId)), }); if (existingItem) { - // Update quantity await db.update(cartItems) .set({ quantity: sql`${cartItems.quantity} + ${quantity}`, }) .where(eq(cartItems.id, existingItem.id)); } else { - // Insert new item await db.insert(cartItems).values({ userId, productId, quantity: quantity.toString(), }); } + */ // Return updated cart - return await getCartData(userId); + return await getCartData(userId) }), updateCartItem: protectedProcedure @@ -121,7 +130,7 @@ export const cartRouter = router({ itemId: z.number().int().positive(), quantity: z.number().int().min(0), })) - .mutation(async ({ input, ctx }): Promise => { + .mutation(async ({ input, ctx }): Promise => { const userId = ctx.user.userId; const { itemId, quantity } = input; @@ -129,6 +138,10 @@ export const cartRouter = router({ throw new ApiError("Positive quantity required", 400); } + const updated = await updateUserCartItemQuantityInDb(userId, itemId, quantity) + + /* + // Old implementation - direct DB queries: const [updatedItem] = await db.update(cartItems) .set({ quantity: quantity.toString() }) .where(and( @@ -140,19 +153,28 @@ export const cartRouter = router({ if (!updatedItem) { throw new ApiError("Cart item not found", 404); } + */ + + if (!updated) { + throw new ApiError('Cart item not found', 404) + } // Return updated cart - return await getCartData(userId); + return await getCartData(userId) }), removeFromCart: protectedProcedure .input(z.object({ itemId: z.number().int().positive(), })) - .mutation(async ({ input, ctx }): Promise => { + .mutation(async ({ input, ctx }): Promise => { const userId = ctx.user.userId; const { itemId } = input; + const deleted = await deleteUserCartItemInDb(userId, itemId) + + /* + // Old implementation - direct DB queries: const [deletedItem] = await db.delete(cartItems) .where(and( eq(cartItems.id, itemId), @@ -163,23 +185,33 @@ export const cartRouter = router({ if (!deletedItem) { throw new ApiError("Cart item not found", 404); } + */ + + if (!deleted) { + throw new ApiError('Cart item not found', 404) + } // Return updated cart - return await getCartData(userId); + return await getCartData(userId) }), clearCart: protectedProcedure - .mutation(async ({ ctx }) => { + .mutation(async ({ ctx }): Promise => { const userId = ctx.user.userId; + await clearUserCartInDb(userId) + + /* + // Old implementation - direct DB query: await db.delete(cartItems).where(eq(cartItems.userId, userId)); + */ return { items: [], totalItems: 0, totalAmount: 0, message: "Cart cleared successfully", - }; + } }), // Original DB-based getCartSlots (commented out) diff --git a/apps/backend/src/trpc/apis/user-apis/apis/complaint.ts b/apps/backend/src/trpc/apis/user-apis/apis/complaint.ts index 08dae54..9ce9c9b 100644 --- a/apps/backend/src/trpc/apis/user-apis/apis/complaint.ts +++ b/apps/backend/src/trpc/apis/user-apis/apis/complaint.ts @@ -1,14 +1,20 @@ -import { router, protectedProcedure } from '@/src/trpc/trpc-index'; -import { z } from 'zod'; -import { db } from '@/src/db/db_index'; -import { complaints } from '@/src/db/schema'; -import { eq } from 'drizzle-orm'; +import { router, protectedProcedure } from '@/src/trpc/trpc-index' +import { z } from 'zod' +import { + getUserComplaints as getUserComplaintsInDb, + createUserComplaint as createUserComplaintInDb, +} from '@/src/dbService' +import type { UserComplaintsResponse, UserRaiseComplaintResponse } from '@packages/shared' export const complaintRouter = router({ getAll: protectedProcedure - .query(async ({ ctx }) => { + .query(async ({ ctx }): Promise => { const userId = ctx.user.userId; + const userComplaints = await getUserComplaintsInDb(userId) + + /* + // Old implementation - direct DB queries: const userComplaints = await db .select({ id: complaints.id, @@ -32,6 +38,11 @@ export const complaintRouter = router({ orderId: c.orderId, })), }; + */ + + return { + complaints: userComplaints, + } }), raise: protectedProcedure @@ -39,7 +50,7 @@ export const complaintRouter = router({ orderId: z.string().optional(), complaintBody: z.string().min(1, 'Complaint body is required'), })) - .mutation(async ({ input, ctx }) => { + .mutation(async ({ input, ctx }): Promise => { const userId = ctx.user.userId; const { orderId, complaintBody } = input; @@ -52,12 +63,17 @@ export const complaintRouter = router({ } } + await createUserComplaintInDb(userId, orderIdNum, complaintBody.trim()) + + /* + // Old implementation - direct DB query: await db.insert(complaints).values({ userId, orderId: orderIdNum, complaintBody: complaintBody.trim(), }); + */ - return { success: true, message: 'Complaint raised successfully' }; + return { success: true, message: 'Complaint raised successfully' } }), }); diff --git a/apps/backend/src/trpc/apis/user-apis/apis/coupon.ts b/apps/backend/src/trpc/apis/user-apis/apis/coupon.ts index 6eab804..c196fc5 100644 --- a/apps/backend/src/trpc/apis/user-apis/apis/coupon.ts +++ b/apps/backend/src/trpc/apis/user-apis/apis/coupon.ts @@ -1,31 +1,20 @@ -import { router, protectedProcedure } from '@/src/trpc/trpc-index'; -import { z } from 'zod'; -import { db } from '@/src/db/db_index'; -import { coupons, couponUsage, couponApplicableUsers, reservedCoupons, couponApplicableProducts } from '@/src/db/schema'; -import { eq, and, or, gt, isNull, sql } from 'drizzle-orm'; -import { ApiError } from '@/src/lib/api-error'; +import { router, protectedProcedure } from '@/src/trpc/trpc-index' +import { z } from 'zod' +import { ApiError } from '@/src/lib/api-error' +import { + getUserActiveCouponsWithRelations as getUserActiveCouponsWithRelationsInDb, + getUserAllCouponsWithRelations as getUserAllCouponsWithRelationsInDb, + getUserReservedCouponByCode as getUserReservedCouponByCodeInDb, + redeemUserReservedCoupon as redeemUserReservedCouponInDb, +} from '@/src/dbService' +import type { + UserCouponDisplay, + UserEligibleCouponsResponse, + UserMyCouponsResponse, + UserRedeemCouponResponse, +} from '@packages/shared' -import { users } from '@/src/db/schema'; - -type CouponWithRelations = typeof coupons.$inferSelect & { - applicableUsers: (typeof couponApplicableUsers.$inferSelect & { user: typeof users.$inferSelect })[]; - usages: typeof couponUsage.$inferSelect[]; -}; - -export interface EligibleCoupon { - id: number; - code: string; - discountType: 'percentage' | 'flat'; - discountValue: number; - maxValue?: number; - minOrder?: number; - description: string; - exclusiveApply?: boolean; - isEligible: boolean; - ineligibilityReason?: string; -} - -const generateCouponDescription = (coupon: any): string => { +const generateCouponDescription = (coupon: { discountPercent?: string | null; flatDiscount?: string | null; minOrder?: string | null; maxValue?: string | null }): string => { let desc = ''; if (coupon.discountPercent) { @@ -45,29 +34,17 @@ const generateCouponDescription = (coupon: any): string => { return desc; }; -export interface CouponDisplay { - id: number; - code: string; - discountType: 'percentage' | 'flat'; - discountValue: number; - maxValue?: number; - minOrder?: number; - description: string; - validTill?: Date; - usageCount: number; - maxLimitForUser?: number; - isExpired: boolean; - isUsedUp: boolean; -} - export const userCouponRouter = router({ getEligible: protectedProcedure - .query(async ({ ctx }) => { + .query(async ({ ctx }): Promise => { try { const userId = ctx.user.userId; - // Get all active, non-expired coupons + const allCoupons = await getUserActiveCouponsWithRelationsInDb(userId) + + /* + // Old implementation - direct DB queries: const allCoupons = await db.query.coupons.findMany({ where: and( eq(coupons.isInvalidated, false), @@ -92,6 +69,7 @@ export const userCouponRouter = router({ }, } }); + */ // Filter to only coupons applicable to current user const applicableCoupons = allCoupons.filter(coupon => { @@ -100,7 +78,7 @@ export const userCouponRouter = router({ return applicableUsers.some(au => au.userId === userId); }); - return { success: true, data: applicableCoupons }; + return { success: true, data: applicableCoupons }; } catch(e) { console.log(e) @@ -110,11 +88,15 @@ export const userCouponRouter = router({ getProductCoupons: protectedProcedure .input(z.object({ productId: z.number().int().positive() })) - .query(async ({ input, ctx }) => { + .query(async ({ input, ctx }): Promise => { const userId = ctx.user.userId; const { productId } = input; // Get all active, non-expired coupons + const allCoupons = await getUserActiveCouponsWithRelationsInDb(userId) + + /* + // Old implementation - direct DB queries: const allCoupons = await db.query.coupons.findMany({ where: and( eq(coupons.isInvalidated, false), @@ -139,6 +121,7 @@ export const userCouponRouter = router({ }, } }); + */ // Filter to only coupons applicable to current user and product const applicableCoupons = allCoupons.filter(coupon => { @@ -155,10 +138,13 @@ export const userCouponRouter = router({ }), getMyCoupons: protectedProcedure - .query(async ({ ctx }) => { + .query(async ({ ctx }): Promise => { const userId = ctx.user.userId; - // Get all coupons + const allCoupons = await getUserAllCouponsWithRelationsInDb(userId) + + /* + // Old implementation - direct DB queries: const allCoupons = await db.query.coupons.findMany({ with: { usages: { @@ -171,9 +157,10 @@ export const userCouponRouter = router({ } } }); + */ // Filter coupons in JS: not invalidated, applicable to user, and not expired - const applicableCoupons = (allCoupons as CouponWithRelations[]).filter(coupon => { + const applicableCoupons = allCoupons.filter(coupon => { const isNotInvalidated = !coupon.isInvalidated; const applicableUsers = coupon.applicableUsers || []; const isApplicable = coupon.isApplyForAll || applicableUsers.some(au => au.userId === userId); @@ -182,15 +169,15 @@ export const userCouponRouter = router({ }); // Categorize coupons - const personalCoupons: CouponDisplay[] = []; - const generalCoupons: CouponDisplay[] = []; + const personalCoupons: UserCouponDisplay[] = []; + const generalCoupons: UserCouponDisplay[] = []; applicableCoupons.forEach(coupon => { const usageCount = coupon.usages.length; const isExpired = false; // Already filtered out expired coupons const isUsedUp = Boolean(coupon.maxLimitForUser && usageCount >= coupon.maxLimitForUser); - const couponDisplay: CouponDisplay = { + const couponDisplay: UserCouponDisplay = { id: coupon.id, code: coupon.couponCode, discountType: coupon.discountPercent ? 'percentage' : 'flat', @@ -225,17 +212,21 @@ export const userCouponRouter = router({ redeemReservedCoupon: protectedProcedure .input(z.object({ secretCode: z.string() })) - .mutation(async ({ input, ctx }) => { + .mutation(async ({ input, ctx }): Promise => { const userId = ctx.user.userId; const { secretCode } = input; - // Find the reserved coupon + const reservedCoupon = await getUserReservedCouponByCodeInDb(secretCode) + + /* + // Old implementation - direct DB queries: const reservedCoupon = await db.query.reservedCoupons.findFirst({ where: and( eq(reservedCoupons.secretCode, secretCode.toUpperCase()), eq(reservedCoupons.isRedeemed, false) ), }); + */ if (!reservedCoupon) { throw new ApiError("Invalid or already redeemed coupon code", 400); @@ -246,9 +237,11 @@ export const userCouponRouter = router({ throw new ApiError("You have already redeemed this coupon", 400); } - // Create the coupon in the main table + const couponResult = await redeemUserReservedCouponInDb(userId, reservedCoupon) + + /* + // Old implementation - direct DB queries: const couponResult = await db.transaction(async (tx) => { - // Insert into coupons const couponInsert = await tx.insert(coupons).values({ couponCode: reservedCoupon.couponCode, isUserBased: true, @@ -266,22 +259,11 @@ export const userCouponRouter = router({ const coupon = couponInsert[0]; - // Insert into couponApplicableUsers await tx.insert(couponApplicableUsers).values({ couponId: coupon.id, userId, }); - // Copy applicable products - if (reservedCoupon.productIds && Array.isArray(reservedCoupon.productIds) && reservedCoupon.productIds.length > 0) { - // Assuming productIds are the IDs, but wait, in schema, productIds is jsonb, but in relations, couponApplicableProducts has productId - // For simplicity, since reservedCoupons has productIds as jsonb, but to match, perhaps insert into couponApplicableProducts if needed - // But in createReservedCoupon, I inserted applicableProducts into couponApplicableProducts - // So for reserved, perhaps do the same, but since it's jsonb, maybe not. - // For now, skip, as the coupon will have productIds in coupons table. - } - - // Update reserved coupon as redeemed await tx.update(reservedCoupons).set({ isRedeemed: true, redeemedBy: userId, @@ -290,6 +272,7 @@ export const userCouponRouter = router({ return coupon; }); + */ return { success: true, coupon: couponResult }; }), diff --git a/apps/backend/src/trpc/apis/user-apis/apis/order.ts b/apps/backend/src/trpc/apis/user-apis/apis/order.ts index d4175a6..2ef55c1 100644 --- a/apps/backend/src/trpc/apis/user-apis/apis/order.ts +++ b/apps/backend/src/trpc/apis/user-apis/apis/order.ts @@ -1,108 +1,43 @@ import { router, protectedProcedure } from "@/src/trpc/trpc-index"; import { z } from "zod"; -import { db } from "@/src/db/db_index"; import { - orders, - orderItems, - orderStatus, - addresses, - productInfo, - paymentInfoTable, - coupons, - couponUsage, - payments, - cartItems, - refunds, - units, - userDetails, -} from "@/src/db/schema"; -import { eq, and, inArray, desc, gte, lte } from "drizzle-orm"; + validateAndGetUserCoupon, + applyDiscountToUserOrder, + getUserAddressByIdAndUser, + getOrderProductById, + checkUserSuspended, + getUserSlotCapacityStatus, + placeUserOrderTransaction, + deleteUserCartItemsForOrder, + recordUserCouponUsage, + getUserOrdersWithRelations, + getUserOrderCount, + getUserOrderByIdWithRelations, + getUserCouponUsageForOrder, + getUserOrderBasic, + cancelUserOrderTransaction, + updateUserOrderNotes, + getUserRecentlyDeliveredOrderIds, + getUserProductIdsFromOrders, + getUserProductsForRecentOrders, +} from "@/src/dbService"; +import { getNextDeliveryDate } from "@/src/trpc/apis/common-apis/common"; import { scaffoldAssetUrl } from "@/src/lib/s3-client"; import { ApiError } from "@/src/lib/api-error"; import { sendOrderPlacedNotification, sendOrderCancelledNotification, } from "@/src/lib/notif-job"; -import { getNextDeliveryDate } from "@/src/trpc/apis/common-apis/common"; import { CONST_KEYS, getConstant, getConstants } from "@/src/lib/const-store"; import { publishFormattedOrder, publishCancellation } from "@/src/lib/post-order-handler"; import { getSlotById } from "@/src/stores/slot-store"; - - -const validateAndGetCoupon = async ( - couponId: number | undefined, - userId: number, - totalAmount: number -) => { - if (!couponId) return null; - - const coupon = await db.query.coupons.findFirst({ - where: eq(coupons.id, couponId), - with: { - usages: { where: eq(couponUsage.userId, userId) }, - }, - }); - - if (!coupon) throw new ApiError("Invalid coupon", 400); - if (coupon.isInvalidated) - throw new ApiError("Coupon is no longer valid", 400); - if (coupon.validTill && new Date(coupon.validTill) < new Date()) - throw new ApiError("Coupon has expired", 400); - if ( - coupon.maxLimitForUser && - coupon.usages.length >= coupon.maxLimitForUser - ) - throw new ApiError("Coupon usage limit exceeded", 400); - if ( - coupon.minOrder && - parseFloat(coupon.minOrder.toString()) > totalAmount - ) - throw new ApiError( - "Order amount does not meet coupon minimum requirement", - 400 - ); - - return coupon; -}; - -const applyDiscountToOrder = ( - orderTotal: number, - appliedCoupon: typeof coupons.$inferSelect | null, - proportion: number -) => { - let finalOrderTotal = orderTotal; - // const proportion = totalAmount / orderTotal; - if (appliedCoupon) { - if (appliedCoupon.discountPercent) { - const discount = Math.min( - (orderTotal * - parseFloat(appliedCoupon.discountPercent.toString())) / - 100, - appliedCoupon.maxValue - ? parseFloat(appliedCoupon.maxValue.toString()) * proportion - : Infinity - ); - finalOrderTotal -= discount; - } else if (appliedCoupon.flatDiscount) { - const discount = Math.min( - parseFloat(appliedCoupon.flatDiscount.toString()) * proportion, - appliedCoupon.maxValue - ? parseFloat(appliedCoupon.maxValue.toString()) * proportion - : finalOrderTotal - ); - finalOrderTotal -= discount; - } - } - - // let orderDeliveryCharge = 0; - // if (isFirstOrder && finalOrderTotal < minOrderValue) { - // orderDeliveryCharge = deliveryCharge; - // finalOrderTotal += deliveryCharge; - // } - - - return { finalOrderTotal, orderGroupProportion: proportion }; -}; +import type { + UserOrdersResponse, + UserOrderDetail, + UserCancelOrderResponse, + UserUpdateNotesResponse, + UserRecentProductsResponse, +} from "@/src/dbService"; const placeOrderUtil = async (params: { userId: number; @@ -139,9 +74,7 @@ const placeOrderUtil = async (params: { const orderGroupId = `${Date.now()}-${userId}`; - const address = await db.query.addresses.findFirst({ - where: and(eq(addresses.userId, userId), eq(addresses.id, addressId)), - }); + const address = await getUserAddressByIdAndUser(addressId, userId); if (!address) { throw new ApiError("Invalid address", 400); } @@ -152,14 +85,12 @@ const placeOrderUtil = async (params: { productId: number; quantity: number; slotId: number | null; - product: any; + product: Awaited>; }> >(); for (const item of selectedItems) { - const product = await db.query.productInfo.findFirst({ - where: eq(productInfo.id, item.productId), - }); + const product = await getOrderProductById(item.productId); if (!product) { throw new ApiError(`Product ${item.productId} not found`, 400); } @@ -172,9 +103,7 @@ const placeOrderUtil = async (params: { if (params.isFlash) { for (const item of selectedItems) { - const product = await db.query.productInfo.findFirst({ - where: eq(productInfo.id, item.productId), - }); + const product = await getOrderProductById(item.productId); if (!product?.isFlashAvailable) { throw new ApiError(`Product ${item.productId} is not available for flash delivery`, 400); } @@ -185,6 +114,7 @@ const placeOrderUtil = async (params: { for (const [slotId, items] of ordersBySlot) { const orderTotal = items.reduce( (sum, item) => { + if (!item.product) return sum const itemPrice = params.isFlash ? parseFloat((item.product.flashPrice || item.product.price).toString()) : parseFloat(item.product.price.toString()); @@ -195,13 +125,16 @@ const placeOrderUtil = async (params: { totalAmount += orderTotal; } - const appliedCoupon = await validateAndGetCoupon(couponId, userId, totalAmount); + const appliedCoupon = await validateAndGetUserCoupon(couponId, userId, totalAmount); const expectedDeliveryCharge = totalAmount < minOrderValue ? deliveryCharge : 0; const totalWithDelivery = totalAmount + expectedDeliveryCharge; + const { db } = await import("postgresService"); + const { orders, orderItems, orderStatus } = await import("postgresService"); + type OrderData = { order: Omit; orderItems: Omit[]; @@ -214,6 +147,7 @@ const placeOrderUtil = async (params: { for (const [slotId, items] of ordersBySlot) { const subOrderTotal = items.reduce( (sum, item) => { + if (!item.product) return sum const itemPrice = params.isFlash ? parseFloat((item.product.flashPrice || item.product.price).toString()) : parseFloat(item.product.price.toString()); @@ -226,7 +160,7 @@ const placeOrderUtil = async (params: { const orderGroupProportion = subOrderTotal / totalAmount; const orderTotalAmount = isFirstOrder ? subOrderTotalWithDelivery : subOrderTotal; - const { finalOrderTotal: finalOrderAmount } = applyDiscountToOrder( + const { finalOrderTotal: finalOrderAmount } = applyDiscountToUserOrder( orderTotalAmount, appliedCoupon, orderGroupProportion @@ -248,21 +182,23 @@ const placeOrderUtil = async (params: { isFlashDelivery: params.isFlash, }; - const orderItemsData: Omit[] = items.map( - (item) => ({ - orderId: 0, - productId: item.productId, - quantity: item.quantity.toString(), - price: params.isFlash - ? item.product.flashPrice || item.product.price - : item.product.price, - discountedPrice: ( - params.isFlash - ? item.product.flashPrice || item.product.price - : item.product.price - ).toString(), - }) - ); + const orderItemsData: Omit[] = items + .filter((item) => item.product !== null && item.product !== undefined) + .map( + (item) => ({ + orderId: 0, + productId: item.productId, + quantity: item.quantity.toString(), + price: params.isFlash + ? item.product!.flashPrice || item.product!.price + : item.product!.price, + discountedPrice: ( + params.isFlash + ? item.product!.flashPrice || item.product!.price + : item.product!.price + ).toString(), + }) + ); const orderStatusData: Omit = { userId, @@ -274,79 +210,24 @@ const placeOrderUtil = async (params: { isFirstOrder = false; } - const createdOrders = await 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, index) => { - const od = ordersData[index]; - od.orderItems.forEach((item) => { - 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) { - // const razorpayOrder = await RazorpayPaymentService.createOrder( - // sharedPaymentInfoId, - // totalWithDelivery.toString() - // ); - // await RazorpayPaymentService.insertPaymentRecord( - // sharedPaymentInfoId, - // razorpayOrder, - // tx - // ); - } - - return insertedOrders; + const createdOrders = await placeUserOrderTransaction({ + userId, + ordersData, + paymentMethod, + totalWithDelivery, }); - await db.delete(cartItems).where( - and( - eq(cartItems.userId, userId), - inArray( - cartItems.productId, - selectedItems.map((item) => item.productId) - ) - ) + await deleteUserCartItemsForOrder( + userId, + selectedItems.map((item) => item.productId) ); if (appliedCoupon && createdOrders.length > 0) { - await db.insert(couponUsage).values({ + await recordUserCouponUsage( userId, - couponId: appliedCoupon.id, - orderId: createdOrders[0].id as number, - orderItemId: null, - usedAt: new Date(), - }); + appliedCoupon.id, + createdOrders[0].id + ); } for (const order of createdOrders) { @@ -379,12 +260,8 @@ export const orderRouter = router({ .mutation(async ({ input, ctx }) => { const userId = ctx.user.userId; - // Check if user is suspended from placing orders - const userDetail = await db.query.userDetails.findFirst({ - where: eq(userDetails.userId, userId), - }); - - if (userDetail?.isSuspended) { + const isSuspended = await checkUserSuspended(userId); + if (isSuspended) { throw new ApiError("Unable to place order", 403); } @@ -397,7 +274,6 @@ export const orderRouter = router({ isFlashDelivery, } = input; - // Check if flash delivery is enabled when placing a flash delivery order if (isFlashDelivery) { const isFlashDeliveryEnabled = await getConstant(CONST_KEYS.isFlashDeliveryEnabled); if (!isFlashDeliveryEnabled) { @@ -405,12 +281,11 @@ export const orderRouter = router({ } } - // Check if any selected slot is at full capacity (only for regular delivery) if (!isFlashDelivery) { const slotIds = [...new Set(selectedItems.filter(i => i.slotId !== null).map(i => i.slotId as number))]; for (const slotId of slotIds) { - const slot = await getSlotById(slotId); - if (slot?.isCapacityFull) { + const isCapacityFull = await getUserSlotCapacityStatus(slotId); + if (isCapacityFull) { throw new ApiError("Selected delivery slot is at full capacity. Please choose another slot.", 403); } } @@ -418,12 +293,10 @@ export const orderRouter = router({ let processedItems = selectedItems; - // Handle flash delivery slot resolution if (isFlashDelivery) { - // For flash delivery, set slotId to null (no specific slot assigned) processedItems = selectedItems.map(item => ({ ...item, - slotId: null as any, // Type override for flash delivery + slotId: null as any, })); } @@ -447,35 +320,13 @@ export const orderRouter = router({ }) .optional() ) - .query(async ({ input, ctx }) => { + .query(async ({ input, ctx }): Promise => { const { page = 1, pageSize = 10 } = input || {}; const userId = ctx.user.userId; const offset = (page - 1) * pageSize; - // Get total count for pagination - const totalCountResult = await db.$count( - orders, - eq(orders.userId, userId) - ); - const totalCount = totalCountResult; - - const userOrders = await 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: pageSize, - offset: offset, - }); + const totalCount = await getUserOrderCount(userId); + const userOrders = await getUserOrdersWithRelations(userId, offset, pageSize); const mappedOrders = await Promise.all( userOrders.map(async (order) => { @@ -515,7 +366,6 @@ export const orderRouter = router({ const items = await Promise.all( order.orderItems.map(async (item) => { - const signedImages = item.product.images ? scaffoldAssetUrl( item.product.images as string[] @@ -571,44 +421,20 @@ export const orderRouter = router({ getOrderById: protectedProcedure .input(z.object({ orderId: z.string() })) - .query(async ({ input, ctx }) => { + .query(async ({ input, ctx }): Promise => { const { orderId } = input; const userId = ctx.user.userId; - const order = await db.query.orders.findFirst({ - where: and(eq(orders.id, parseInt(orderId)), eq(orders.userId, userId)), - with: { - orderItems: { - with: { - product: true, - }, - }, - slot: true, - paymentInfo: true, - orderStatus: { - with: { - refundCoupon: true, - }, - }, - refunds: true, - }, - }); + const order = await getUserOrderByIdWithRelations(parseInt(orderId), userId); if (!order) { throw new Error("Order not found"); } - // Get coupon usage for this specific order using new orderId field - const couponUsageData = await db.query.couponUsage.findMany({ - where: eq(couponUsage.orderId, order.id), // Use new orderId field - with: { - coupon: true, - }, - }); + const couponUsageData = await getUserCouponUsageForOrder(order.id); let couponData = null; if (couponUsageData.length > 0) { - // Calculate total discount from multiple coupons let totalDiscountAmount = 0; const orderTotal = parseFloat(order.totalAmount.toString()); @@ -624,7 +450,6 @@ export const orderRouter = router({ discountAmount = parseFloat(usage.coupon.flatDiscount.toString()); } - // Apply max value limit if set if ( usage.coupon.maxValue && discountAmount > parseFloat(usage.coupon.maxValue.toString()) @@ -651,7 +476,7 @@ export const orderRouter = router({ type OrderStatus = "cancelled" | "success"; let deliveryStatus: DeliveryStatus; - let orderStatus: OrderStatus; + let orderStatusResult: OrderStatus; const allItemsPackaged = order.orderItems.every( (item) => item.is_packaged @@ -659,16 +484,16 @@ export const orderRouter = router({ if (status?.isCancelled) { deliveryStatus = "cancelled"; - orderStatus = "cancelled"; + orderStatusResult = "cancelled"; } else if (status?.isDelivered) { deliveryStatus = "success"; - orderStatus = "success"; + orderStatusResult = "success"; } else if (allItemsPackaged) { deliveryStatus = "packaged"; - orderStatus = "success"; + orderStatusResult = "success"; } else { deliveryStatus = "pending"; - orderStatus = "success"; + orderStatusResult = "success"; } const paymentMode = order.isCod ? "CoD" : "Online"; @@ -705,8 +530,8 @@ export const orderRouter = router({ orderDate: order.createdAt.toISOString(), deliveryStatus, deliveryDate: order.slot?.deliveryTime.toISOString(), - orderStatus: order.orderStatus, - cancellationStatus: orderStatus, + orderStatus: orderStatusResult, + cancellationStatus: orderStatusResult, cancelReason: status?.cancelReason || null, paymentMode, paymentStatus, @@ -720,29 +545,24 @@ export const orderRouter = router({ orderAmount: parseFloat(order.totalAmount.toString()), isFlashDelivery: order.isFlashDelivery, createdAt: order.createdAt.toISOString(), + totalAmount: parseFloat(order.totalAmount.toString()), + deliveryCharge: parseFloat(order.deliveryCharge.toString()), }; }), cancelOrder: protectedProcedure .input( z.object({ - // id: z.string().regex(/^ORD\d+$/, "Invalid order ID format"), id: z.number(), reason: z.string().min(1, "Cancellation reason is required"), }) ) - .mutation(async ({ input, ctx }) => { + .mutation(async ({ input, ctx }): Promise => { try { const userId = ctx.user.userId; const { id, reason } = input; - // Check if order exists and belongs to user - const order = await db.query.orders.findFirst({ - where: eq(orders.id, Number(id)), - with: { - orderStatus: true, - }, - }); + const order = await getUserOrderBasic(id); if (!order) { console.error("Order not found:", id); @@ -775,39 +595,11 @@ export const orderRouter = router({ throw new ApiError("Cannot cancel delivered order", 400); } - // Perform database operations in transaction - const result = await db.transaction(async (tx) => { - // Update order status - await tx - .update(orderStatus) - .set({ - isCancelled: true, - cancelReason: reason, - cancellationUserNotes: reason, - cancellationReviewed: false, - }) - .where(eq(orderStatus.id, status.id)); + await cancelUserOrderTransaction(id, status.id, reason, order.isCod); - // Determine refund status based on payment method - const refundStatus = order.isCod ? "na" : "pending"; + await sendOrderCancelledNotification(userId, id.toString()); - // Insert refund record - await tx.insert(refunds).values({ - orderId: order.id, - refundStatus, - }); - - return { orderId: order.id, userId }; - }); - - // Send notification outside transaction (idempotent operation) - await sendOrderCancelledNotification( - result.userId, - result.orderId.toString() - ); - - // Publish to Redis for Telegram notification - await publishCancellation(result.orderId, 'user', reason); + await publishCancellation(id, 'user', reason); return { success: true, message: "Order cancelled successfully" }; } catch (e) { @@ -823,25 +615,11 @@ export const orderRouter = router({ userNotes: z.string(), }) ) - .mutation(async ({ input, ctx }) => { + .mutation(async ({ input, ctx }): Promise => { const userId = ctx.user.userId; const { id, userNotes } = input; - // Extract readable ID from orderId (e.g., ORD001 -> 1) - // const readableIdMatch = id.match(/^ORD(\d+)$/); - // if (!readableIdMatch) { - // console.error("Invalid order ID format:", id); - // throw new ApiError("Invalid order ID format", 400); - // } - // const readableId = parseInt(readableIdMatch[1]); - - // Check if order exists and belongs to user - const order = await db.query.orders.findFirst({ - where: eq(orders.id, Number(id)), - with: { - orderStatus: true, - }, - }); + const order = await getUserOrderBasic(id); if (!order) { console.error("Order not found:", id); @@ -863,7 +641,6 @@ export const orderRouter = router({ throw new ApiError("Order status not found", 400); } - // Only allow updating notes for orders that are not delivered or cancelled if (status.isDelivered) { console.error("Cannot update notes for delivered order:", id); throw new ApiError("Cannot update notes for delivered order", 400); @@ -874,13 +651,7 @@ export const orderRouter = router({ throw new ApiError("Cannot update notes for cancelled order", 400); } - // Update user notes - await db - .update(orders) - .set({ - userNotes: userNotes || null, - }) - .where(eq(orders.id, order.id)); + await updateUserOrderNotes(id, userNotes); return { success: true, message: "Notes updated successfully" }; }), @@ -893,72 +664,27 @@ export const orderRouter = router({ }) .optional() ) - .query(async ({ input, ctx }) => { + .query(async ({ input, ctx }): Promise => { const { limit = 20 } = input || {}; const userId = ctx.user.userId; - // Get user's recent delivered orders (last 30 days) const thirtyDaysAgo = new Date(); thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); - const recentOrders = await 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, thirtyDaysAgo) - ) - ) - .orderBy(desc(orders.createdAt)) - .limit(10); // Get last 10 orders + const recentOrderIds = await getUserRecentlyDeliveredOrderIds(userId, 10, thirtyDaysAgo); - if (recentOrders.length === 0) { + if (recentOrderIds.length === 0) { return { success: true, products: [] }; } - const orderIds = recentOrders.map((order) => order.id); - - // Get unique product IDs from recent orders - const orderItemsResult = await db - .select({ productId: orderItems.productId }) - .from(orderItems) - .where(inArray(orderItems.orderId, orderIds)); - - const productIds = [ - ...new Set(orderItemsResult.map((item) => item.productId)), - ]; + const productIds = await getUserProductIdsFromOrders(recentOrderIds); if (productIds.length === 0) { return { success: true, products: [] }; } - // Get product details - const productsWithUnits = await 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)) - .limit(limit); + const productsWithUnits = await getUserProductsForRecentOrders(productIds, limit); - // Generate signed URLs for product images const formattedProducts = await Promise.all( productsWithUnits.map(async (product) => { const nextDeliveryDate = await getNextDeliveryDate(product.id); @@ -973,7 +699,7 @@ export const orderRouter = router({ nextDeliveryDate: nextDeliveryDate ? nextDeliveryDate.toISOString() : null, - images: scaffoldAssetUrl( + images: scaffoldAssetUrl( (product.images as string[]) || [] ), }; diff --git a/apps/backend/src/trpc/apis/user-apis/apis/payments.ts b/apps/backend/src/trpc/apis/user-apis/apis/payments.ts index 4115c10..fe94925 100644 --- a/apps/backend/src/trpc/apis/user-apis/apis/payments.ts +++ b/apps/backend/src/trpc/apis/user-apis/apis/payments.ts @@ -1,14 +1,23 @@ -import { router, protectedProcedure } from '@/src/trpc/trpc-index'; -import { z } from 'zod'; -import { db } from '@/src/db/db_index'; -import { orders, payments, orderStatus } from '@/src/db/schema'; -import { eq } from 'drizzle-orm'; -import { ApiError } from '@/src/lib/api-error'; -import crypto from 'crypto'; -import { razorpayId, razorpaySecret } from "@/src/lib/env-exporter"; -import { DiskPersistedSet } from "@/src/lib/disk-persisted-set"; -import { RazorpayPaymentService } from "@/src/lib/payments-utils"; +import { router, protectedProcedure } from '@/src/trpc/trpc-index' +import { z } from 'zod' +import { ApiError } from '@/src/lib/api-error' +import crypto from 'crypto' +import { razorpayId, razorpaySecret } from "@/src/lib/env-exporter" +import { RazorpayPaymentService } from "@/src/lib/payments-utils" +import { + getUserPaymentOrderById as getUserPaymentOrderByIdInDb, + getUserPaymentByOrderId as getUserPaymentByOrderIdInDb, + getUserPaymentByMerchantOrderId as getUserPaymentByMerchantOrderIdInDb, + updateUserPaymentSuccess as updateUserPaymentSuccessInDb, + updateUserOrderPaymentStatus as updateUserOrderPaymentStatusInDb, + markUserPaymentFailed as markUserPaymentFailedInDb, +} from '@/src/dbService' +import type { + UserPaymentOrderResponse, + UserPaymentVerifyResponse, + UserPaymentFailResponse, +} from '@packages/shared' @@ -18,27 +27,36 @@ export const paymentRouter = router({ .input(z.object({ orderId: z.string(), })) - .mutation(async ({ input, ctx }) => { + .mutation(async ({ input, ctx }): Promise => { const userId = ctx.user.userId; const { orderId } = input; - // Validate order exists and belongs to user + const order = await getUserPaymentOrderByIdInDb(parseInt(orderId)) + + /* + // Old implementation - direct DB queries: const order = await db.query.orders.findFirst({ where: eq(orders.id, parseInt(orderId)), }); + */ if (!order) { - throw new ApiError("Order not found", 404); + throw new ApiError("Order not found", 404) } if (order.userId !== userId) { - throw new ApiError("Order does not belong to user", 403); + throw new ApiError("Order does not belong to user", 403) } // Check for existing pending payment + const existingPayment = await getUserPaymentByOrderIdInDb(parseInt(orderId)) + + /* + // Old implementation - direct DB queries: const existingPayment = await db.query.payments.findFirst({ where: eq(payments.orderId, parseInt(orderId)), }); + */ if (existingPayment && existingPayment.status === 'pending') { return { @@ -48,14 +66,13 @@ export const paymentRouter = router({ } // Create Razorpay order and insert payment record - const razorpayOrder = await RazorpayPaymentService.createOrder(parseInt(orderId), order.totalAmount); - await RazorpayPaymentService.insertPaymentRecord(parseInt(orderId), razorpayOrder); + const razorpayOrder = await RazorpayPaymentService.createOrder(parseInt(orderId), order.totalAmount); + await RazorpayPaymentService.insertPaymentRecord(parseInt(orderId), razorpayOrder); - return { - razorpayOrderId: 0, - // razorpayOrderId: razorpayOrder.id, - key: razorpayId, - }; + return { + razorpayOrderId: 0, + key: razorpayId, + } }), @@ -66,7 +83,7 @@ export const paymentRouter = router({ razorpay_order_id: z.string(), razorpay_signature: z.string(), })) - .mutation(async ({ input, ctx }) => { + .mutation(async ({ input, ctx }): Promise => { const { razorpay_payment_id, razorpay_order_id, razorpay_signature } = input; // Verify signature @@ -80,9 +97,14 @@ export const paymentRouter = router({ } // Get current payment record + const currentPayment = await getUserPaymentByMerchantOrderIdInDb(razorpay_order_id) + + /* + // Old implementation - direct DB queries: const currentPayment = await db.query.payments.findFirst({ where: eq(payments.merchantOrderId, razorpay_order_id), }); + */ if (!currentPayment) { throw new ApiError("Payment record not found", 404); @@ -95,6 +117,10 @@ export const paymentRouter = router({ signature: razorpay_signature, }; + const updatedPayment = await updateUserPaymentSuccessInDb(razorpay_order_id, updatedPayload) + + /* + // Old implementation - direct DB queries: const [updatedPayment] = await db .update(payments) .set({ @@ -104,56 +130,77 @@ export const paymentRouter = router({ .where(eq(payments.merchantOrderId, razorpay_order_id)) .returning(); - // Update order status to mark payment as processed await db .update(orderStatus) .set({ paymentStatus: 'success', }) .where(eq(orderStatus.orderId, updatedPayment.orderId)); + */ + + if (!updatedPayment) { + throw new ApiError("Payment record not found", 404) + } + + await updateUserOrderPaymentStatusInDb(updatedPayment.orderId, 'success') return { success: true, message: "Payment verified successfully", - }; + } }), markPaymentFailed: protectedProcedure .input(z.object({ merchantOrderId: z.string(), })) - .mutation(async ({ input, ctx }) => { + .mutation(async ({ input, ctx }): Promise => { const userId = ctx.user.userId; const { merchantOrderId } = input; // Find payment by merchantOrderId + const payment = await getUserPaymentByMerchantOrderIdInDb(merchantOrderId) + + /* + // Old implementation - direct DB queries: const payment = await db.query.payments.findFirst({ where: eq(payments.merchantOrderId, merchantOrderId), }); + */ if (!payment) { throw new ApiError("Payment not found", 404); } // Check if payment belongs to user's order + const order = await getUserPaymentOrderByIdInDb(payment.orderId) + + /* + // Old implementation - direct DB queries: const order = await db.query.orders.findFirst({ where: eq(orders.id, payment.orderId), }); + */ if (!order || order.userId !== userId) { throw new ApiError("Payment does not belong to user", 403); } // Update payment status to failed + await markUserPaymentFailedInDb(payment.id) + + /* + // Old implementation - direct DB queries: await db .update(payments) .set({ status: 'failed' }) .where(eq(payments.id, payment.id)); + */ return { success: true, message: "Payment marked as failed", - }; + } }), }); diff --git a/apps/backend/src/trpc/apis/user-apis/apis/product.ts b/apps/backend/src/trpc/apis/user-apis/apis/product.ts index 0fc86ee..5e1dc00 100644 --- a/apps/backend/src/trpc/apis/user-apis/apis/product.ts +++ b/apps/backend/src/trpc/apis/user-apis/apis/product.ts @@ -1,39 +1,34 @@ -import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index'; -import { z } from 'zod'; -import { db } from '@/src/db/db_index'; -import { productInfo, units, productSlots, deliverySlotInfo, specialDeals, storeInfo, productTagInfo, productTags, productReviews, users } from '@/src/db/schema'; -import { claimUploadUrl, extractKeyFromPresignedUrl, scaffoldAssetUrl } from '@/src/lib/s3-client'; -import { ApiError } from '@/src/lib/api-error'; -import { eq, and, gt, sql, inArray, desc } from 'drizzle-orm'; -import { getProductById as getProductByIdFromCache, getAllProducts as getAllProductsFromCache } from '@/src/stores/product-store'; -import dayjs from 'dayjs'; +import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index' +import { z } from 'zod' +import { claimUploadUrl, extractKeyFromPresignedUrl, scaffoldAssetUrl } from '@/src/lib/s3-client' +import { ApiError } from '@/src/lib/api-error' +import { getProductById as getProductByIdFromCache, getAllProducts as getAllProductsFromCache } from '@/src/stores/product-store' +import dayjs from 'dayjs' +import { + getUserProductDetailById as getUserProductDetailByIdInDb, + getUserProductReviews as getUserProductReviewsInDb, + getUserProductByIdBasic as getUserProductByIdBasicInDb, + createUserProductReview as createUserProductReviewInDb, +} from '@/src/dbService' +import type { + UserProductDetail, + UserProductDetailData, + UserProductReviewsResponse, + UserCreateReviewResponse, + UserProductReviewWithSignedUrls, +} from '@packages/shared' -// Uniform Product Type -interface Product { - id: number; - name: string; - shortDescription: string | null; - longDescription: string | null; - price: string; - marketPrice: string | null; - unitNotation: string; - images: string[]; - isOutOfStock: boolean; - store: { id: number; name: string; description: string | null } | null; - incrementStep: number; - productQuantity: number; - isFlashAvailable: boolean; - flashPrice: string | null; - deliverySlots: Array<{ id: number; deliveryTime: Date; freezeTime: Date }>; - specialDeals: Array<{ quantity: string; price: string; validTill: Date }>; -} +const signProductImages = (product: UserProductDetailData): UserProductDetail => ({ + ...product, + images: scaffoldAssetUrl(product.images || []), +}) export const productRouter = router({ getProductDetails: publicProcedure .input(z.object({ id: z.string().regex(/^\d+$/, 'Invalid product ID'), })) - .query(async ({ input }): Promise => { + .query(async ({ input }): Promise => { const { id } = input; const productId = parseInt(id); @@ -60,6 +55,10 @@ export const productRouter = router({ } // If not in cache, fetch from database (fallback) + const productData = await getUserProductDetailByIdInDb(productId) + + /* + // Old implementation - direct DB queries: const productData = await db .select({ id: productInfo.id, @@ -81,82 +80,13 @@ export const productRouter = router({ .innerJoin(units, eq(productInfo.unitId, units.id)) .where(eq(productInfo.id, productId)) .limit(1); + */ - if (productData.length === 0) { - throw new Error('Product not found'); + if (!productData) { + throw new Error('Product not found') } - const product = productData[0]; - - // Fetch store info for this product - const storeData = product.storeId ? await db.query.storeInfo.findFirst({ - where: eq(storeInfo.id, product.storeId), - columns: { id: true, name: true, description: true }, - }) : null; - - // Fetch delivery slots for this product - const deliverySlotsData = await 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), - eq(deliverySlotInfo.isCapacityFull, false), - gt(deliverySlotInfo.deliveryTime, sql`NOW()`), - gt(deliverySlotInfo.freezeTime, sql`NOW()`) - ) - ) - .orderBy(deliverySlotInfo.deliveryTime); - - // Fetch special deals for this product - const specialDealsData = await 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); - - // Generate signed URLs for images - const signedImages = scaffoldAssetUrl((product.images as string[]) || []); - - const response: Product = { - id: product.id, - name: product.name, - shortDescription: product.shortDescription, - longDescription: product.longDescription, - price: product.price.toString(), - marketPrice: product.marketPrice?.toString() || null, - unitNotation: product.unitShortNotation, - images: signedImages, - isOutOfStock: product.isOutOfStock, - store: storeData ? { - id: storeData.id, - name: storeData.name, - description: storeData.description, - } : null, - incrementStep: product.incrementStep, - productQuantity: product.productQuantity, - isFlashAvailable: product.isFlashAvailable, - flashPrice: product.flashPrice?.toString() || null, - deliverySlots: deliverySlotsData, - specialDeals: specialDealsData.map(d => ({ quantity: d.quantity.toString(), price: d.price.toString(), validTill: d.validTill })), - }; - - return response; + return signProductImages(productData) }), getProductReviews: publicProcedure @@ -165,9 +95,13 @@ export const productRouter = router({ limit: z.number().int().min(1).max(50).optional().default(10), offset: z.number().int().min(0).optional().default(0), })) - .query(async ({ input }) => { + .query(async ({ input }): Promise => { const { productId, limit, offset } = input; + const { reviews, totalCount } = await getUserProductReviewsInDb(productId, limit, offset) + + /* + // Old implementation - direct DB queries: const reviews = await db .select({ id: productReviews.id, @@ -184,15 +118,6 @@ export const productRouter = router({ .limit(limit) .offset(offset); - // Generate signed URLs for images - const reviewsWithSignedUrls = await Promise.all( - reviews.map(async (review) => ({ - ...review, - signedImageUrls: scaffoldAssetUrl((review.imageUrls as string[]) || []), - })) - ); - - // Check if more reviews exist const totalCountResult = await db .select({ count: sql`count(*)` }) .from(productReviews) @@ -200,8 +125,16 @@ export const productRouter = router({ const totalCount = Number(totalCountResult[0].count); const hasMore = offset + limit < totalCount; + */ - return { reviews: reviewsWithSignedUrls, hasMore }; + const reviewsWithSignedUrls: UserProductReviewWithSignedUrls[] = reviews.map((review) => ({ + ...review, + signedImageUrls: scaffoldAssetUrl(review.imageUrls || []), + })) + + const hasMore = offset + limit < totalCount + + return { reviews: reviewsWithSignedUrls, hasMore } }), createReview: protectedProcedure @@ -212,11 +145,20 @@ export const productRouter = router({ imageUrls: z.array(z.string()).optional().default([]), uploadUrls: z.array(z.string()).optional().default([]), })) - .mutation(async ({ input, ctx }) => { + .mutation(async ({ input, ctx }): Promise => { const { productId, reviewBody, ratings, imageUrls, uploadUrls } = input; const userId = ctx.user.userId; - // Optional: Check if product exists + const product = await getUserProductByIdBasicInDb(productId) + if (!product) { + throw new ApiError('Product not found', 404) + } + + const imageKeys = uploadUrls.map(item => extractKeyFromPresignedUrl(item)) + const newReview = await createUserProductReviewInDb(userId, productId, reviewBody, ratings, imageKeys) + + /* + // Old implementation - direct DB queries: const product = await db.query.productInfo.findFirst({ where: eq(productInfo.id, productId), }); @@ -224,7 +166,6 @@ export const productRouter = router({ throw new ApiError('Product not found', 404); } - // Insert review const [newReview] = await db.insert(productReviews).values({ userId, productId, @@ -232,6 +173,7 @@ export const productRouter = router({ ratings, imageUrls: uploadUrls.map(item => extractKeyFromPresignedUrl(item)), }).returning(); + */ // Claim upload URLs if (uploadUrls && uploadUrls.length > 0) { @@ -243,24 +185,25 @@ export const productRouter = router({ } } - return { success: true, review: newReview }; + return { success: true, review: newReview } }), - getAllProductsSummary: publicProcedure - .query(async (): Promise => { + getAllProductsSummary: publicProcedure + .query(async (): Promise => { // Get all products from cache const allCachedProducts = await getAllProductsFromCache(); // Transform the cached products to match the expected summary format // (with empty deliverySlots and specialDeals arrays for summary view) - const transformedProducts = allCachedProducts.map(product => ({ + const transformedProducts: UserProductDetail[] = allCachedProducts.map(product => ({ ...product, - deliverySlots: [], // Empty for summary view - specialDeals: [], // Empty for summary view - })); + images: product.images || [], + deliverySlots: [], + specialDeals: [], + })) - return transformedProducts; + return transformedProducts }), }); diff --git a/apps/backend/src/trpc/apis/user-apis/apis/slots.ts b/apps/backend/src/trpc/apis/user-apis/apis/slots.ts index 92dd37f..265f42e 100644 --- a/apps/backend/src/trpc/apis/user-apis/apis/slots.ts +++ b/apps/backend/src/trpc/apis/user-apis/apis/slots.ts @@ -1,15 +1,9 @@ -import { router, publicProcedure } from "@/src/trpc/trpc-index"; -import { z } from "zod"; -import { db } from "@/src/db/db_index"; -import { - deliverySlotInfo, - productSlots, - productInfo, - units, -} from "@/src/db/schema"; -import { eq, and } from "drizzle-orm"; -import { getAllSlots as getAllSlotsFromCache, getSlotById as getSlotByIdFromCache } from "@/src/stores/slot-store"; -import dayjs from 'dayjs'; +import { router, publicProcedure } from "@/src/trpc/trpc-index" +import { z } from "zod" +import { getAllSlots as getAllSlotsFromCache, getSlotById as getSlotByIdFromCache } from "@/src/stores/slot-store" +import dayjs from 'dayjs' +import { getUserActiveSlotsList as getUserActiveSlotsListInDb, getUserProductAvailability as getUserProductAvailabilityInDb } from '@/src/dbService' +import type { UserSlotData, UserSlotsListResponse, UserSlotsWithProductsResponse } from '@packages/shared' // Helper method to get formatted slot data by ID async function getSlotData(slotId: number) { @@ -32,7 +26,7 @@ async function getSlotData(slotId: number) { }; } -export async function scaffoldSlotsWithProducts() { +export async function scaffoldSlotsWithProducts(): Promise { const allSlots = await getAllSlotsFromCache(); const currentTime = new Date(); const validSlots = allSlots @@ -43,7 +37,10 @@ export async function scaffoldSlotsWithProducts() { }) .sort((a, b) => dayjs(a.deliveryTime).valueOf() - dayjs(b.deliveryTime).valueOf()); - // Fetch all products for availability info + const productAvailability = await getUserProductAvailabilityInDb() + + /* + // Old implementation - direct DB query: const allProducts = await db .select({ id: productInfo.id, @@ -60,6 +57,7 @@ export async function scaffoldSlotsWithProducts() { isOutOfStock: product.isOutOfStock, isFlashAvailable: product.isFlashAvailable, })); + */ return { slots: validSlots, @@ -69,24 +67,30 @@ export async function scaffoldSlotsWithProducts() { } export const slotsRouter = router({ - getSlots: publicProcedure.query(async () => { + getSlots: publicProcedure.query(async (): Promise => { + const slots = await getUserActiveSlotsListInDb() + + /* + // Old implementation - direct DB query: const slots = await db.query.deliverySlotInfo.findMany({ where: eq(deliverySlotInfo.isActive, true), }); + */ + return { slots, count: slots.length, - }; + } }), - getSlotsWithProducts: publicProcedure.query(async () => { + getSlotsWithProducts: publicProcedure.query(async (): Promise => { const response = await scaffoldSlotsWithProducts(); return response; }), getSlotById: publicProcedure .input(z.object({ slotId: z.number() })) - .query(async ({ input }) => { + .query(async ({ input }): Promise => { return await getSlotData(input.slotId); }), }); diff --git a/apps/backend/src/trpc/apis/user-apis/apis/stores.ts b/apps/backend/src/trpc/apis/user-apis/apis/stores.ts index 9522b7a..fe2c751 100644 --- a/apps/backend/src/trpc/apis/user-apis/apis/stores.ts +++ b/apps/backend/src/trpc/apis/user-apis/apis/stores.ts @@ -1,13 +1,23 @@ -import { router, publicProcedure } from '@/src/trpc/trpc-index'; -import { z } from 'zod'; -import { db } from '@/src/db/db_index'; -import { storeInfo, productInfo, units } from '@/src/db/schema'; -import { eq, and, sql } from 'drizzle-orm'; -import { scaffoldAssetUrl } from '@/src/lib/s3-client'; -import { ApiError } from '@/src/lib/api-error'; -import { getTagsByStoreId } from '@/src/stores/product-tag-store'; +import { router, publicProcedure } from '@/src/trpc/trpc-index' +import { z } from 'zod' +import { scaffoldAssetUrl } from '@/src/lib/s3-client' +import { ApiError } from '@/src/lib/api-error' +import { getTagsByStoreId } from '@/src/stores/product-tag-store' +import { + getUserStoreSummaries as getUserStoreSummariesInDb, + getUserStoreDetail as getUserStoreDetailInDb, +} from '@/src/dbService' +import type { + UserStoresResponse, + UserStoreDetail, + UserStoreSummary, +} from '@packages/shared' -export async function scaffoldStores() { +export async function scaffoldStores(): Promise { + const storesData = await getUserStoreSummariesInDb() + + /* + // Old implementation - direct DB queries: const storesData = await db .select({ id: storeInfo.id, @@ -22,53 +32,38 @@ export async function scaffoldStores() { and(eq(productInfo.storeId, storeInfo.id), eq(productInfo.isSuspended, false)) ) .groupBy(storeInfo.id); + */ - // Generate signed URLs for store images and fetch sample products - const storesWithDetails = await Promise.all( - storesData.map(async (store) => { - const signedImageUrl = store.imageUrl ? scaffoldAssetUrl(store.imageUrl) : null; + const storesWithDetails: UserStoreSummary[] = storesData.map((store) => { + const signedImageUrl = store.imageUrl ? scaffoldAssetUrl(store.imageUrl) : null + const sampleProducts = store.sampleProducts.map((product) => ({ + id: product.id, + name: product.name, + signedImageUrl: product.images && product.images.length > 0 + ? scaffoldAssetUrl(product.images[0]) + : null, + })) - // Fetch up to 3 products for this store - const sampleProducts = await db - .select({ - id: productInfo.id, - name: productInfo.name, - images: productInfo.images, - }) - .from(productInfo) - .where(and(eq(productInfo.storeId, store.id), eq(productInfo.isSuspended, false))) - .limit(3); - - // Generate signed URLs for product images - const productsWithSignedUrls = await Promise.all( - sampleProducts.map(async (product) => { - const images = product.images as string[]; - return { - id: product.id, - name: product.name, - signedImageUrl: (images && images.length > 0) ? scaffoldAssetUrl(images[0]) : null, - }; - }) - ); - - return { - id: store.id, - name: store.name, - description: store.description, - signedImageUrl, - productCount: store.productCount, - sampleProducts: productsWithSignedUrls, - }; - }) - ); + return { + id: store.id, + name: store.name, + description: store.description, + signedImageUrl, + productCount: store.productCount, + sampleProducts, + } + }) return { stores: storesWithDetails, - }; + } } -export async function scaffoldStoreWithProducts(storeId: number) { - // Fetch store info +export async function scaffoldStoreWithProducts(storeId: number): Promise { + const storeDetail = await getUserStoreDetailInDb(storeId) + + /* + // Old implementation - direct DB queries: const storeData = await db.query.storeInfo.findFirst({ where: eq(storeInfo.id, storeId), columns: { @@ -83,10 +78,8 @@ export async function scaffoldStoreWithProducts(storeId: number) { throw new ApiError('Store not found', 404); } - // Generate signed URL for store image const signedImageUrl = storeData.imageUrl ? scaffoldAssetUrl(storeData.imageUrl) : null; - // Fetch products for this store const productsData = await db .select({ id: productInfo.id, @@ -105,8 +98,6 @@ export async function scaffoldStoreWithProducts(storeId: number) { .innerJoin(units, eq(productInfo.unitId, units.id)) .where(and(eq(productInfo.storeId, storeId), eq(productInfo.isSuspended, false))); - - // Generate signed URLs for product images const productsWithSignedUrls = await Promise.all( productsData.map(async (product) => ({ id: product.id, @@ -141,11 +132,53 @@ export async function scaffoldStoreWithProducts(storeId: number) { productIds: tag.productIds, })), }; + */ + + if (!storeDetail) { + throw new ApiError('Store not found', 404) + } + + const signedImageUrl = storeDetail.store.imageUrl + ? scaffoldAssetUrl(storeDetail.store.imageUrl) + : null + + const productsWithSignedUrls = storeDetail.products.map((product) => ({ + id: product.id, + name: product.name, + shortDescription: product.shortDescription, + price: product.price, + marketPrice: product.marketPrice, + incrementStep: product.incrementStep, + unit: product.unit, + unitNotation: product.unitNotation, + images: scaffoldAssetUrl(product.images || []), + isOutOfStock: product.isOutOfStock, + productQuantity: product.productQuantity, + })) + + const tags = await getTagsByStoreId(storeId) + + return { + store: { + id: storeDetail.store.id, + name: storeDetail.store.name, + description: storeDetail.store.description, + signedImageUrl, + }, + products: productsWithSignedUrls, + tags: tags.map(tag => ({ + id: tag.id, + tagName: tag.tagName, + tagDescription: tag.tagDescription, + imageUrl: tag.imageUrl, + productIds: tag.productIds, + })), + } } export const storesRouter = router({ getStores: publicProcedure - .query(async () => { + .query(async (): Promise => { const response = await scaffoldStores(); return response; }), @@ -154,7 +187,7 @@ export const storesRouter = router({ .input(z.object({ storeId: z.number(), })) - .query(async ({ input }) => { + .query(async ({ input }): Promise => { const { storeId } = input; const response = await scaffoldStoreWithProducts(storeId); return response; diff --git a/apps/backend/src/trpc/apis/user-apis/apis/user.ts b/apps/backend/src/trpc/apis/user-apis/apis/user.ts index 730c1af..cf318f9 100644 --- a/apps/backend/src/trpc/apis/user-apis/apis/user.ts +++ b/apps/backend/src/trpc/apis/user-apis/apis/user.ts @@ -1,27 +1,23 @@ -import { router, protectedProcedure, publicProcedure } from '@/src/trpc/trpc-index'; -import jwt from 'jsonwebtoken'; -import { eq, and } from 'drizzle-orm'; -import { z } from 'zod'; -import { db } from '@/src/db/db_index'; -import { users, userDetails, userCreds, notifCreds, unloggedUserTokens } from '@/src/db/schema'; -import { ApiError } from '@/src/lib/api-error'; -import { jwtSecret } from '@/src/lib/env-exporter'; -import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client'; - -interface AuthResponse { - token: string; - user: { - id: number; - name: string | null; - email: string | null; - mobile: string | null; - profileImage?: string | null; - bio?: string | null; - dateOfBirth?: string | null; - gender?: string | null; - occupation?: string | null; - }; -} +import { router, protectedProcedure, publicProcedure } from '@/src/trpc/trpc-index' +import jwt from 'jsonwebtoken' +import { z } from 'zod' +import { ApiError } from '@/src/lib/api-error' +import { jwtSecret } from '@/src/lib/env-exporter' +import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client' +import { + getUserProfileById as getUserProfileByIdInDb, + getUserProfileDetailById as getUserProfileDetailByIdInDb, + getUserWithCreds as getUserWithCredsInDb, + upsertUserNotifCred as upsertUserNotifCredInDb, + deleteUserUnloggedToken as deleteUserUnloggedTokenInDb, + getUserUnloggedToken as getUserUnloggedTokenInDb, + upsertUserUnloggedToken as upsertUserUnloggedTokenInDb, +} from '@/src/dbService' +import type { + UserSelfDataResponse, + UserProfileCompleteResponse, + UserSavePushTokenResponse, +} from '@packages/shared' const generateToken = (userId: number): string => { const secret = jwtSecret; @@ -34,137 +30,87 @@ const generateToken = (userId: number): string => { export const userRouter = router({ getSelfData: protectedProcedure - .query(async ({ ctx }) => { + .query(async ({ ctx }): Promise => { const userId = ctx.user.userId; if (!userId) { throw new ApiError('User not authenticated', 401); } - const [user] = await db - .select() - .from(users) - .where(eq(users.id, userId)) - .limit(1); + const user = await getUserProfileByIdInDb(userId) if (!user) { throw new ApiError('User not found', 404); } // Get user details for profile image - const [userDetail] = await db - .select() - .from(userDetails) - .where(eq(userDetails.userId, userId)) - .limit(1); + const userDetail = await getUserProfileDetailByIdInDb(userId) // Generate signed URL for profile image if it exists const profileImageSignedUrl = userDetail?.profileImage ? await generateSignedUrlFromS3Url(userDetail.profileImage) : null; - const response: Omit = { - user: { - id: user.id, - name: user.name, - email: user.email, - mobile: user.mobile, - profileImage: profileImageSignedUrl, - bio: userDetail?.bio || null, - dateOfBirth: userDetail?.dateOfBirth || null, - gender: userDetail?.gender || null, - occupation: userDetail?.occupation || null, - }, - }; - return { success: true, - data: response, - }; + data: { + user: { + id: user.id, + name: user.name, + email: user.email, + mobile: user.mobile, + profileImage: profileImageSignedUrl, + bio: userDetail?.bio || null, + dateOfBirth: userDetail?.dateOfBirth || null, + gender: userDetail?.gender || null, + occupation: userDetail?.occupation || null, + }, + }, + } }), checkProfileComplete: protectedProcedure - .query(async ({ ctx }) => { + .query(async ({ ctx }): Promise => { const userId = ctx.user.userId; if (!userId) { throw new ApiError('User not authenticated', 401); } - const result = await db - .select() - .from(users) - .leftJoin(userCreds, eq(users.id, userCreds.userId)) - .where(eq(users.id, userId)) - .limit(1); + const result = await getUserWithCredsInDb(userId) - if (result.length === 0) { - throw new ApiError('User not found', 404); + if (!result) { + throw new ApiError('User not found', 404) } - const { users: user, user_creds: creds } = result[0]; - return { - isComplete: !!(user.name && user.email && creds), + isComplete: !!(result.user.name && result.user.email && result.creds), }; }), savePushToken: publicProcedure .input(z.object({ token: z.string() })) - .mutation(async ({ input, ctx }) => { + .mutation(async ({ input, ctx }): Promise => { const { token } = input; const userId = ctx.user?.userId; if (userId) { // AUTHENTICATED USER // Check if token exists in notif_creds for this user - const existing = await db.query.notifCreds.findFirst({ - where: and( - eq(notifCreds.userId, userId), - eq(notifCreds.token, token) - ), - }); - - if (existing) { - // Update lastVerified timestamp - await db - .update(notifCreds) - .set({ lastVerified: new Date() }) - .where(eq(notifCreds.id, existing.id)); - } else { - // Insert new token into notif_creds - await db.insert(notifCreds).values({ - userId, - token, - lastVerified: new Date(), - }); - } - - // Remove from unlogged_user_tokens if it exists - await db - .delete(unloggedUserTokens) - .where(eq(unloggedUserTokens.token, token)); + await upsertUserNotifCredInDb(userId, token) + await deleteUserUnloggedTokenInDb(token) } else { // UNAUTHENTICATED USER // Save/update in unlogged_user_tokens - const existing = await db.query.unloggedUserTokens.findFirst({ - where: eq(unloggedUserTokens.token, token), - }); - + const existing = await getUserUnloggedTokenInDb(token) if (existing) { - await db - .update(unloggedUserTokens) - .set({ lastVerified: new Date() }) - .where(eq(unloggedUserTokens.id, existing.id)); + await upsertUserUnloggedTokenInDb(token) } else { - await db.insert(unloggedUserTokens).values({ - token, - lastVerified: new Date(), - }); + await upsertUserUnloggedTokenInDb(token) } } - return { success: true }; + return { success: true } }), }); diff --git a/packages/db_helper_postgres/index.ts b/packages/db_helper_postgres/index.ts index a17b5e3..ddf7441 100644 --- a/packages/db_helper_postgres/index.ts +++ b/packages/db_helper_postgres/index.ts @@ -165,7 +165,123 @@ export { getVendorOrders, } from './src/admin-apis/vendor-snippets'; -// Note: User API helpers are available in their respective files -// but not exported from main index to avoid naming conflicts -// Import them directly from the file paths if needed: -// import { getAllProducts } from '@packages/db_helper_postgres/src/user-apis/product' +export { + // User Address + getDefaultAddress as getUserDefaultAddress, + getUserAddresses, + getUserAddressById, + clearDefaultAddress as clearUserDefaultAddress, + createUserAddress, + updateUserAddress, + deleteUserAddress, + hasOngoingOrdersForAddress, +} from './src/user-apis/address'; + +export { + // User Banners + getActiveBanners as getUserActiveBanners, +} from './src/user-apis/banners'; + +export { + // User Cart + getCartItemsWithProducts as getUserCartItemsWithProducts, + getProductById as getUserProductById, + getCartItemByUserProduct as getUserCartItemByUserProduct, + incrementCartItemQuantity as incrementUserCartItemQuantity, + insertCartItem as insertUserCartItem, + updateCartItemQuantity as updateUserCartItemQuantity, + deleteCartItem as deleteUserCartItem, + clearUserCart, +} from './src/user-apis/cart'; + +export { + // User Complaint + getUserComplaints as getUserComplaints, + createComplaint as createUserComplaint, +} from './src/user-apis/complaint'; + +export { + // User Stores + getStoreSummaries as getUserStoreSummaries, + getStoreDetail as getUserStoreDetail, +} from './src/user-apis/stores'; + +export { + // User Product + getProductDetailById as getUserProductDetailById, + getProductReviews as getUserProductReviews, + getProductById as getUserProductByIdBasic, + createProductReview as createUserProductReview, +} from './src/user-apis/product'; + +export { + // User Slots + getActiveSlotsList as getUserActiveSlotsList, + getProductAvailability as getUserProductAvailability, +} from './src/user-apis/slots'; + +export { + // User Payments + getOrderById as getUserPaymentOrderById, + getPaymentByOrderId as getUserPaymentByOrderId, + getPaymentByMerchantOrderId as getUserPaymentByMerchantOrderId, + updatePaymentSuccess as updateUserPaymentSuccess, + updateOrderPaymentStatus as updateUserOrderPaymentStatus, + markPaymentFailed as markUserPaymentFailed, +} from './src/user-apis/payments'; + +export { + // User Auth + getUserByEmail as getUserAuthByEmail, + getUserByMobile as getUserAuthByMobile, + getUserById as getUserAuthById, + getUserCreds as getUserAuthCreds, + getUserDetails as getUserAuthDetails, + createUserWithCreds as createUserAuthWithCreds, + createUserWithMobile as createUserAuthWithMobile, + upsertUserPassword as upsertUserAuthPassword, + deleteUserAccount as deleteUserAuthAccount, +} from './src/user-apis/auth'; + +export { + // User Coupon + getActiveCouponsWithRelations as getUserActiveCouponsWithRelations, + getAllCouponsWithRelations as getUserAllCouponsWithRelations, + getReservedCouponByCode as getUserReservedCouponByCode, + redeemReservedCoupon as redeemUserReservedCoupon, +} from './src/user-apis/coupon'; + +export { + // User Profile + getUserById as getUserProfileById, + getUserDetailByUserId as getUserProfileDetailById, + getUserWithCreds as getUserWithCreds, + getNotifCred as getUserNotifCred, + upsertNotifCred as upsertUserNotifCred, + deleteUnloggedToken as deleteUserUnloggedToken, + getUnloggedToken as getUserUnloggedToken, + upsertUnloggedToken as upsertUserUnloggedToken, +} from './src/user-apis/user'; + +export { + // User Order + validateAndGetCoupon as validateAndGetUserCoupon, + applyDiscountToOrder as applyDiscountToUserOrder, + getAddressByIdAndUser as getUserAddressByIdAndUser, + getProductById as getOrderProductById, + checkUserSuspended, + getSlotCapacityStatus as getUserSlotCapacityStatus, + placeOrderTransaction as placeUserOrderTransaction, + deleteCartItemsForOrder as deleteUserCartItemsForOrder, + recordCouponUsage as recordUserCouponUsage, + getOrdersWithRelations as getUserOrdersWithRelations, + getOrderCount as getUserOrderCount, + getOrderByIdWithRelations as getUserOrderByIdWithRelations, + getCouponUsageForOrder as getUserCouponUsageForOrder, + getOrderBasic as getUserOrderBasic, + cancelOrderTransaction as cancelUserOrderTransaction, + updateOrderNotes as updateUserOrderNotes, + getRecentlyDeliveredOrderIds as getUserRecentlyDeliveredOrderIds, + getProductIdsFromOrders as getUserProductIdsFromOrders, + getProductsForRecentOrders as getUserProductsForRecentOrders, +} from './src/user-apis/order'; diff --git a/packages/db_helper_postgres/src/user-apis/address.ts b/packages/db_helper_postgres/src/user-apis/address.ts index 9428831..ee5792e 100644 --- a/packages/db_helper_postgres/src/user-apis/address.ts +++ b/packages/db_helper_postgres/src/user-apis/address.ts @@ -1,23 +1,148 @@ -import { db } from '../db/db_index'; -import { addresses, addressAreas, addressZones } from '../db/schema'; -import { eq, desc } from 'drizzle-orm'; +import { db } from '../db/db_index' +import { addresses, deliverySlotInfo, orders, orderStatus } from '../db/schema' +import { and, eq, gte } from 'drizzle-orm' +import type { InferSelectModel } from 'drizzle-orm' +import type { UserAddress } from '@packages/shared' -export async function getZones(): Promise { - const zones = await db.select().from(addressZones).orderBy(desc(addressZones.addedAt)); - return zones; +type AddressRow = InferSelectModel + +const mapUserAddress = (address: AddressRow): UserAddress => ({ + id: address.id, + userId: address.userId, + name: address.name, + phone: address.phone, + addressLine1: address.addressLine1, + addressLine2: address.addressLine2 ?? null, + city: address.city, + state: address.state, + pincode: address.pincode, + isDefault: address.isDefault, + latitude: address.latitude ?? null, + longitude: address.longitude ?? null, + googleMapsUrl: address.googleMapsUrl ?? null, + adminLatitude: address.adminLatitude ?? null, + adminLongitude: address.adminLongitude ?? null, + zoneId: address.zoneId ?? null, + createdAt: address.createdAt, +}) + +export async function getDefaultAddress(userId: number): Promise { + const [defaultAddress] = await db + .select() + .from(addresses) + .where(and(eq(addresses.userId, userId), eq(addresses.isDefault, true))) + .limit(1) + + return defaultAddress ? mapUserAddress(defaultAddress) : null } -export async function getAreas(): Promise { - const areas = await db.select().from(addressAreas).orderBy(desc(addressAreas.createdAt)); - return areas; +export async function getUserAddresses(userId: number): Promise { + const userAddresses = await db.select().from(addresses).where(eq(addresses.userId, userId)) + return userAddresses.map(mapUserAddress) } -export async function createZone(zoneName: string): Promise { - const [zone] = await db.insert(addressZones).values({ zoneName }).returning(); - return zone; +export async function getUserAddressById(userId: number, addressId: number): Promise { + const [address] = await db + .select() + .from(addresses) + .where(and(eq(addresses.id, addressId), eq(addresses.userId, userId))) + .limit(1) + + return address ? mapUserAddress(address) : null } -export async function createArea(placeName: string, zoneId: number | null): Promise { - const [area] = await db.insert(addressAreas).values({ placeName, zoneId }).returning(); - return area; +export async function clearDefaultAddress(userId: number): Promise { + await db.update(addresses).set({ isDefault: false }).where(eq(addresses.userId, userId)) +} + +export async function createUserAddress(input: { + userId: number + name: string + phone: string + addressLine1: string + addressLine2?: string + city: string + state: string + pincode: string + isDefault: boolean + latitude?: number + longitude?: number + googleMapsUrl?: string +}): Promise { + const [newAddress] = await db.insert(addresses).values({ + userId: input.userId, + name: input.name, + phone: input.phone, + addressLine1: input.addressLine1, + addressLine2: input.addressLine2, + city: input.city, + state: input.state, + pincode: input.pincode, + isDefault: input.isDefault, + latitude: input.latitude, + longitude: input.longitude, + googleMapsUrl: input.googleMapsUrl, + }).returning() + + return mapUserAddress(newAddress) +} + +export async function updateUserAddress(input: { + userId: number + addressId: number + name: string + phone: string + addressLine1: string + addressLine2?: string + city: string + state: string + pincode: string + isDefault: boolean + latitude?: number + longitude?: number + googleMapsUrl?: string +}): Promise { + const [updatedAddress] = await db.update(addresses) + .set({ + name: input.name, + phone: input.phone, + addressLine1: input.addressLine1, + addressLine2: input.addressLine2, + city: input.city, + state: input.state, + pincode: input.pincode, + isDefault: input.isDefault, + googleMapsUrl: input.googleMapsUrl, + latitude: input.latitude, + longitude: input.longitude, + }) + .where(and(eq(addresses.id, input.addressId), eq(addresses.userId, input.userId))) + .returning() + + return updatedAddress ? mapUserAddress(updatedAddress) : null +} + +export async function deleteUserAddress(userId: number, addressId: number): Promise { + const [deleted] = await db.delete(addresses) + .where(and(eq(addresses.id, addressId), eq(addresses.userId, userId))) + .returning({ id: addresses.id }) + + return !!deleted +} + +export async function hasOngoingOrdersForAddress(addressId: number): Promise { + const ongoingOrders = await db.select({ + orderId: orders.id, + }) + .from(orders) + .innerJoin(orderStatus, eq(orders.id, orderStatus.orderId)) + .innerJoin(deliverySlotInfo, eq(orders.slotId, deliverySlotInfo.id)) + .where(and( + eq(orders.addressId, addressId), + eq(orderStatus.isCancelled, false), + gte(deliverySlotInfo.deliveryTime, new Date()) + )) + .limit(1) + + return ongoingOrders.length > 0 } diff --git a/packages/db_helper_postgres/src/user-apis/auth.ts b/packages/db_helper_postgres/src/user-apis/auth.ts index ea56fa9..0ed5424 100644 --- a/packages/db_helper_postgres/src/user-apis/auth.ts +++ b/packages/db_helper_postgres/src/user-apis/auth.ts @@ -1,14 +1,132 @@ -import { db } from '../db/db_index'; -import { users } from '../db/schema'; -import { eq } from 'drizzle-orm'; +import { db } from '../db/db_index' +import { + users, + userCreds, + userDetails, + addresses, + cartItems, + complaints, + couponApplicableUsers, + couponUsage, + notifCreds, + notifications, + orderItems, + orderStatus, + orders, + payments, + refunds, + productReviews, + reservedCoupons, +} 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 getUserByEmail(email: string) { + const [user] = await db.select().from(users).where(eq(users.email, email)).limit(1) + return user || null } -export async function createUser(userData: any): Promise { - const [user] = await db.insert(users).values(userData).returning(); - return user; +export async function getUserByMobile(mobile: string) { + const [user] = await db.select().from(users).where(eq(users.mobile, mobile)).limit(1) + return user || null +} + +export async function getUserById(userId: number) { + const [user] = await db.select().from(users).where(eq(users.id, userId)).limit(1) + return user || null +} + +export async function getUserCreds(userId: number) { + const [creds] = await db.select().from(userCreds).where(eq(userCreds.userId, userId)).limit(1) + return creds || null +} + +export async function getUserDetails(userId: number) { + const [details] = await db.select().from(userDetails).where(eq(userDetails.userId, userId)).limit(1) + return details || null +} + +export async function createUserWithCreds(input: { + name: string + email: string + mobile: string + hashedPassword: string +}) { + return db.transaction(async (tx) => { + const [user] = await tx.insert(users).values({ + name: input.name, + email: input.email, + mobile: input.mobile, + }).returning() + + await tx.insert(userCreds).values({ + userId: user.id, + userPassword: input.hashedPassword, + }) + + return user + }) +} + +export async function createUserWithMobile(mobile: string) { + const [user] = await db.insert(users).values({ + name: null, + email: null, + mobile, + }).returning() + + return user +} + +export async function upsertUserPassword(userId: number, hashedPassword: string) { + try { + await db.insert(userCreds).values({ + userId, + userPassword: hashedPassword, + }) + return + } catch (error: any) { + if (error.code === '23505') { + await db.update(userCreds).set({ + userPassword: hashedPassword, + }).where(eq(userCreds.userId, userId)) + return + } + throw error + } +} + +export async function deleteUserAccount(userId: number) { + await db.transaction(async (tx) => { + await tx.delete(notifCreds).where(eq(notifCreds.userId, userId)) + await tx.delete(couponApplicableUsers).where(eq(couponApplicableUsers.userId, userId)) + await tx.delete(couponUsage).where(eq(couponUsage.userId, userId)) + await tx.delete(complaints).where(eq(complaints.userId, userId)) + await tx.delete(cartItems).where(eq(cartItems.userId, userId)) + await tx.delete(notifications).where(eq(notifications.userId, userId)) + await tx.delete(productReviews).where(eq(productReviews.userId, userId)) + + await tx.update(reservedCoupons) + .set({ redeemedBy: null }) + .where(eq(reservedCoupons.redeemedBy, userId)) + + const userOrders = await tx + .select({ id: orders.id }) + .from(orders) + .where(eq(orders.userId, userId)) + + for (const order of userOrders) { + await tx.delete(orderItems).where(eq(orderItems.orderId, order.id)) + await tx.delete(orderStatus).where(eq(orderStatus.orderId, order.id)) + await tx.delete(payments).where(eq(payments.orderId, order.id)) + await tx.delete(refunds).where(eq(refunds.orderId, order.id)) + await tx.delete(couponUsage).where(eq(couponUsage.orderId, order.id)) + await tx.delete(complaints).where(eq(complaints.orderId, order.id)) + } + + await tx.delete(orders).where(eq(orders.userId, userId)) + await tx.delete(addresses).where(eq(addresses.userId, userId)) + await tx.delete(userDetails).where(eq(userDetails.userId, userId)) + await tx.delete(userCreds).where(eq(userCreds.userId, userId)) + await tx.delete(users).where(eq(users.id, userId)) + }) } diff --git a/packages/db_helper_postgres/src/user-apis/banners.ts b/packages/db_helper_postgres/src/user-apis/banners.ts index f062cbd..5aeb02b 100644 --- a/packages/db_helper_postgres/src/user-apis/banners.ts +++ b/packages/db_helper_postgres/src/user-apis/banners.ts @@ -1,11 +1,29 @@ -import { db } from '../db/db_index'; -import { homeBanners } from '../db/schema'; -import { eq, and, desc, sql } from 'drizzle-orm'; +import { db } from '../db/db_index' +import { homeBanners } from '../db/schema' +import { asc, isNotNull } from 'drizzle-orm' +import type { InferSelectModel } from 'drizzle-orm' +import type { UserBanner } from '@packages/shared' -export async function getActiveBanners(): Promise { +type BannerRow = InferSelectModel + +const mapBanner = (banner: BannerRow): UserBanner => ({ + id: banner.id, + name: banner.name, + imageUrl: banner.imageUrl, + description: banner.description ?? null, + productIds: banner.productIds ?? null, + redirectUrl: banner.redirectUrl ?? null, + serialNum: banner.serialNum ?? null, + isActive: banner.isActive, + createdAt: banner.createdAt, + lastUpdated: banner.lastUpdated, +}) + +export async function getActiveBanners(): Promise { const banners = await db.query.homeBanners.findMany({ - where: eq(homeBanners.isActive, true), - orderBy: desc(homeBanners.createdAt), - }); - return banners; + where: isNotNull(homeBanners.serialNum), + orderBy: asc(homeBanners.serialNum), + }) + + return banners.map(mapBanner) } diff --git a/packages/db_helper_postgres/src/user-apis/cart.ts b/packages/db_helper_postgres/src/user-apis/cart.ts index a398987..211699c 100644 --- a/packages/db_helper_postgres/src/user-apis/cart.ts +++ b/packages/db_helper_postgres/src/user-apis/cart.ts @@ -1,41 +1,95 @@ -import { db } from '../db/db_index'; -import { cartItems, productInfo } from '../db/schema'; -import { eq, and } from 'drizzle-orm'; +import { db } from '../db/db_index' +import { cartItems, productInfo, units } from '../db/schema' +import { and, eq, sql } from 'drizzle-orm' +import type { UserCartItem } from '@packages/shared' -export async function getCartItems(userId: number): Promise { - return await db.query.cartItems.findMany({ - where: eq(cartItems.userId, userId), - with: { - product: { - with: { - unit: true, - }, - }, - }, - }); +const getStringArray = (value: unknown): string[] => { + if (!Array.isArray(value)) return [] + return value.map((item) => String(item)) } -export async function addToCart(userId: number, productId: number, quantity: number): Promise { - const [item] = await db.insert(cartItems).values({ +export async function getCartItemsWithProducts(userId: number): Promise { + const cartItemsWithProducts = await db + .select({ + cartId: cartItems.id, + productId: productInfo.id, + productName: productInfo.name, + productPrice: productInfo.price, + productImages: productInfo.images, + productQuantity: productInfo.productQuantity, + isOutOfStock: productInfo.isOutOfStock, + unitShortNotation: units.shortNotation, + quantity: cartItems.quantity, + addedAt: cartItems.addedAt, + }) + .from(cartItems) + .innerJoin(productInfo, eq(cartItems.productId, productInfo.id)) + .innerJoin(units, eq(productInfo.unitId, units.id)) + .where(eq(cartItems.userId, userId)) + + return cartItemsWithProducts.map((item) => ({ + id: item.cartId, + productId: item.productId, + quantity: parseFloat(item.quantity), + addedAt: item.addedAt, + product: { + id: item.productId, + name: item.productName, + price: item.productPrice.toString(), + productQuantity: item.productQuantity, + unit: item.unitShortNotation, + isOutOfStock: item.isOutOfStock, + images: getStringArray(item.productImages), + }, + subtotal: parseFloat(item.productPrice.toString()) * parseFloat(item.quantity), + })) +} + +export async function getProductById(productId: number) { + return db.query.productInfo.findFirst({ + where: eq(productInfo.id, productId), + }) +} + +export async function getCartItemByUserProduct(userId: number, productId: number) { + return db.query.cartItems.findFirst({ + where: and(eq(cartItems.userId, userId), eq(cartItems.productId, productId)), + }) +} + +export async function incrementCartItemQuantity(itemId: number, quantity: number): Promise { + await db.update(cartItems) + .set({ + quantity: sql`${cartItems.quantity} + ${quantity}`, + }) + .where(eq(cartItems.id, itemId)) +} + +export async function insertCartItem(userId: number, productId: number, quantity: number): Promise { + await db.insert(cartItems).values({ userId, productId, - quantity, - }).returning(); - return item; + quantity: quantity.toString(), + }) } -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 updateCartItemQuantity(userId: number, itemId: number, quantity: number) { + const [updatedItem] = await db.update(cartItems) + .set({ quantity: quantity.toString() }) + .where(and(eq(cartItems.id, itemId), eq(cartItems.userId, userId))) + .returning({ id: cartItems.id }) + + return !!updatedItem } -export async function removeFromCart(itemId: number): Promise { - await db.delete(cartItems).where(eq(cartItems.id, itemId)); +export async function deleteCartItem(userId: number, itemId: number): Promise { + const [deletedItem] = await db.delete(cartItems) + .where(and(eq(cartItems.id, itemId), eq(cartItems.userId, userId))) + .returning({ id: cartItems.id }) + + return !!deletedItem } -export async function clearCart(userId: number): Promise { - await db.delete(cartItems).where(eq(cartItems.userId, userId)); +export async function clearUserCart(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 index 314a273..02b2913 100644 --- a/packages/db_helper_postgres/src/user-apis/complaint.ts +++ b/packages/db_helper_postgres/src/user-apis/complaint.ts @@ -1,21 +1,39 @@ -import { db } from '../db/db_index'; -import { complaints } from '../db/schema'; -import { eq, desc } from 'drizzle-orm'; +import { db } from '../db/db_index' +import { complaints } from '../db/schema' +import { asc, eq } from 'drizzle-orm' +import type { InferSelectModel } from 'drizzle-orm' +import type { UserComplaint } from '@packages/shared' -export async function getUserComplaints(userId: number): Promise { - return await db.query.complaints.findMany({ - where: eq(complaints.userId, userId), - orderBy: desc(complaints.createdAt), - }); +type ComplaintRow = InferSelectModel + +export async function getUserComplaints(userId: number): Promise { + const userComplaints = await db + .select({ + id: complaints.id, + complaintBody: complaints.complaintBody, + response: complaints.response, + isResolved: complaints.isResolved, + createdAt: complaints.createdAt, + orderId: complaints.orderId, + }) + .from(complaints) + .where(eq(complaints.userId, userId)) + .orderBy(asc(complaints.createdAt)) + + return userComplaints.map((complaint) => ({ + id: complaint.id, + complaintBody: complaint.complaintBody, + response: complaint.response ?? null, + isResolved: complaint.isResolved, + createdAt: complaint.createdAt, + orderId: complaint.orderId ?? null, + })) } -export async function createComplaint(userId: number, orderId: number | null, complaintBody: string, images?: string[]): Promise { - const [complaint] = await db.insert(complaints).values({ +export async function createComplaint(userId: number, orderId: number | null, complaintBody: string): Promise { + 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 index cf167c6..61b11a2 100644 --- a/packages/db_helper_postgres/src/user-apis/coupon.ts +++ b/packages/db_helper_postgres/src/user-apis/coupon.ts @@ -1,43 +1,146 @@ -import { db } from '../db/db_index'; -import { coupons, couponUsage } from '../db/schema'; -import { eq, and } from 'drizzle-orm'; +import { db } from '../db/db_index' +import { + couponApplicableProducts, + couponApplicableUsers, + couponUsage, + coupons, + reservedCoupons, +} from '../db/schema' +import { and, eq, gt, isNull, or } from 'drizzle-orm' +import type { InferSelectModel } from 'drizzle-orm' +import type { UserCoupon, UserCouponApplicableProduct, UserCouponApplicableUser, UserCouponUsage, UserCouponWithRelations } from '@packages/shared' -export async function validateUserCoupon(code: string, userId: number, orderAmount: number): Promise { - const coupon = await db.query.coupons.findFirst({ - where: eq(coupons.couponCode, code.toUpperCase()), - }); +type CouponRow = InferSelectModel +type CouponUsageRow = InferSelectModel +type CouponApplicableUserRow = InferSelectModel +type CouponApplicableProductRow = InferSelectModel +type ReservedCouponRow = InferSelectModel - if (!coupon || coupon.isInvalidated) { - return { valid: false, message: 'Invalid coupon' }; - } +const mapCoupon = (coupon: CouponRow): UserCoupon => ({ + id: coupon.id, + couponCode: coupon.couponCode, + isUserBased: coupon.isUserBased, + discountPercent: coupon.discountPercent ? coupon.discountPercent.toString() : null, + flatDiscount: coupon.flatDiscount ? coupon.flatDiscount.toString() : null, + minOrder: coupon.minOrder ? coupon.minOrder.toString() : null, + productIds: coupon.productIds, + maxValue: coupon.maxValue ? coupon.maxValue.toString() : null, + isApplyForAll: coupon.isApplyForAll, + validTill: coupon.validTill ?? null, + maxLimitForUser: coupon.maxLimitForUser ?? null, + isInvalidated: coupon.isInvalidated, + exclusiveApply: coupon.exclusiveApply, + createdAt: coupon.createdAt, +}) - if (coupon.validTill && new Date(coupon.validTill) < new Date()) { - return { valid: false, message: 'Coupon expired' }; - } +const mapUsage = (usage: CouponUsageRow): UserCouponUsage => ({ + id: usage.id, + userId: usage.userId, + couponId: usage.couponId, + orderId: usage.orderId ?? null, + orderItemId: usage.orderItemId ?? null, + usedAt: usage.usedAt, +}) - let discountAmount = 0; - if (coupon.discountPercent) { - discountAmount = (orderAmount * parseFloat(coupon.discountPercent)) / 100; - } else if (coupon.flatDiscount) { - discountAmount = parseFloat(coupon.flatDiscount); - } +const mapApplicableUser = (applicable: CouponApplicableUserRow): UserCouponApplicableUser => ({ + id: applicable.id, + couponId: applicable.couponId, + userId: applicable.userId, +}) - if (coupon.maxValue) { - const maxDiscount = parseFloat(coupon.maxValue); - if (discountAmount > maxDiscount) { - discountAmount = maxDiscount; - } - } +const mapApplicableProduct = (applicable: CouponApplicableProductRow): UserCouponApplicableProduct => ({ + id: applicable.id, + couponId: applicable.couponId, + productId: applicable.productId, +}) - return { - valid: true, - discountAmount, - couponId: coupon.id, - }; +const mapCouponWithRelations = (coupon: CouponRow & { + usages: CouponUsageRow[] + applicableUsers: CouponApplicableUserRow[] + applicableProducts: CouponApplicableProductRow[] +}): UserCouponWithRelations => ({ + ...mapCoupon(coupon), + usages: coupon.usages.map(mapUsage), + applicableUsers: coupon.applicableUsers.map(mapApplicableUser), + applicableProducts: coupon.applicableProducts.map(mapApplicableProduct), +}) + +export async function getActiveCouponsWithRelations(userId: number): Promise { + const allCoupons = await db.query.coupons.findMany({ + where: and( + eq(coupons.isInvalidated, false), + or( + isNull(coupons.validTill), + gt(coupons.validTill, new Date()) + ) + ), + with: { + usages: { + where: eq(couponUsage.userId, userId), + }, + applicableUsers: true, + applicableProducts: true, + }, + }) + + return allCoupons.map(mapCouponWithRelations) } -export async function getUserCoupons(userId: number): Promise { - return await db.query.coupons.findMany({ - where: eq(coupons.userId, userId), - }); +export async function getAllCouponsWithRelations(userId: number): Promise { + const allCoupons = await db.query.coupons.findMany({ + with: { + usages: { + where: eq(couponUsage.userId, userId), + }, + applicableUsers: true, + applicableProducts: true, + }, + }) + + return allCoupons.map(mapCouponWithRelations) +} + +export async function getReservedCouponByCode(secretCode: string): Promise { + const reserved = await db.query.reservedCoupons.findFirst({ + where: and( + eq(reservedCoupons.secretCode, secretCode.toUpperCase()), + eq(reservedCoupons.isRedeemed, false) + ), + }) + + return reserved || null +} + +export async function redeemReservedCoupon(userId: number, reservedCoupon: ReservedCouponRow): Promise { + const couponResult = await db.transaction(async (tx) => { + const [coupon] = await tx.insert(coupons).values({ + couponCode: reservedCoupon.couponCode, + isUserBased: true, + discountPercent: reservedCoupon.discountPercent, + flatDiscount: reservedCoupon.flatDiscount, + minOrder: reservedCoupon.minOrder, + productIds: reservedCoupon.productIds, + maxValue: reservedCoupon.maxValue, + isApplyForAll: false, + validTill: reservedCoupon.validTill, + maxLimitForUser: reservedCoupon.maxLimitForUser, + exclusiveApply: reservedCoupon.exclusiveApply, + createdBy: reservedCoupon.createdBy, + }).returning() + + await tx.insert(couponApplicableUsers).values({ + couponId: coupon.id, + userId, + }) + + await tx.update(reservedCoupons).set({ + isRedeemed: true, + redeemedBy: userId, + redeemedAt: new Date(), + }).where(eq(reservedCoupons.id, reservedCoupon.id)) + + return coupon + }) + + return mapCoupon(couponResult) } diff --git a/packages/db_helper_postgres/src/user-apis/order.ts b/packages/db_helper_postgres/src/user-apis/order.ts index a36bd7c..19fbfe3 100644 --- a/packages/db_helper_postgres/src/user-apis/order.ts +++ b/packages/db_helper_postgres/src/user-apis/order.ts @@ -1,59 +1,624 @@ -import { db } from '../db/db_index'; -import { orders, orderItems, orderStatus } from '../db/schema'; -import { eq, desc } from 'drizzle-orm'; +import { db } from '../db/db_index' +import { + orders, + orderItems, + orderStatus, + addresses, + productInfo, + paymentInfoTable, + coupons, + couponUsage, + cartItems, + refunds, + units, + userDetails, + deliverySlotInfo, +} from '../db/schema' +import { and, eq, inArray, desc, gte, lte } from 'drizzle-orm' +import type { + UserOrderSummary, + UserOrderDetail, + UserRecentProduct, +} from '@packages/shared' -export async function getUserOrders(userId: number): Promise { - return await db.query.orders.findMany({ +export interface OrderItemInput { + productId: number + quantity: number + slotId: number | null +} + +export interface PlaceOrderInput { + userId: number + selectedItems: OrderItemInput[] + addressId: number + paymentMethod: 'online' | 'cod' + couponId?: number + userNotes?: string + isFlash?: boolean +} + +export interface OrderGroupData { + slotId: number | null + items: Array<{ + productId: number + quantity: number + slotId: number | null + product: typeof productInfo.$inferSelect + }> +} + +export interface PlacedOrder { + id: number + userId: number + addressId: number + slotId: number | null + totalAmount: string + deliveryCharge: string + isCod: boolean + isOnlinePayment: boolean + paymentInfoId: number | null + readableId: number + userNotes: string | null + orderGroupId: string + orderGroupProportion: string + isFlashDelivery: boolean + createdAt: Date +} + +export interface OrderWithRelations { + id: number + userId: number + addressId: number + slotId: number | null + totalAmount: string + deliveryCharge: string + isCod: boolean + isOnlinePayment: boolean + isFlashDelivery: boolean + userNotes: string | null + createdAt: Date + orderItems: Array<{ + id: number + productId: number + quantity: string + price: string + discountedPrice: string | null + is_packaged: boolean + product: { + id: number + name: string + images: unknown + } + }> + slot: { + deliveryTime: Date + } | null + paymentInfo: { + id: number + status: string + } | null + orderStatus: Array<{ + id: number + isCancelled: boolean + isDelivered: boolean + paymentStatus: string + cancelReason: string | null + }> + refunds: Array<{ + refundStatus: string + refundAmount: string | null + }> +} + +export interface OrderDetailWithRelations { + id: number + userId: number + addressId: number + slotId: number | null + totalAmount: string + deliveryCharge: string + isCod: boolean + isOnlinePayment: boolean + isFlashDelivery: boolean + userNotes: string | null + createdAt: Date + orderItems: Array<{ + id: number + productId: number + quantity: string + price: string + discountedPrice: string | null + is_packaged: boolean + product: { + id: number + name: string + images: unknown + } + }> + slot: { + deliveryTime: Date + } | null + paymentInfo: { + id: number + status: string + } | null + orderStatus: Array<{ + id: number + isCancelled: boolean + isDelivered: boolean + paymentStatus: string + cancelReason: string | null + }> + refunds: Array<{ + refundStatus: string + refundAmount: string | null + }> +} + +export interface CouponValidationResult { + id: number + couponCode: string + isInvalidated: boolean + validTill: Date | null + maxLimitForUser: number | null + minOrder: string | null + discountPercent: string | null + flatDiscount: string | null + maxValue: string | null + usages: Array<{ + id: number + userId: number + }> +} + +export interface CouponUsageWithCoupon { + id: number + couponId: number + orderId: number | null + coupon: { + id: number + couponCode: string + discountPercent: string | null + flatDiscount: string | null + maxValue: string | null + } +} + +export async function validateAndGetCoupon( + couponId: number | undefined, + userId: number, + totalAmount: number +): Promise { + if (!couponId) return null + + const coupon = await db.query.coupons.findFirst({ + where: eq(coupons.id, couponId), + with: { + usages: { where: eq(couponUsage.userId, userId) }, + }, + }) + + if (!coupon) throw new Error('Invalid coupon') + if (coupon.isInvalidated) throw new Error('Coupon is no longer valid') + if (coupon.validTill && new Date(coupon.validTill) < new Date()) + throw new Error('Coupon has expired') + if ( + coupon.maxLimitForUser && + coupon.usages.length >= coupon.maxLimitForUser + ) + throw new Error('Coupon usage limit exceeded') + if ( + coupon.minOrder && + parseFloat(coupon.minOrder.toString()) > totalAmount + ) + throw new Error('Order amount does not meet coupon minimum requirement') + + return coupon as CouponValidationResult +} + +export function applyDiscountToOrder( + orderTotal: number, + appliedCoupon: CouponValidationResult | null, + proportion: number +): { finalOrderTotal: number; orderGroupProportion: number } { + let finalOrderTotal = orderTotal + + if (appliedCoupon) { + if (appliedCoupon.discountPercent) { + const discount = Math.min( + (orderTotal * + parseFloat(appliedCoupon.discountPercent.toString())) / + 100, + appliedCoupon.maxValue + ? parseFloat(appliedCoupon.maxValue.toString()) * proportion + : Infinity + ) + finalOrderTotal -= discount + } else if (appliedCoupon.flatDiscount) { + const discount = Math.min( + parseFloat(appliedCoupon.flatDiscount.toString()) * proportion, + appliedCoupon.maxValue + ? parseFloat(appliedCoupon.maxValue.toString()) * proportion + : finalOrderTotal + ) + finalOrderTotal -= discount + } + } + + return { finalOrderTotal, orderGroupProportion: proportion } +} + +export async function getAddressByIdAndUser( + addressId: number, + userId: number +) { + return db.query.addresses.findFirst({ + where: and(eq(addresses.userId, userId), eq(addresses.id, addressId)), + }) +} + +export async function getProductById(productId: number) { + return db.query.productInfo.findFirst({ + where: eq(productInfo.id, productId), + }) +} + +export async function checkUserSuspended(userId: number): Promise { + const userDetail = await db.query.userDetails.findFirst({ + where: eq(userDetails.userId, userId), + }) + return userDetail?.isSuspended ?? false +} + +export async function getSlotCapacityStatus(slotId: number): Promise { + const slot = await db.query.deliverySlotInfo.findFirst({ + where: eq(deliverySlotInfo.id, slotId), + columns: { + isCapacityFull: true, + }, + }) + return slot?.isCapacityFull ?? false +} + +export async function placeOrderTransaction(params: { + userId: number + ordersData: Array<{ + order: Omit + orderItems: Omit[] + orderStatus: Omit + }> + paymentMethod: 'online' | 'cod' + totalWithDelivery: number +}): Promise { + const { userId, ordersData, paymentMethod } = params + + 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, index) => { + const od = ordersData[index] + od.orderItems.forEach((item) => { + allOrderItems.push({ ...item, orderId: order.id }) + }) + allOrderStatuses.push({ + ...od.orderStatus, + orderId: order.id, + }) + }) + + await tx.insert(orderItems).values(allOrderItems) + await tx.insert(orderStatus).values(allOrderStatuses) + + return insertedOrders as PlacedOrder[] + }) +} + +export async function deleteCartItemsForOrder( + userId: number, + productIds: number[] +): Promise { + await db.delete(cartItems).where( + and( + eq(cartItems.userId, userId), + inArray(cartItems.productId, productIds) + ) + ) +} + +export async function recordCouponUsage( + userId: number, + couponId: number, + orderId: number +): Promise { + await db.insert(couponUsage).values({ + userId, + couponId, + orderId, + orderItemId: null, + usedAt: new Date(), + }) +} + +export async function getOrdersWithRelations( + userId: number, + offset: number, + pageSize: number +): Promise { + return db.query.orders.findMany({ where: eq(orders.userId, userId), with: { orderItems: { with: { product: { - with: { - unit: true, + columns: { + id: true, + name: true, + images: true, }, }, }, }, - orderStatus: true, - slot: true, + slot: { + columns: { + deliveryTime: true, + }, + }, + paymentInfo: { + columns: { + id: true, + status: true, + }, + }, + orderStatus: { + columns: { + id: true, + isCancelled: true, + isDelivered: true, + paymentStatus: true, + cancelReason: true, + }, + }, + refunds: { + columns: { + refundStatus: true, + refundAmount: true, + }, + }, }, - orderBy: desc(orders.createdAt), - }); + orderBy: (orders, { desc }) => [desc(orders.createdAt)], + limit: pageSize, + offset: offset, + }) as Promise } -export async function getOrderById(orderId: number, userId: number): Promise { - return await db.query.orders.findFirst({ - where: eq(orders.id, orderId), +export async function getOrderCount(userId: number): Promise { + const result = await db + .select({ count: db.$count(orders, eq(orders.userId, userId)) }) + .from(orders) + .where(eq(orders.userId, userId)) + + return result[0]?.count ?? 0 +} + +export async function getOrderByIdWithRelations( + orderId: number, + userId: number +): Promise { + const order = await db.query.orders.findFirst({ + where: and(eq(orders.id, orderId), eq(orders.userId, userId)), with: { orderItems: { with: { - product: true, + product: { + columns: { + id: true, + name: true, + images: true, + }, + }, + }, + }, + slot: { + columns: { + deliveryTime: true, + }, + }, + paymentInfo: { + columns: { + id: true, + status: true, + }, + }, + orderStatus: { + columns: { + id: true, + isCancelled: true, + isDelivered: true, + paymentStatus: true, + cancelReason: true, + }, + with: { + refundCoupon: { + columns: { + id: true, + couponCode: true, + }, + }, + }, + }, + refunds: { + columns: { + refundStatus: true, + refundAmount: true, }, }, - orderStatus: true, - address: true, - slot: true, }, - }); + }) + + return order as OrderDetailWithRelations | null } -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; - }); +export async function getCouponUsageForOrder( + orderId: number +): Promise { + return db.query.couponUsage.findMany({ + where: eq(couponUsage.orderId, orderId), + with: { + coupon: { + columns: { + id: true, + couponCode: true, + discountPercent: true, + flatDiscount: true, + maxValue: true, + }, + }, + }, + }) as Promise +} + +export async function getOrderBasic(orderId: number) { + return db.query.orders.findFirst({ + where: eq(orders.id, orderId), + with: { + orderStatus: { + columns: { + id: true, + isCancelled: true, + isDelivered: true, + }, + }, + }, + }) +} + +export async function cancelOrderTransaction( + orderId: number, + statusId: number, + reason: string, + isCod: boolean +): Promise { + await db.transaction(async (tx) => { + await tx + .update(orderStatus) + .set({ + isCancelled: true, + cancelReason: reason, + cancellationUserNotes: reason, + cancellationReviewed: false, + }) + .where(eq(orderStatus.id, statusId)) + + const refundStatus = isCod ? 'na' : 'pending' + + await tx.insert(refunds).values({ + orderId, + refundStatus, + }) + }) +} + +export async function updateOrderNotes( + orderId: number, + userNotes: string +): Promise { + await db + .update(orders) + .set({ + userNotes: userNotes || null, + }) + .where(eq(orders.id, orderId)) +} + +export async function getRecentlyDeliveredOrderIds( + userId: number, + limit: number, + since: Date +): Promise { + const recentOrders = await 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) + + return recentOrders.map((order) => order.id) +} + +export async function getProductIdsFromOrders( + orderIds: number[] +): Promise { + const orderItemsResult = await db + .select({ productId: orderItems.productId }) + .from(orderItems) + .where(inArray(orderItems.orderId, orderIds)) + + return [...new Set(orderItemsResult.map((item) => item.productId))] +} + +export interface RecentProductData { + id: number + name: string + shortDescription: string | null + price: string + images: unknown + isOutOfStock: boolean + unitShortNotation: string + incrementStep: number +} + +export async function getProductsForRecentOrders( + productIds: number[], + limit: number +): Promise { + 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)) + .limit(limit) } diff --git a/packages/db_helper_postgres/src/user-apis/payments.ts b/packages/db_helper_postgres/src/user-apis/payments.ts index e09233a..5657bba 100644 --- a/packages/db_helper_postgres/src/user-apis/payments.ts +++ b/packages/db_helper_postgres/src/user-apis/payments.ts @@ -1,22 +1,51 @@ -import { db } from '../db/db_index'; -import { payments, orders, orderStatus } from '../db/schema'; -import { eq } from 'drizzle-orm'; +import { db } from '../db/db_index' +import { orders, payments, 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 getOrderById(orderId: number) { + return db.query.orders.findFirst({ + where: eq(orders.id, orderId), + }) } -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({ +export async function getPaymentByOrderId(orderId: number) { + return db.query.payments.findFirst({ where: eq(payments.orderId, orderId), - }); + }) +} + +export async function getPaymentByMerchantOrderId(merchantOrderId: string) { + return db.query.payments.findFirst({ + where: eq(payments.merchantOrderId, merchantOrderId), + }) +} + +export async function updatePaymentSuccess(merchantOrderId: string, payload: unknown) { + const [updatedPayment] = await db + .update(payments) + .set({ + status: 'success', + payload, + }) + .where(eq(payments.merchantOrderId, merchantOrderId)) + .returning({ + id: payments.id, + orderId: payments.orderId, + }) + + return updatedPayment || null +} + +export async function updateOrderPaymentStatus(orderId: number, status: 'pending' | 'success' | 'cod' | 'failed') { + await db + .update(orderStatus) + .set({ paymentStatus: status }) + .where(eq(orderStatus.orderId, orderId)) +} + +export async function markPaymentFailed(paymentId: number) { + await db + .update(payments) + .set({ status: 'failed' }) + .where(eq(payments.id, paymentId)) } diff --git a/packages/db_helper_postgres/src/user-apis/product.ts b/packages/db_helper_postgres/src/user-apis/product.ts index a2f4ecd..907bee3 100644 --- a/packages/db_helper_postgres/src/user-apis/product.ts +++ b/packages/db_helper_postgres/src/user-apis/product.ts @@ -1,41 +1,181 @@ -import { db } from '../db/db_index'; -import { productInfo, productReviews } from '../db/schema'; -import { eq, desc } from 'drizzle-orm'; +import { db } from '../db/db_index' +import { deliverySlotInfo, productInfo, productReviews, productSlots, specialDeals, storeInfo, units, users } from '../db/schema' +import { and, desc, eq, gt, sql } from 'drizzle-orm' +import type { UserProductDetailData, UserProductReview } from '@packages/shared' -export async function getAllProducts(): Promise { - return await db.query.productInfo.findMany({ - with: { - unit: true, - store: true, - specialDeals: true, - }, - orderBy: productInfo.name, - }); +const getStringArray = (value: unknown): string[] | null => { + if (!Array.isArray(value)) return null + return value.map((item) => String(item)) } -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 getProductDetailById(productId: number): Promise { + const productData = await 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) + + if (productData.length === 0) { + return null + } + + const product = productData[0] + + const storeData = product.storeId ? await db.query.storeInfo.findFirst({ + where: eq(storeInfo.id, product.storeId), + columns: { id: true, name: true, description: true }, + }) : null + + const deliverySlotsData = await 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), + eq(deliverySlotInfo.isCapacityFull, false), + gt(deliverySlotInfo.deliveryTime, sql`NOW()`), + gt(deliverySlotInfo.freezeTime, sql`NOW()`) + ) + ) + .orderBy(deliverySlotInfo.deliveryTime) + + const specialDealsData = await 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) + + return { + id: product.id, + name: product.name, + shortDescription: product.shortDescription ?? null, + longDescription: product.longDescription ?? null, + price: product.price.toString(), + marketPrice: product.marketPrice?.toString() || null, + unitNotation: product.unitShortNotation, + images: getStringArray(product.images), + isOutOfStock: product.isOutOfStock, + store: storeData ? { + id: storeData.id, + name: storeData.name, + description: storeData.description ?? null, + } : null, + incrementStep: product.incrementStep, + productQuantity: product.productQuantity, + isFlashAvailable: product.isFlashAvailable, + flashPrice: product.flashPrice?.toString() || null, + deliverySlots: deliverySlotsData, + specialDeals: specialDealsData.map((deal) => ({ + quantity: deal.quantity.toString(), + price: deal.price.toString(), + validTill: deal.validTill, + })), + } } -export async function createProductReview(userId: number, productId: number, rating: number, comment?: string): Promise { - const [review] = await db.insert(productReviews).values({ +export async function getProductReviews(productId: number, limit: number, offset: number) { + const reviews = await db + .select({ + id: productReviews.id, + reviewBody: productReviews.reviewBody, + ratings: productReviews.ratings, + imageUrls: productReviews.imageUrls, + reviewTime: productReviews.reviewTime, + userName: users.name, + }) + .from(productReviews) + .innerJoin(users, eq(productReviews.userId, users.id)) + .where(eq(productReviews.productId, productId)) + .orderBy(desc(productReviews.reviewTime)) + .limit(limit) + .offset(offset) + + const totalCountResult = await db + .select({ count: sql`count(*)` }) + .from(productReviews) + .where(eq(productReviews.productId, productId)) + + const totalCount = Number(totalCountResult[0].count) + + const mappedReviews: UserProductReview[] = reviews.map((review) => ({ + id: review.id, + reviewBody: review.reviewBody, + ratings: review.ratings, + imageUrls: getStringArray(review.imageUrls), + reviewTime: review.reviewTime, + userName: review.userName ?? null, + })) + + return { + reviews: mappedReviews, + totalCount, + } +} + +export async function getProductById(productId: number) { + return db.query.productInfo.findFirst({ + where: eq(productInfo.id, productId), + }) +} + +export async function createProductReview( + userId: number, + productId: number, + reviewBody: string, + ratings: number, + imageUrls: string[] +): Promise { + const [newReview] = await db.insert(productReviews).values({ userId, productId, - rating, - comment, - }).returning(); - return review; + reviewBody, + ratings, + imageUrls, + }).returning({ + id: productReviews.id, + reviewBody: productReviews.reviewBody, + ratings: productReviews.ratings, + imageUrls: productReviews.imageUrls, + reviewTime: productReviews.reviewTime, + }) + + return { + id: newReview.id, + reviewBody: newReview.reviewBody, + ratings: newReview.ratings, + imageUrls: getStringArray(newReview.imageUrls), + reviewTime: newReview.reviewTime, + userName: null, + } } diff --git a/packages/db_helper_postgres/src/user-apis/slots.ts b/packages/db_helper_postgres/src/user-apis/slots.ts index f01a97f..eeef416 100644 --- a/packages/db_helper_postgres/src/user-apis/slots.ts +++ b/packages/db_helper_postgres/src/user-apis/slots.ts @@ -1,17 +1,46 @@ -import { db } from '../db/db_index'; -import { deliverySlotInfo } from '../db/schema'; -import { eq, gte, and } from 'drizzle-orm'; +import { db } from '../db/db_index' +import { deliverySlotInfo, productInfo } from '../db/schema' +import { asc, eq } from 'drizzle-orm' +import type { InferSelectModel } from 'drizzle-orm' +import type { UserDeliverySlot, UserSlotAvailability } from '@packages/shared' -export async function getAvailableSlots(): Promise { - const now = new Date(); - return await db.query.deliverySlotInfo.findMany({ - where: gte(deliverySlotInfo.freezeTime, now), - orderBy: deliverySlotInfo.deliveryTime, - }); +type SlotRow = InferSelectModel + +const mapSlot = (slot: SlotRow): UserDeliverySlot => ({ + id: slot.id, + deliveryTime: slot.deliveryTime, + freezeTime: slot.freezeTime, + isActive: slot.isActive, + isFlash: slot.isFlash, + isCapacityFull: slot.isCapacityFull, + deliverySequence: slot.deliverySequence, + groupIds: slot.groupIds, +}) + +export async function getActiveSlotsList(): Promise { + const slots = await db.query.deliverySlotInfo.findMany({ + where: eq(deliverySlotInfo.isActive, true), + orderBy: asc(deliverySlotInfo.deliveryTime), + }) + + return slots.map(mapSlot) } -export async function getSlotById(id: number): Promise { - return await db.query.deliverySlotInfo.findFirst({ - where: eq(deliverySlotInfo.id, id), - }); +export async function getProductAvailability(): Promise { + const products = await db + .select({ + id: productInfo.id, + name: productInfo.name, + isOutOfStock: productInfo.isOutOfStock, + isFlashAvailable: productInfo.isFlashAvailable, + }) + .from(productInfo) + .where(eq(productInfo.isSuspended, false)) + + return products.map((product) => ({ + id: product.id, + name: product.name, + isOutOfStock: product.isOutOfStock, + isFlashAvailable: product.isFlashAvailable, + })) } diff --git a/packages/db_helper_postgres/src/user-apis/stores.ts b/packages/db_helper_postgres/src/user-apis/stores.ts index 24feccf..2ae1875 100644 --- a/packages/db_helper_postgres/src/user-apis/stores.ts +++ b/packages/db_helper_postgres/src/user-apis/stores.ts @@ -1,20 +1,127 @@ -import { db } from '../db/db_index'; -import { storeInfo } from '../db/schema'; -import { eq } from 'drizzle-orm'; +import { db } from '../db/db_index' +import { productInfo, storeInfo, units } from '../db/schema' +import { and, eq, sql } from 'drizzle-orm' +import type { InferSelectModel } from 'drizzle-orm' +import type { UserStoreDetailData, UserStoreProductData, UserStoreSummaryData } from '@packages/shared' -export async function getAllStores(): Promise { - return await db.query.storeInfo.findMany({ - with: { - owner: true, - }, - }); +type StoreRow = InferSelectModel +type StoreProductRow = { + id: number + name: string + shortDescription: string | null + price: string + marketPrice: string | null + images: unknown + isOutOfStock: boolean + incrementStep: number + unitShortNotation: string + productQuantity: number } -export async function getStoreById(id: number): Promise { - return await db.query.storeInfo.findFirst({ - where: eq(storeInfo.id, id), - with: { - owner: true, - }, - }); +const getStringArray = (value: unknown): string[] | null => { + if (!Array.isArray(value)) return null + return value.map((item) => String(item)) +} + +export async function getStoreSummaries(): Promise { + const storesData = await db + .select({ + id: storeInfo.id, + name: storeInfo.name, + description: storeInfo.description, + imageUrl: storeInfo.imageUrl, + productCount: sql`count(${productInfo.id})`.as('productCount'), + }) + .from(storeInfo) + .leftJoin( + productInfo, + and(eq(productInfo.storeId, storeInfo.id), eq(productInfo.isSuspended, false)) + ) + .groupBy(storeInfo.id) + + const storesWithDetails = await Promise.all( + storesData.map(async (store) => { + const sampleProducts = await db + .select({ + id: productInfo.id, + name: productInfo.name, + images: productInfo.images, + }) + .from(productInfo) + .where(and(eq(productInfo.storeId, store.id), eq(productInfo.isSuspended, false))) + .limit(3) + + return { + id: store.id, + name: store.name, + description: store.description ?? null, + imageUrl: store.imageUrl ?? null, + productCount: store.productCount || 0, + sampleProducts: sampleProducts.map((product) => ({ + id: product.id, + name: product.name, + images: getStringArray(product.images), + })), + } + }) + ) + + return storesWithDetails +} + +export async function getStoreDetail(storeId: number): Promise { + const storeData = await db.query.storeInfo.findFirst({ + where: eq(storeInfo.id, storeId), + columns: { + id: true, + name: true, + description: true, + imageUrl: true, + }, + }) + + if (!storeData) { + return null + } + + const productsData = await db + .select({ + id: productInfo.id, + name: productInfo.name, + shortDescription: productInfo.shortDescription, + price: productInfo.price, + marketPrice: productInfo.marketPrice, + images: productInfo.images, + isOutOfStock: productInfo.isOutOfStock, + incrementStep: productInfo.incrementStep, + unitShortNotation: units.shortNotation, + productQuantity: productInfo.productQuantity, + }) + .from(productInfo) + .innerJoin(units, eq(productInfo.unitId, units.id)) + .where(and(eq(productInfo.storeId, storeId), eq(productInfo.isSuspended, false))) + + const products = productsData.map((product: StoreProductRow): UserStoreProductData => ({ + id: product.id, + name: product.name, + shortDescription: product.shortDescription ?? null, + price: product.price.toString(), + marketPrice: product.marketPrice ? product.marketPrice.toString() : null, + incrementStep: product.incrementStep, + unit: product.unitShortNotation, + unitNotation: product.unitShortNotation, + images: getStringArray(product.images), + isOutOfStock: product.isOutOfStock, + productQuantity: product.productQuantity, + })) + + return { + store: { + id: storeData.id, + name: storeData.name, + description: storeData.description ?? null, + imageUrl: storeData.imageUrl ?? null, + }, + products, + } } diff --git a/packages/db_helper_postgres/src/user-apis/tags.ts b/packages/db_helper_postgres/src/user-apis/tags.ts index bbd09eb..49524b2 100644 --- a/packages/db_helper_postgres/src/user-apis/tags.ts +++ b/packages/db_helper_postgres/src/user-apis/tags.ts @@ -5,11 +5,11 @@ import { eq } from 'drizzle-orm'; export async function getAllTags(): Promise { return await db.query.productTags.findMany({ with: { - products: { - with: { - product: true, - }, - }, + // products: { + // with: { + // product: true, + // }, + // }, }, }); } @@ -18,11 +18,11 @@ export async function getTagById(id: number): Promise { return await db.query.productTags.findFirst({ where: eq(productTags.id, id), with: { - products: { - with: { - product: true, - }, - }, + // 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 index defc0df..9b35803 100644 --- a/packages/db_helper_postgres/src/user-apis/user.ts +++ b/packages/db_helper_postgres/src/user-apis/user.ts @@ -1,44 +1,75 @@ -import { db } from '../db/db_index'; -import { users, userDetails, addresses } from '../db/schema'; -import { eq, desc } from 'drizzle-orm'; +import { db } from '../db/db_index' +import { notifCreds, unloggedUserTokens, userCreds, userDetails, users } from '../db/schema' +import { and, eq } 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 getUserById(userId: number) { + const [user] = await db.select().from(users).where(eq(users.id, userId)).limit(1) + return user || null } -export async function updateUser(userId: number, updates: any): Promise { - const [user] = await db.update(users) - .set(updates) +export async function getUserDetailByUserId(userId: number) { + const [detail] = await db.select().from(userDetails).where(eq(userDetails.userId, userId)).limit(1) + return detail || null +} + +export async function getUserWithCreds(userId: number) { + const result = await db + .select() + .from(users) + .leftJoin(userCreds, eq(users.id, userCreds.userId)) .where(eq(users.id, userId)) - .returning(); - return user; + .limit(1) + + if (result.length === 0) return null + return { + user: result[0].users, + creds: result[0].user_creds, + } } -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 getNotifCred(userId: number, token: string) { + return db.query.notifCreds.findFirst({ + where: and(eq(notifCreds.userId, userId), eq(notifCreds.token, token)), + }) } -export async function createAddress(addressData: any): Promise { - const [address] = await db.insert(addresses).values(addressData).returning(); - return address; +export async function upsertNotifCred(userId: number, token: string): Promise { + const existing = await getNotifCred(userId, token) + if (existing) { + await db.update(notifCreds) + .set({ lastVerified: new Date() }) + .where(eq(notifCreds.id, existing.id)) + return + } + + await db.insert(notifCreds).values({ + userId, + token, + lastVerified: new Date(), + }) } -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 deleteUnloggedToken(token: string): Promise { + await db.delete(unloggedUserTokens).where(eq(unloggedUserTokens.token, token)) } -export async function deleteAddress(addressId: number): Promise { - await db.delete(addresses).where(eq(addresses.id, addressId)); +export async function getUnloggedToken(token: string) { + return db.query.unloggedUserTokens.findFirst({ + where: eq(unloggedUserTokens.token, token), + }) +} + +export async function upsertUnloggedToken(token: string): Promise { + const existing = await getUnloggedToken(token) + if (existing) { + await db.update(unloggedUserTokens) + .set({ lastVerified: new Date() }) + .where(eq(unloggedUserTokens.id, existing.id)) + return + } + + await db.insert(unloggedUserTokens).values({ + token, + lastVerified: new Date(), + }) } diff --git a/packages/shared/types/user.ts b/packages/shared/types/user.ts index 0c257b7..f45bd16 100644 --- a/packages/shared/types/user.ts +++ b/packages/shared/types/user.ts @@ -28,6 +28,41 @@ export interface Address { isDefault: boolean; } +export interface UserAddress { + id: number; + userId: number; + name: string; + phone: string; + addressLine1: string; + addressLine2: string | null; + city: string; + state: string; + pincode: string; + isDefault: boolean; + latitude: number | null; + longitude: number | null; + googleMapsUrl: string | null; + adminLatitude: number | null; + adminLongitude: number | null; + zoneId: number | null; + createdAt: Date; +} + +export interface UserAddressResponse { + success: boolean; + data: UserAddress | null; +} + +export interface UserAddressesResponse { + success: boolean; + data: UserAddress[]; +} + +export interface UserAddressDeleteResponse { + success: boolean; + message: string; +} + export interface Product { id: number; name: string; @@ -80,3 +115,531 @@ export interface Payment { amount: string; createdAt: Date; } + +export interface UserBanner { + 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 UserBannersResponse { + banners: UserBanner[]; +} + +export interface UserCartProduct { + id: number; + name: string; + price: string; + productQuantity: number; + unit: string; + isOutOfStock: boolean; + images: string[]; +} + +export interface UserCartItem { + id: number; + productId: number; + quantity: number; + addedAt: Date; + product: UserCartProduct; + subtotal: number; +} + +export interface UserCartResponse { + items: UserCartItem[]; + totalItems: number; + totalAmount: number; + message?: string; +} + +export interface UserComplaint { + id: number; + complaintBody: string; + response: string | null; + isResolved: boolean; + createdAt: Date; + orderId: number | null; +} + +export interface UserComplaintsResponse { + complaints: UserComplaint[]; +} + +export interface UserRaiseComplaintResponse { + success: boolean; + message: string; +} + +export interface UserTagSummary { + id: number; + tagName: string; + tagDescription: string | null; + imageUrl: string | null; + productIds: number[]; +} + +export interface UserStoreSampleProduct { + id: number; + name: string; + signedImageUrl: string | null; +} + +export interface UserStoreSampleProductData { + id: number; + name: string; + images: string[] | null; +} + +export interface UserStoreSummary { + id: number; + name: string; + description: string | null; + signedImageUrl: string | null; + productCount: number; + sampleProducts: UserStoreSampleProduct[]; +} + +export interface UserStoreSummaryData { + id: number; + name: string; + description: string | null; + imageUrl: string | null; + productCount: number; + sampleProducts: UserStoreSampleProductData[]; +} + +export interface UserStoresResponse { + stores: UserStoreSummary[]; +} + +export interface UserStoreProduct { + id: number; + name: string; + shortDescription: string | null; + price: string; + marketPrice: string | null; + incrementStep: number; + unit: string; + unitNotation: string; + images: string[]; + isOutOfStock: boolean; + productQuantity: number; +} + +export interface UserStoreProductData { + id: number; + name: string; + shortDescription: string | null; + price: string; + marketPrice: string | null; + incrementStep: number; + unit: string; + unitNotation: string; + images: string[] | null; + isOutOfStock: boolean; + productQuantity: number; +} + +export interface UserStoreDetail { + store: { + id: number; + name: string; + description: string | null; + signedImageUrl: string | null; + }; + products: UserStoreProduct[]; + tags: UserTagSummary[]; +} + +export interface UserStoreDetailData { + store: { + id: number; + name: string; + description: string | null; + imageUrl: string | null; + }; + products: UserStoreProductData[]; +} + +export interface UserProductStoreInfo { + id: number; + name: string; + description: string | null; +} + +export interface UserProductDeliverySlot { + id: number; + deliveryTime: Date; + freezeTime: Date; +} + +export interface UserProductSpecialDeal { + quantity: string; + price: string; + validTill: Date; +} + +export interface UserProductDetailData { + id: number; + name: string; + shortDescription: string | null; + longDescription: string | null; + price: string; + marketPrice: string | null; + unitNotation: string; + images: string[] | null; + isOutOfStock: boolean; + store: UserProductStoreInfo | null; + incrementStep: number; + productQuantity: number; + isFlashAvailable: boolean; + flashPrice: string | null; + deliverySlots: UserProductDeliverySlot[]; + specialDeals: UserProductSpecialDeal[]; +} + +export interface UserProductDetail extends UserProductDetailData { + images: string[]; +} + +export interface UserProductReview { + id: number; + reviewBody: string; + ratings: number; + imageUrls: string[] | null; + reviewTime: Date; + userName: string | null; +} + +export interface UserProductReviewWithSignedUrls extends UserProductReview { + signedImageUrls: string[]; +} + +export interface UserProductReviewsResponse { + reviews: UserProductReviewWithSignedUrls[]; + hasMore: boolean; +} + +export interface UserCreateReviewResponse { + success: boolean; + review: UserProductReview; +} + +export interface UserSlotProduct { + id: number; + name: string; + shortDescription: string | null; + productQuantity: number; + price: string; + marketPrice: string | null; + unit: string | null; + images: string[]; + isOutOfStock: boolean; + storeId: number | null; + nextDeliveryDate: Date; +} + +export interface UserSlotWithProducts { + id: number; + deliveryTime: Date; + freezeTime: Date; + isActive: boolean; + isCapacityFull: boolean; + products: UserSlotProduct[]; +} + +export interface UserSlotData { + slotId: number; + deliveryTime: Date; + freezeTime: Date; + products: UserSlotProduct[]; +} + +export interface UserSlotAvailability { + id: number; + name: string; + isOutOfStock: boolean; + isFlashAvailable: boolean; +} + +export interface UserDeliverySlot { + id: number; + deliveryTime: Date; + freezeTime: Date; + isActive: boolean; + isFlash: boolean; + isCapacityFull: boolean; + deliverySequence: unknown; + groupIds: unknown; +} + +export interface UserSlotsResponse { + slots: UserSlotWithProducts[]; + count: number; +} + +export interface UserSlotsListResponse { + slots: UserDeliverySlot[]; + count: number; +} + +export interface UserSlotsWithProductsResponse { + slots: UserSlotWithProducts[]; + productAvailability: UserSlotAvailability[]; + count: number; +} + +export interface UserPaymentOrderResponse { + razorpayOrderId: string | number; + key: string | undefined; +} + +export interface UserPaymentVerifyResponse { + success: boolean; + message: string; +} + +export interface UserPaymentFailResponse { + success: boolean; + message: string; +} + +export interface UserAuthProfile { + id: number; + name?: string | null; + email: string | null; + mobile: string | null; + createdAt: string; + profileImage: string | null; + bio?: string | null; + dateOfBirth?: string | null; + gender?: string | null; + occupation?: string | null; +} + +export interface UserAuthResponse { + token: string; + user: UserAuthProfile; +} + +export interface UserAuthResult { + success: boolean; + data: UserAuthResponse; +} + +export interface UserOtpVerifyResponse { + success: boolean; + token: string; + user: { + id: number; + name: string | null; + email: string | null; + mobile: string | null; + createdAt: string; + profileImage: string | null; + }; +} + +export interface UserPasswordUpdateResponse { + success: boolean; + message: string; +} + +export interface UserProfileResponse { + success: boolean; + data: { + id: number; + name: string | null; + email: string | null; + mobile: string | null; + }; +} + +export interface UserDeleteAccountResponse { + success: boolean; + message: string; +} + +export interface UserCouponUsage { + id: number; + userId: number; + couponId: number; + orderId: number | null; + orderItemId: number | null; + usedAt: Date; +} + +export interface UserCouponApplicableUser { + id: number; + couponId: number; + userId: number; +} + +export interface UserCouponApplicableProduct { + id: number; + couponId: number; + productId: number; +} + +export interface UserCoupon { + id: number; + couponCode: string; + isUserBased: boolean; + discountPercent: string | null; + flatDiscount: string | null; + minOrder: string | null; + productIds: unknown; + maxValue: string | null; + isApplyForAll: boolean; + validTill: Date | null; + maxLimitForUser: number | null; + isInvalidated: boolean; + exclusiveApply: boolean; + createdAt: Date; +} + +export interface UserCouponWithRelations extends UserCoupon { + usages: UserCouponUsage[]; + applicableUsers: UserCouponApplicableUser[]; + applicableProducts: UserCouponApplicableProduct[]; +} + +export interface UserEligibleCouponsResponse { + success: boolean; + data: UserCouponWithRelations[]; +} + +export interface UserCouponDisplay { + id: number; + code: string; + discountType: 'percentage' | 'flat'; + discountValue: number; + maxValue?: number; + minOrder?: number; + description: string; + validTill?: Date; + usageCount: number; + maxLimitForUser?: number; + isExpired: boolean; + isUsedUp: boolean; +} + +export interface UserMyCouponsResponse { + success: boolean; + data: { + personal: UserCouponDisplay[]; + general: UserCouponDisplay[]; + }; +} + +export interface UserRedeemCouponResponse { + success: boolean; + coupon: UserCoupon; +} + +export interface UserSelfDataResponse { + success: boolean; + data: { + user: { + id: number; + name: string | null; + email: string | null; + mobile: string | null; + profileImage?: string | null; + bio?: string | null; + dateOfBirth?: string | null; + gender?: string | null; + occupation?: string | null; + }; + }; +} + +export interface UserProfileCompleteResponse { + isComplete: boolean; +} + +export interface UserSavePushTokenResponse { + success: boolean; +} + +export interface UserOrderItemSummary { + productName: string; + quantity: number; + price: number; + discountedPrice: number; + amount: number; + image: string | null; +} + +export interface UserOrderSummary { + id: number; + orderId: string; + orderDate: string; + deliveryStatus: string; + deliveryDate?: string; + orderStatus: string; + cancelReason: string | null; + paymentMode: string; + totalAmount: number; + deliveryCharge: number; + paymentStatus: string; + refundStatus: string; + refundAmount: number | null; + userNotes: string | null; + items: UserOrderItemSummary[]; + isFlashDelivery: boolean; + createdAt: string; +} + +export interface UserOrdersResponse { + success: boolean; + data: UserOrderSummary[]; + pagination: { + page: number; + pageSize: number; + totalCount: number; + totalPages: number; + }; +} + +export interface UserOrderDetail extends UserOrderSummary { + cancellationStatus: string; + couponCode: string | null; + couponDescription: string | null; + discountAmount: number | null; + orderAmount: number; +} + +export interface UserCancelOrderResponse { + success: boolean; + message: string; +} + +export interface UserUpdateNotesResponse { + success: boolean; + message: string; +} + +export interface UserRecentProduct { + id: number; + name: string; + shortDescription: string | null; + price: string; + images: string[]; + isOutOfStock: boolean; + unit: string; + incrementStep: number; + nextDeliveryDate: string | null; +} + +export interface UserRecentProductsResponse { + success: boolean; + products: UserRecentProduct[]; +}