This commit is contained in:
shafi54 2026-03-26 00:34:31 +05:30
parent 4414f9f64b
commit fe05769343
28 changed files with 3189 additions and 1280 deletions

View file

@ -125,6 +125,61 @@ export {
updateSlotCapacity, updateSlotCapacity,
getSlotDeliverySequence, getSlotDeliverySequence,
updateSlotDeliverySequence, 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 // Order methods
updateOrderNotes, updateOrderNotes,
updateOrderPackaged, updateOrderPackaged,
@ -137,6 +192,26 @@ export {
rebalanceSlots, rebalanceSlots,
cancelOrder, cancelOrder,
deleteOrderById, 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' } from 'postgresService'
export async function getOrderDetails(orderId: number): Promise<AdminOrderDetails | null> { export async function getOrderDetails(orderId: number): Promise<AdminOrderDetails | null> {
@ -220,6 +295,72 @@ export type {
AdminVendorOrderSummary, AdminVendorOrderSummary,
AdminUpcomingSlotsResult, AdminUpcomingSlotsResult,
AdminVendorUpdatePackagingResult, 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'; } from '@packages/shared';
export type { export type {

View file

@ -1,30 +1,52 @@
import { router, protectedProcedure } from '@/src/trpc/trpc-index'; import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'; import { z } from 'zod'
import { db } from '@/src/db/db_index'; import { extractCoordsFromRedirectUrl } from '@/src/lib/license-util'
import { addresses, orders, orderStatus, deliverySlotInfo } from '@/src/db/schema'; import {
import { eq, and, gte } from 'drizzle-orm'; getUserDefaultAddress as getDefaultAddressInDb,
import dayjs from 'dayjs'; getUserAddresses as getUserAddressesInDb,
import { extractCoordsFromRedirectUrl } from '@/src/lib/license-util'; 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({ export const addressRouter = router({
getDefaultAddress: protectedProcedure getDefaultAddress: protectedProcedure
.query(async ({ ctx }) => { .query(async ({ ctx }): Promise<UserAddressResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
const defaultAddress = await getDefaultAddressInDb(userId)
/*
// Old implementation - direct DB queries:
const [defaultAddress] = await db const [defaultAddress] = await db
.select() .select()
.from(addresses) .from(addresses)
.where(and(eq(addresses.userId, userId), eq(addresses.isDefault, true))) .where(and(eq(addresses.userId, userId), eq(addresses.isDefault, true)))
.limit(1); .limit(1);
*/
return { success: true, data: defaultAddress || null }; return { success: true, data: defaultAddress }
}), }),
getUserAddresses: protectedProcedure getUserAddresses: protectedProcedure
.query(async ({ ctx }) => { .query(async ({ ctx }): Promise<UserAddressesResponse> => {
const userId = ctx.user.userId; 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)); const userAddresses = await db.select().from(addresses).where(eq(addresses.userId, userId));
return { success: true, data: userAddresses }; */
return { success: true, data: userAddresses }
}), }),
createAddress: protectedProcedure createAddress: protectedProcedure
@ -41,7 +63,7 @@ export const addressRouter = router({
longitude: z.number().optional(), longitude: z.number().optional(),
googleMapsUrl: z.string().optional(), googleMapsUrl: z.string().optional(),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<UserAddressResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
const { name, phone, addressLine1, addressLine2, city, state, pincode, isDefault, googleMapsUrl } = input; 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 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) { if (isDefault) {
await db.update(addresses).set({ isDefault: false }).where(eq(addresses.userId, userId)); await db.update(addresses).set({ isDefault: false }).where(eq(addresses.userId, userId));
} }
@ -79,8 +122,9 @@ export const addressRouter = router({
longitude, longitude,
googleMapsUrl, googleMapsUrl,
}).returning(); }).returning();
*/
return { success: true, data: newAddress }; return { success: true, data: newAddress }
}), }),
updateAddress: protectedProcedure updateAddress: protectedProcedure
@ -98,7 +142,7 @@ export const addressRouter = router({
longitude: z.number().optional(), longitude: z.number().optional(),
googleMapsUrl: z.string().optional(), googleMapsUrl: z.string().optional(),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<UserAddressResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
const { id, name, phone, addressLine1, addressLine2, city, state, pincode, isDefault, googleMapsUrl } = input; 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 // 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); const existingAddress = await getUserAddressByIdInDb(userId, id)
if (existingAddress.length === 0) { if (!existingAddress) {
throw new Error('Address not found'); throw new Error('Address not found')
} }
// If setting as default, unset other defaults // 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) { if (isDefault) {
await db.update(addresses).set({ isDefault: false }).where(eq(addresses.userId, userId)); 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(); 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 deleteAddress: protectedProcedure
.input(z.object({ .input(z.object({
id: z.number().int().positive(), id: z.number().int().positive(),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<UserAddressDeleteResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
const { id } = input; 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); const existingAddress = await db.select().from(addresses).where(and(eq(addresses.id, id), eq(addresses.userId, userId))).limit(1);
if (existingAddress.length === 0) { if (existingAddress.length === 0) {
throw new Error('Address not found or does not belong to user'); 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({ const ongoingOrders = await db.select({
order: orders, order: orders,
status: orderStatus, 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.'); throw new Error('Address is attached to an ongoing order. Please cancel the order first.');
} }
// Prevent deletion of default address
if (existingAddress[0].isDefault) { if (existingAddress[0].isDefault) {
throw new Error('Cannot delete default address. Please set another address as default first.'); 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))); 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' }
}), }),
}); });

View file

@ -1,23 +1,33 @@
import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index'; import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'; import { z } from 'zod'
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs'
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken'
import { eq } from 'drizzle-orm'; import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client'
import { db } from '@/src/db/db_index'; import { ApiError } from '@/src/lib/api-error'
import { jwtSecret } from '@/src/lib/env-exporter'
import { sendOtp, verifyOtpUtil, getOtpCreds } from '@/src/lib/otp-utils'
import { import {
users, userCreds, userDetails, addresses, cartItems, complaints, getUserAuthByEmail as getUserAuthByEmailInDb,
couponApplicableUsers, couponUsage, notifCreds, notifications, getUserAuthByMobile as getUserAuthByMobileInDb,
orderItems, orderStatus, orders, payments, refunds, getUserAuthById as getUserAuthByIdInDb,
productReviews, reservedCoupons getUserAuthCreds as getUserAuthCredsInDb,
} from '@/src/db/schema'; getUserAuthDetails as getUserAuthDetailsInDb,
import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client'; createUserAuthWithCreds as createUserAuthWithCredsInDb,
import { ApiError } from '@/src/lib/api-error'; createUserAuthWithMobile as createUserAuthWithMobileInDb,
import catchAsync from '@/src/lib/catch-async'; upsertUserAuthPassword as upsertUserAuthPasswordInDb,
import { jwtSecret } from '@/src/lib/env-exporter'; deleteUserAuthAccount as deleteUserAuthAccountInDb,
import { sendOtp, verifyOtpUtil, getOtpCreds } from '@/src/lib/otp-utils'; } from '@/src/dbService'
import type {
UserAuthResult,
UserAuthResponse,
UserOtpVerifyResponse,
UserPasswordUpdateResponse,
UserProfileResponse,
UserDeleteAccountResponse,
} from '@packages/shared'
interface LoginRequest { interface LoginRequest {
identifier: string; // email or mobile identifier: string;
password: string; password: string;
} }
@ -28,22 +38,6 @@ interface RegisterRequest {
password: string; 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 generateToken = (userId: number): string => {
const secret = jwtSecret; const secret = jwtSecret;
if (!secret) { if (!secret) {
@ -61,7 +55,7 @@ export const authRouter = router({
identifier: z.string().min(1, 'Email/mobile is required'), identifier: z.string().min(1, 'Email/mobile is required'),
password: z.string().min(1, 'Password is required'), password: z.string().min(1, 'Password is required'),
})) }))
.mutation(async ({ input }) => { .mutation(async ({ input }): Promise<UserAuthResult> => {
const { identifier, password }: LoginRequest = input; const { identifier, password }: LoginRequest = input;
if (!identifier || !password) { if (!identifier || !password) {
@ -69,22 +63,13 @@ export const authRouter = router({
} }
// Find user by email or mobile // Find user by email or mobile
const [user] = await db const user = await getUserAuthByEmailInDb(identifier.toLowerCase())
.select() let foundUser = user || null
.from(users)
.where(eq(users.email, identifier.toLowerCase()))
.limit(1);
let foundUser = user;
if (!foundUser) { if (!foundUser) {
// Try mobile if email didn't work // Try mobile if email didn't work
const [userByMobile] = await db const userByMobile = await getUserAuthByMobileInDb(identifier)
.select() foundUser = userByMobile || null
.from(users)
.where(eq(users.mobile, identifier))
.limit(1);
foundUser = userByMobile;
} }
if (!foundUser) { if (!foundUser) {
@ -92,22 +77,14 @@ export const authRouter = router({
} }
// Get user credentials // Get user credentials
const [userCredentials] = await db const userCredentials = await getUserAuthCredsInDb(foundUser.id)
.select()
.from(userCreds)
.where(eq(userCreds.userId, foundUser.id))
.limit(1);
if (!userCredentials) { if (!userCredentials) {
throw new ApiError('Account setup incomplete. Please contact support.', 401); throw new ApiError('Account setup incomplete. Please contact support.', 401);
} }
// Get user details for profile image // Get user details for profile image
const [userDetail] = await db const userDetail = await getUserAuthDetailsInDb(foundUser.id)
.select()
.from(userDetails)
.where(eq(userDetails.userId, foundUser.id))
.limit(1);
// Generate signed URL for profile image if it exists // Generate signed URL for profile image if it exists
const profileImageSignedUrl = userDetail?.profileImage const profileImageSignedUrl = userDetail?.profileImage
@ -122,7 +99,7 @@ export const authRouter = router({
const token = generateToken(foundUser.id); const token = generateToken(foundUser.id);
const response: AuthResponse = { const response: UserAuthResponse = {
token, token,
user: { user: {
id: foundUser.id, id: foundUser.id,
@ -141,7 +118,7 @@ export const authRouter = router({
return { return {
success: true, success: true,
data: response, data: response,
}; }
}), }),
register: publicProcedure register: publicProcedure
@ -151,7 +128,7 @@ export const authRouter = router({
mobile: z.string().min(1, 'Mobile is required'), mobile: z.string().min(1, 'Mobile is required'),
password: z.string().min(1, 'Password is required'), password: z.string().min(1, 'Password is required'),
})) }))
.mutation(async ({ input }) => { .mutation(async ({ input }): Promise<UserAuthResult> => {
const { name, email, mobile, password }: RegisterRequest = input; const { name, email, mobile, password }: RegisterRequest = input;
if (!name || !email || !mobile || !password) { if (!name || !email || !mobile || !password) {
@ -171,22 +148,14 @@ export const authRouter = router({
} }
// Check if email already exists // Check if email already exists
const [existingEmail] = await db const existingEmail = await getUserAuthByEmailInDb(email.toLowerCase())
.select()
.from(users)
.where(eq(users.email, email.toLowerCase()))
.limit(1);
if (existingEmail) { if (existingEmail) {
throw new ApiError('Email already registered', 409); throw new ApiError('Email already registered', 409);
} }
// Check if mobile already exists // Check if mobile already exists
const [existingMobile] = await db const existingMobile = await getUserAuthByMobileInDb(cleanMobile)
.select()
.from(users)
.where(eq(users.mobile, cleanMobile))
.limit(1);
if (existingMobile) { if (existingMobile) {
throw new ApiError('Mobile number already registered', 409); throw new ApiError('Mobile number already registered', 409);
@ -196,31 +165,16 @@ export const authRouter = router({
const hashedPassword = await bcrypt.hash(password, 12); const hashedPassword = await bcrypt.hash(password, 12);
// Create user and credentials in a transaction // Create user and credentials in a transaction
const newUser = await db.transaction(async (tx) => { const newUser = await createUserAuthWithCredsInDb({
// Create user
const [user] = await tx
.insert(users)
.values({
name: name.trim(), name: name.trim(),
email: email.toLowerCase().trim(), email: email.toLowerCase().trim(),
mobile: cleanMobile, mobile: cleanMobile,
hashedPassword,
}) })
.returning();
// Create user credentials
await tx
.insert(userCreds)
.values({
userId: user.id,
userPassword: hashedPassword,
});
return user;
});
const token = generateToken(newUser.id); const token = generateToken(newUser.id);
const response: AuthResponse = { const response: UserAuthResponse = {
token, token,
user: { user: {
id: newUser.id, id: newUser.id,
@ -235,7 +189,7 @@ export const authRouter = router({
return { return {
success: true, success: true,
data: response, data: response,
}; }
}), }),
sendOtp: publicProcedure sendOtp: publicProcedure
@ -252,7 +206,7 @@ export const authRouter = router({
mobile: z.string(), mobile: z.string(),
otp: z.string(), otp: z.string(),
})) }))
.mutation(async ({ input }) => { .mutation(async ({ input }): Promise<UserOtpVerifyResponse> => {
const verificationId = getOtpCreds(input.mobile); const verificationId = getOtpCreds(input.mobile);
if (!verificationId) { if (!verificationId) {
throw new ApiError("OTP not sent or expired", 400); throw new ApiError("OTP not sent or expired", 400);
@ -264,21 +218,11 @@ export const authRouter = router({
} }
// Find user // Find user
let user = await db.query.users.findFirst({ let user = await getUserAuthByMobileInDb(input.mobile)
where: eq(users.mobile, input.mobile),
});
// If user doesn't exist, create one // If user doesn't exist, create one
if (!user) { if (!user) {
const [newUser] = await db user = await createUserAuthWithMobileInDb(input.mobile)
.insert(users)
.values({
name: null,
email: null,
mobile: input.mobile,
})
.returning();
user = newUser;
} }
// Generate JWT // Generate JWT
@ -295,14 +239,14 @@ export const authRouter = router({
createdAt: user.createdAt.toISOString(), createdAt: user.createdAt.toISOString(),
profileImage: null, profileImage: null,
}, },
}; }
}), }),
updatePassword: protectedProcedure updatePassword: protectedProcedure
.input(z.object({ .input(z.object({
password: z.string().min(6, 'Password must be at least 6 characters'), password: z.string().min(6, 'Password must be at least 6 characters'),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<UserPasswordUpdateResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
if (!userId) { if (!userId) {
throw new ApiError('User not authenticated', 401); throw new ApiError('User not authenticated', 401);
@ -311,41 +255,38 @@ export const authRouter = router({
const hashedPassword = await bcrypt.hash(input.password, 10); const hashedPassword = await bcrypt.hash(input.password, 10);
// Insert if not exists, then update if exists // Insert if not exists, then update if exists
await upsertUserAuthPasswordInDb(userId, hashedPassword)
/*
// Old implementation - direct DB queries:
try { try {
await db.insert(userCreds).values({ await db.insert(userCreds).values({
userId: userId, userId: userId,
userPassword: hashedPassword, userPassword: hashedPassword,
}); });
// Insert succeeded - new credentials created
} catch (error: any) { } catch (error: any) {
// Insert failed - check if it's a unique constraint violation if (error.code === '23505') {
if (error.code === '23505') { // PostgreSQL unique constraint violation
// Update existing credentials
await db.update(userCreds).set({ await db.update(userCreds).set({
userPassword: hashedPassword, userPassword: hashedPassword,
}).where(eq(userCreds.userId, userId)); }).where(eq(userCreds.userId, userId));
} else { } else {
// Re-throw if it's a different error
throw error; throw error;
} }
} }
*/
return { success: true, message: 'Password updated successfully' }; return { success: true, message: 'Password updated successfully' }
}), }),
getProfile: protectedProcedure getProfile: protectedProcedure
.query(async ({ ctx }) => { .query(async ({ ctx }): Promise<UserProfileResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
if (!userId) { if (!userId) {
throw new ApiError('User not authenticated', 401); throw new ApiError('User not authenticated', 401);
} }
const [user] = await db const user = await getUserAuthByIdInDb(userId)
.select()
.from(users)
.where(eq(users.id, userId))
.limit(1);
if (!user) { if (!user) {
throw new ApiError('User not found', 404); throw new ApiError('User not found', 404);
@ -359,14 +300,14 @@ export const authRouter = router({
email: user.email, email: user.email,
mobile: user.mobile, mobile: user.mobile,
}, },
}; }
}), }),
deleteAccount: protectedProcedure deleteAccount: protectedProcedure
.input(z.object({ .input(z.object({
mobile: z.string().min(10, 'Mobile number is required'), mobile: z.string().min(10, 'Mobile number is required'),
})) }))
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }): Promise<UserDeleteAccountResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
const { mobile } = input; const { mobile } = input;
@ -375,10 +316,7 @@ export const authRouter = router({
} }
// Double-check: verify user exists and is the authenticated user // Double-check: verify user exists and is the authenticated user
const existingUser = await db.query.users.findFirst({ const existingUser = await getUserAuthByIdInDb(userId)
where: eq(users.id, userId),
columns: { id: true, mobile: true },
});
if (!existingUser) { if (!existingUser) {
throw new ApiError('User not found', 404); throw new ApiError('User not found', 404);
@ -399,8 +337,11 @@ export const authRouter = router({
} }
// Use transaction for atomic deletion // Use transaction for atomic deletion
await deleteUserAuthAccountInDb(userId)
/*
// Old implementation - direct DB queries:
await db.transaction(async (tx) => { 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(notifCreds).where(eq(notifCreds.userId, userId));
await tx.delete(couponApplicableUsers).where(eq(couponApplicableUsers.userId, userId)); await tx.delete(couponApplicableUsers).where(eq(couponApplicableUsers.userId, userId));
await tx.delete(couponUsage).where(eq(couponUsage.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(cartItems).where(eq(cartItems.userId, userId));
await tx.delete(notifications).where(eq(notifications.userId, userId)); await tx.delete(notifications).where(eq(notifications.userId, userId));
await tx.delete(productReviews).where(eq(productReviews.userId, userId)); await tx.delete(productReviews).where(eq(productReviews.userId, userId));
// Update reserved coupons (set redeemedBy to null)
await tx.update(reservedCoupons) await tx.update(reservedCoupons)
.set({ redeemedBy: null }) .set({ redeemedBy: null })
.where(eq(reservedCoupons.redeemedBy, userId)); .where(eq(reservedCoupons.redeemedBy, userId));
// Phase 2: Order dependencies
const userOrders = await tx const userOrders = await tx
.select({ id: orders.id }) .select({ id: orders.id })
.from(orders) .from(orders)
@ -425,23 +363,18 @@ export const authRouter = router({
await tx.delete(orderStatus).where(eq(orderStatus.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(payments).where(eq(payments.orderId, order.id));
await tx.delete(refunds).where(eq(refunds.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(couponUsage).where(eq(couponUsage.orderId, order.id));
await tx.delete(complaints).where(eq(complaints.orderId, order.id)); await tx.delete(complaints).where(eq(complaints.orderId, order.id));
} }
// Delete orders
await tx.delete(orders).where(eq(orders.userId, userId)); 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)); 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(userDetails).where(eq(userDetails.userId, userId));
await tx.delete(userCreds).where(eq(userCreds.userId, userId)); await tx.delete(userCreds).where(eq(userCreds.userId, userId));
await tx.delete(users).where(eq(users.id, userId)); await tx.delete(users).where(eq(users.id, userId));
}); });
*/
return { success: true, message: 'Account deleted successfully' }; return { success: true, message: 'Account deleted successfully' }
}), }),
}); });

View file

@ -1,24 +1,27 @@
import { db } from '@/src/db/db_index'; import { publicProcedure, router } from '@/src/trpc/trpc-index'
import { homeBanners } from '@/src/db/schema'; import { scaffoldAssetUrl } from '@/src/lib/s3-client'
import { publicProcedure, router } from '@/src/trpc/trpc-index'; import { getUserActiveBanners as getUserActiveBannersInDb } from '@/src/dbService'
import { scaffoldAssetUrl } from '@/src/lib/s3-client'; import type { UserBannersResponse } from '@packages/shared'
import { isNotNull, asc } from 'drizzle-orm';
export async function scaffoldBanners() { export async function scaffoldBanners(): Promise<UserBannersResponse> {
const banners = await getUserActiveBannersInDb()
/*
// Old implementation - direct DB queries:
const banners = await db.query.homeBanners.findMany({ const banners = await db.query.homeBanners.findMany({
where: isNotNull(homeBanners.serialNum), // Only show assigned banners where: isNotNull(homeBanners.serialNum), // Only show assigned banners
orderBy: asc(homeBanners.serialNum), // Order by slot number 1-4 orderBy: asc(homeBanners.serialNum), // Order by slot number 1-4
}); });
*/
// Convert S3 keys to signed URLs for client
const bannersWithSignedUrls = banners.map((banner) => ({ const bannersWithSignedUrls = banners.map((banner) => ({
...banner, ...banner,
imageUrl: banner.imageUrl ? scaffoldAssetUrl(banner.imageUrl) : banner.imageUrl, imageUrl: banner.imageUrl ? scaffoldAssetUrl(banner.imageUrl) : banner.imageUrl,
})); }))
return { return {
banners: bannersWithSignedUrls, banners: bannersWithSignedUrls,
}; }
} }
export const bannerRouter = router({ export const bannerRouter = router({

View file

@ -1,19 +1,25 @@
import { router, protectedProcedure, publicProcedure } from '@/src/trpc/trpc-index'; import { router, protectedProcedure, publicProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'; import { z } from 'zod'
import { db } from '@/src/db/db_index'; import { ApiError } from '@/src/lib/api-error'
import { cartItems, productInfo, units, productSlots, deliverySlotInfo } from '@/src/db/schema'; import { scaffoldAssetUrl } from '@/src/lib/s3-client'
import { eq, and, sql, inArray, gt } from 'drizzle-orm'; import { getMultipleProductsSlots } from '@/src/stores/slot-store'
import { ApiError } from '@/src/lib/api-error'; import {
import { generateSignedUrlsFromS3Urls, scaffoldAssetUrl } from '@/src/lib/s3-client'; getUserCartItemsWithProducts as getUserCartItemsWithProductsInDb,
import { getProductSlots, getMultipleProductsSlots } from '@/src/stores/slot-store'; 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 { const getCartData = async (userId: number): Promise<UserCartResponse> => {
items: any[]; const cartItemsWithProducts = await getUserCartItemsWithProductsInDb(userId)
totalItems: number;
totalAmount: number;
}
const getCartData = async (userId: number): Promise<CartResponse> => { /*
// Old implementation - direct DB queries:
const cartItemsWithProducts = await db const cartItemsWithProducts = await db
.select({ .select({
cartId: cartItems.id, cartId: cartItems.id,
@ -31,39 +37,28 @@ const getCartData = async (userId: number): Promise<CartResponse> => {
.innerJoin(productInfo, eq(cartItems.productId, productInfo.id)) .innerJoin(productInfo, eq(cartItems.productId, productInfo.id))
.innerJoin(units, eq(productInfo.unitId, units.id)) .innerJoin(units, eq(productInfo.unitId, units.id))
.where(eq(cartItems.userId, userId)); .where(eq(cartItems.userId, userId));
*/
// Generate signed URLs for images const cartWithSignedUrls = cartItemsWithProducts.map((item) => ({
const cartWithSignedUrls = await Promise.all( ...item,
cartItemsWithProducts.map(async (item) => ({
id: item.cartId,
productId: item.productId,
quantity: parseFloat(item.quantity),
addedAt: item.addedAt,
product: { product: {
id: item.productId, ...item.product,
name: item.productName, images: scaffoldAssetUrl(item.product.images || []),
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 totalAmount = cartWithSignedUrls.reduce((sum, item) => sum + item.subtotal, 0); const totalAmount = cartWithSignedUrls.reduce((sum, item) => sum + item.subtotal, 0)
return { return {
items: cartWithSignedUrls, items: cartWithSignedUrls,
totalItems: cartWithSignedUrls.length, totalItems: cartWithSignedUrls.length,
totalAmount, totalAmount,
}; }
}; }
export const cartRouter = router({ export const cartRouter = router({
getCart: protectedProcedure getCart: protectedProcedure
.query(async ({ ctx }): Promise<CartResponse> => { .query(async ({ ctx }): Promise<UserCartResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
return await getCartData(userId); return await getCartData(userId);
}), }),
@ -73,7 +68,7 @@ export const cartRouter = router({
productId: z.number().int().positive(), productId: z.number().int().positive(),
quantity: z.number().int().positive(), quantity: z.number().int().positive(),
})) }))
.mutation(async ({ input, ctx }): Promise<CartResponse> => { .mutation(async ({ input, ctx }): Promise<UserCartResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
const { productId, quantity } = input; const { productId, quantity } = input;
@ -83,6 +78,22 @@ export const cartRouter = router({
} }
// Check if product exists // 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({ const product = await db.query.productInfo.findFirst({
where: eq(productInfo.id, productId), where: eq(productInfo.id, productId),
}); });
@ -91,29 +102,27 @@ export const cartRouter = router({
throw new ApiError("Product not found", 404); throw new ApiError("Product not found", 404);
} }
// Check if item already exists in cart
const existingItem = await db.query.cartItems.findFirst({ const existingItem = await db.query.cartItems.findFirst({
where: and(eq(cartItems.userId, userId), eq(cartItems.productId, productId)), where: and(eq(cartItems.userId, userId), eq(cartItems.productId, productId)),
}); });
if (existingItem) { if (existingItem) {
// Update quantity
await db.update(cartItems) await db.update(cartItems)
.set({ .set({
quantity: sql`${cartItems.quantity} + ${quantity}`, quantity: sql`${cartItems.quantity} + ${quantity}`,
}) })
.where(eq(cartItems.id, existingItem.id)); .where(eq(cartItems.id, existingItem.id));
} else { } else {
// Insert new item
await db.insert(cartItems).values({ await db.insert(cartItems).values({
userId, userId,
productId, productId,
quantity: quantity.toString(), quantity: quantity.toString(),
}); });
} }
*/
// Return updated cart // Return updated cart
return await getCartData(userId); return await getCartData(userId)
}), }),
updateCartItem: protectedProcedure updateCartItem: protectedProcedure
@ -121,7 +130,7 @@ export const cartRouter = router({
itemId: z.number().int().positive(), itemId: z.number().int().positive(),
quantity: z.number().int().min(0), quantity: z.number().int().min(0),
})) }))
.mutation(async ({ input, ctx }): Promise<CartResponse> => { .mutation(async ({ input, ctx }): Promise<UserCartResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
const { itemId, quantity } = input; const { itemId, quantity } = input;
@ -129,6 +138,10 @@ export const cartRouter = router({
throw new ApiError("Positive quantity required", 400); 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) const [updatedItem] = await db.update(cartItems)
.set({ quantity: quantity.toString() }) .set({ quantity: quantity.toString() })
.where(and( .where(and(
@ -140,19 +153,28 @@ export const cartRouter = router({
if (!updatedItem) { if (!updatedItem) {
throw new ApiError("Cart item not found", 404); throw new ApiError("Cart item not found", 404);
} }
*/
if (!updated) {
throw new ApiError('Cart item not found', 404)
}
// Return updated cart // Return updated cart
return await getCartData(userId); return await getCartData(userId)
}), }),
removeFromCart: protectedProcedure removeFromCart: protectedProcedure
.input(z.object({ .input(z.object({
itemId: z.number().int().positive(), itemId: z.number().int().positive(),
})) }))
.mutation(async ({ input, ctx }): Promise<CartResponse> => { .mutation(async ({ input, ctx }): Promise<UserCartResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
const { itemId } = input; const { itemId } = input;
const deleted = await deleteUserCartItemInDb(userId, itemId)
/*
// Old implementation - direct DB queries:
const [deletedItem] = await db.delete(cartItems) const [deletedItem] = await db.delete(cartItems)
.where(and( .where(and(
eq(cartItems.id, itemId), eq(cartItems.id, itemId),
@ -163,23 +185,33 @@ export const cartRouter = router({
if (!deletedItem) { if (!deletedItem) {
throw new ApiError("Cart item not found", 404); throw new ApiError("Cart item not found", 404);
} }
*/
if (!deleted) {
throw new ApiError('Cart item not found', 404)
}
// Return updated cart // Return updated cart
return await getCartData(userId); return await getCartData(userId)
}), }),
clearCart: protectedProcedure clearCart: protectedProcedure
.mutation(async ({ ctx }) => { .mutation(async ({ ctx }): Promise<UserCartResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
await clearUserCartInDb(userId)
/*
// Old implementation - direct DB query:
await db.delete(cartItems).where(eq(cartItems.userId, userId)); await db.delete(cartItems).where(eq(cartItems.userId, userId));
*/
return { return {
items: [], items: [],
totalItems: 0, totalItems: 0,
totalAmount: 0, totalAmount: 0,
message: "Cart cleared successfully", message: "Cart cleared successfully",
}; }
}), }),
// Original DB-based getCartSlots (commented out) // Original DB-based getCartSlots (commented out)

View file

@ -1,14 +1,20 @@
import { router, protectedProcedure } from '@/src/trpc/trpc-index'; import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'; import { z } from 'zod'
import { db } from '@/src/db/db_index'; import {
import { complaints } from '@/src/db/schema'; getUserComplaints as getUserComplaintsInDb,
import { eq } from 'drizzle-orm'; createUserComplaint as createUserComplaintInDb,
} from '@/src/dbService'
import type { UserComplaintsResponse, UserRaiseComplaintResponse } from '@packages/shared'
export const complaintRouter = router({ export const complaintRouter = router({
getAll: protectedProcedure getAll: protectedProcedure
.query(async ({ ctx }) => { .query(async ({ ctx }): Promise<UserComplaintsResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
const userComplaints = await getUserComplaintsInDb(userId)
/*
// Old implementation - direct DB queries:
const userComplaints = await db const userComplaints = await db
.select({ .select({
id: complaints.id, id: complaints.id,
@ -32,6 +38,11 @@ export const complaintRouter = router({
orderId: c.orderId, orderId: c.orderId,
})), })),
}; };
*/
return {
complaints: userComplaints,
}
}), }),
raise: protectedProcedure raise: protectedProcedure
@ -39,7 +50,7 @@ export const complaintRouter = router({
orderId: z.string().optional(), orderId: z.string().optional(),
complaintBody: z.string().min(1, 'Complaint body is required'), complaintBody: z.string().min(1, 'Complaint body is required'),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<UserRaiseComplaintResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
const { orderId, complaintBody } = input; 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({ await db.insert(complaints).values({
userId, userId,
orderId: orderIdNum, orderId: orderIdNum,
complaintBody: complaintBody.trim(), complaintBody: complaintBody.trim(),
}); });
*/
return { success: true, message: 'Complaint raised successfully' }; return { success: true, message: 'Complaint raised successfully' }
}), }),
}); });

View file

@ -1,31 +1,20 @@
import { router, protectedProcedure } from '@/src/trpc/trpc-index'; import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'; import { z } from 'zod'
import { db } from '@/src/db/db_index'; import { ApiError } from '@/src/lib/api-error'
import { coupons, couponUsage, couponApplicableUsers, reservedCoupons, couponApplicableProducts } from '@/src/db/schema'; import {
import { eq, and, or, gt, isNull, sql } from 'drizzle-orm'; getUserActiveCouponsWithRelations as getUserActiveCouponsWithRelationsInDb,
import { ApiError } from '@/src/lib/api-error'; 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'; const generateCouponDescription = (coupon: { discountPercent?: string | null; flatDiscount?: string | null; minOrder?: string | null; maxValue?: string | null }): string => {
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 => {
let desc = ''; let desc = '';
if (coupon.discountPercent) { if (coupon.discountPercent) {
@ -45,29 +34,17 @@ const generateCouponDescription = (coupon: any): string => {
return desc; 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({ export const userCouponRouter = router({
getEligible: protectedProcedure getEligible: protectedProcedure
.query(async ({ ctx }) => { .query(async ({ ctx }): Promise<UserEligibleCouponsResponse> => {
try { try {
const userId = ctx.user.userId; 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({ const allCoupons = await db.query.coupons.findMany({
where: and( where: and(
eq(coupons.isInvalidated, false), eq(coupons.isInvalidated, false),
@ -92,6 +69,7 @@ export const userCouponRouter = router({
}, },
} }
}); });
*/
// Filter to only coupons applicable to current user // Filter to only coupons applicable to current user
const applicableCoupons = allCoupons.filter(coupon => { const applicableCoupons = allCoupons.filter(coupon => {
@ -110,11 +88,15 @@ export const userCouponRouter = router({
getProductCoupons: protectedProcedure getProductCoupons: protectedProcedure
.input(z.object({ productId: z.number().int().positive() })) .input(z.object({ productId: z.number().int().positive() }))
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }): Promise<UserEligibleCouponsResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
const { productId } = input; const { productId } = input;
// Get all active, non-expired coupons // Get all active, non-expired coupons
const allCoupons = await getUserActiveCouponsWithRelationsInDb(userId)
/*
// Old implementation - direct DB queries:
const allCoupons = await db.query.coupons.findMany({ const allCoupons = await db.query.coupons.findMany({
where: and( where: and(
eq(coupons.isInvalidated, false), eq(coupons.isInvalidated, false),
@ -139,6 +121,7 @@ export const userCouponRouter = router({
}, },
} }
}); });
*/
// Filter to only coupons applicable to current user and product // Filter to only coupons applicable to current user and product
const applicableCoupons = allCoupons.filter(coupon => { const applicableCoupons = allCoupons.filter(coupon => {
@ -155,10 +138,13 @@ export const userCouponRouter = router({
}), }),
getMyCoupons: protectedProcedure getMyCoupons: protectedProcedure
.query(async ({ ctx }) => { .query(async ({ ctx }): Promise<UserMyCouponsResponse> => {
const userId = ctx.user.userId; 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({ const allCoupons = await db.query.coupons.findMany({
with: { with: {
usages: { usages: {
@ -171,9 +157,10 @@ export const userCouponRouter = router({
} }
} }
}); });
*/
// Filter coupons in JS: not invalidated, applicable to user, and not expired // 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 isNotInvalidated = !coupon.isInvalidated;
const applicableUsers = coupon.applicableUsers || []; const applicableUsers = coupon.applicableUsers || [];
const isApplicable = coupon.isApplyForAll || applicableUsers.some(au => au.userId === userId); const isApplicable = coupon.isApplyForAll || applicableUsers.some(au => au.userId === userId);
@ -182,15 +169,15 @@ export const userCouponRouter = router({
}); });
// Categorize coupons // Categorize coupons
const personalCoupons: CouponDisplay[] = []; const personalCoupons: UserCouponDisplay[] = [];
const generalCoupons: CouponDisplay[] = []; const generalCoupons: UserCouponDisplay[] = [];
applicableCoupons.forEach(coupon => { applicableCoupons.forEach(coupon => {
const usageCount = coupon.usages.length; const usageCount = coupon.usages.length;
const isExpired = false; // Already filtered out expired coupons const isExpired = false; // Already filtered out expired coupons
const isUsedUp = Boolean(coupon.maxLimitForUser && usageCount >= coupon.maxLimitForUser); const isUsedUp = Boolean(coupon.maxLimitForUser && usageCount >= coupon.maxLimitForUser);
const couponDisplay: CouponDisplay = { const couponDisplay: UserCouponDisplay = {
id: coupon.id, id: coupon.id,
code: coupon.couponCode, code: coupon.couponCode,
discountType: coupon.discountPercent ? 'percentage' : 'flat', discountType: coupon.discountPercent ? 'percentage' : 'flat',
@ -225,17 +212,21 @@ export const userCouponRouter = router({
redeemReservedCoupon: protectedProcedure redeemReservedCoupon: protectedProcedure
.input(z.object({ secretCode: z.string() })) .input(z.object({ secretCode: z.string() }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<UserRedeemCouponResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
const { secretCode } = input; const { secretCode } = input;
// Find the reserved coupon const reservedCoupon = await getUserReservedCouponByCodeInDb(secretCode)
/*
// Old implementation - direct DB queries:
const reservedCoupon = await db.query.reservedCoupons.findFirst({ const reservedCoupon = await db.query.reservedCoupons.findFirst({
where: and( where: and(
eq(reservedCoupons.secretCode, secretCode.toUpperCase()), eq(reservedCoupons.secretCode, secretCode.toUpperCase()),
eq(reservedCoupons.isRedeemed, false) eq(reservedCoupons.isRedeemed, false)
), ),
}); });
*/
if (!reservedCoupon) { if (!reservedCoupon) {
throw new ApiError("Invalid or already redeemed coupon code", 400); 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); 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) => { const couponResult = await db.transaction(async (tx) => {
// Insert into coupons
const couponInsert = await tx.insert(coupons).values({ const couponInsert = await tx.insert(coupons).values({
couponCode: reservedCoupon.couponCode, couponCode: reservedCoupon.couponCode,
isUserBased: true, isUserBased: true,
@ -266,22 +259,11 @@ export const userCouponRouter = router({
const coupon = couponInsert[0]; const coupon = couponInsert[0];
// Insert into couponApplicableUsers
await tx.insert(couponApplicableUsers).values({ await tx.insert(couponApplicableUsers).values({
couponId: coupon.id, couponId: coupon.id,
userId, 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({ await tx.update(reservedCoupons).set({
isRedeemed: true, isRedeemed: true,
redeemedBy: userId, redeemedBy: userId,
@ -290,6 +272,7 @@ export const userCouponRouter = router({
return coupon; return coupon;
}); });
*/
return { success: true, coupon: couponResult }; return { success: true, coupon: couponResult };
}), }),

View file

@ -1,108 +1,43 @@
import { router, protectedProcedure } from "@/src/trpc/trpc-index"; import { router, protectedProcedure } from "@/src/trpc/trpc-index";
import { z } from "zod"; import { z } from "zod";
import { db } from "@/src/db/db_index";
import { import {
orders, validateAndGetUserCoupon,
orderItems, applyDiscountToUserOrder,
orderStatus, getUserAddressByIdAndUser,
addresses, getOrderProductById,
productInfo, checkUserSuspended,
paymentInfoTable, getUserSlotCapacityStatus,
coupons, placeUserOrderTransaction,
couponUsage, deleteUserCartItemsForOrder,
payments, recordUserCouponUsage,
cartItems, getUserOrdersWithRelations,
refunds, getUserOrderCount,
units, getUserOrderByIdWithRelations,
userDetails, getUserCouponUsageForOrder,
} from "@/src/db/schema"; getUserOrderBasic,
import { eq, and, inArray, desc, gte, lte } from "drizzle-orm"; 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 { scaffoldAssetUrl } from "@/src/lib/s3-client";
import { ApiError } from "@/src/lib/api-error"; import { ApiError } from "@/src/lib/api-error";
import { import {
sendOrderPlacedNotification, sendOrderPlacedNotification,
sendOrderCancelledNotification, sendOrderCancelledNotification,
} from "@/src/lib/notif-job"; } 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 { CONST_KEYS, getConstant, getConstants } from "@/src/lib/const-store";
import { publishFormattedOrder, publishCancellation } from "@/src/lib/post-order-handler"; import { publishFormattedOrder, publishCancellation } from "@/src/lib/post-order-handler";
import { getSlotById } from "@/src/stores/slot-store"; import { getSlotById } from "@/src/stores/slot-store";
import type {
UserOrdersResponse,
const validateAndGetCoupon = async ( UserOrderDetail,
couponId: number | undefined, UserCancelOrderResponse,
userId: number, UserUpdateNotesResponse,
totalAmount: number UserRecentProductsResponse,
) => { } from "@/src/dbService";
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 };
};
const placeOrderUtil = async (params: { const placeOrderUtil = async (params: {
userId: number; userId: number;
@ -139,9 +74,7 @@ const placeOrderUtil = async (params: {
const orderGroupId = `${Date.now()}-${userId}`; const orderGroupId = `${Date.now()}-${userId}`;
const address = await db.query.addresses.findFirst({ const address = await getUserAddressByIdAndUser(addressId, userId);
where: and(eq(addresses.userId, userId), eq(addresses.id, addressId)),
});
if (!address) { if (!address) {
throw new ApiError("Invalid address", 400); throw new ApiError("Invalid address", 400);
} }
@ -152,14 +85,12 @@ const placeOrderUtil = async (params: {
productId: number; productId: number;
quantity: number; quantity: number;
slotId: number | null; slotId: number | null;
product: any; product: Awaited<ReturnType<typeof getOrderProductById>>;
}> }>
>(); >();
for (const item of selectedItems) { for (const item of selectedItems) {
const product = await db.query.productInfo.findFirst({ const product = await getOrderProductById(item.productId);
where: eq(productInfo.id, item.productId),
});
if (!product) { if (!product) {
throw new ApiError(`Product ${item.productId} not found`, 400); throw new ApiError(`Product ${item.productId} not found`, 400);
} }
@ -172,9 +103,7 @@ const placeOrderUtil = async (params: {
if (params.isFlash) { if (params.isFlash) {
for (const item of selectedItems) { for (const item of selectedItems) {
const product = await db.query.productInfo.findFirst({ const product = await getOrderProductById(item.productId);
where: eq(productInfo.id, item.productId),
});
if (!product?.isFlashAvailable) { if (!product?.isFlashAvailable) {
throw new ApiError(`Product ${item.productId} is not available for flash delivery`, 400); 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) { for (const [slotId, items] of ordersBySlot) {
const orderTotal = items.reduce( const orderTotal = items.reduce(
(sum, item) => { (sum, item) => {
if (!item.product) return sum
const itemPrice = params.isFlash const itemPrice = params.isFlash
? parseFloat((item.product.flashPrice || item.product.price).toString()) ? parseFloat((item.product.flashPrice || item.product.price).toString())
: parseFloat(item.product.price.toString()); : parseFloat(item.product.price.toString());
@ -195,13 +125,16 @@ const placeOrderUtil = async (params: {
totalAmount += orderTotal; totalAmount += orderTotal;
} }
const appliedCoupon = await validateAndGetCoupon(couponId, userId, totalAmount); const appliedCoupon = await validateAndGetUserCoupon(couponId, userId, totalAmount);
const expectedDeliveryCharge = const expectedDeliveryCharge =
totalAmount < minOrderValue ? deliveryCharge : 0; totalAmount < minOrderValue ? deliveryCharge : 0;
const totalWithDelivery = totalAmount + expectedDeliveryCharge; const totalWithDelivery = totalAmount + expectedDeliveryCharge;
const { db } = await import("postgresService");
const { orders, orderItems, orderStatus } = await import("postgresService");
type OrderData = { type OrderData = {
order: Omit<typeof orders.$inferInsert, "id">; order: Omit<typeof orders.$inferInsert, "id">;
orderItems: Omit<typeof orderItems.$inferInsert, "id">[]; orderItems: Omit<typeof orderItems.$inferInsert, "id">[];
@ -214,6 +147,7 @@ const placeOrderUtil = async (params: {
for (const [slotId, items] of ordersBySlot) { for (const [slotId, items] of ordersBySlot) {
const subOrderTotal = items.reduce( const subOrderTotal = items.reduce(
(sum, item) => { (sum, item) => {
if (!item.product) return sum
const itemPrice = params.isFlash const itemPrice = params.isFlash
? parseFloat((item.product.flashPrice || item.product.price).toString()) ? parseFloat((item.product.flashPrice || item.product.price).toString())
: parseFloat(item.product.price.toString()); : parseFloat(item.product.price.toString());
@ -226,7 +160,7 @@ const placeOrderUtil = async (params: {
const orderGroupProportion = subOrderTotal / totalAmount; const orderGroupProportion = subOrderTotal / totalAmount;
const orderTotalAmount = isFirstOrder ? subOrderTotalWithDelivery : subOrderTotal; const orderTotalAmount = isFirstOrder ? subOrderTotalWithDelivery : subOrderTotal;
const { finalOrderTotal: finalOrderAmount } = applyDiscountToOrder( const { finalOrderTotal: finalOrderAmount } = applyDiscountToUserOrder(
orderTotalAmount, orderTotalAmount,
appliedCoupon, appliedCoupon,
orderGroupProportion orderGroupProportion
@ -248,18 +182,20 @@ const placeOrderUtil = async (params: {
isFlashDelivery: params.isFlash, isFlashDelivery: params.isFlash,
}; };
const orderItemsData: Omit<typeof orderItems.$inferInsert, "id">[] = items.map( const orderItemsData: Omit<typeof orderItems.$inferInsert, "id">[] = items
.filter((item) => item.product !== null && item.product !== undefined)
.map(
(item) => ({ (item) => ({
orderId: 0, orderId: 0,
productId: item.productId, productId: item.productId,
quantity: item.quantity.toString(), quantity: item.quantity.toString(),
price: params.isFlash price: params.isFlash
? item.product.flashPrice || item.product.price ? item.product!.flashPrice || item.product!.price
: item.product.price, : item.product!.price,
discountedPrice: ( discountedPrice: (
params.isFlash params.isFlash
? item.product.flashPrice || item.product.price ? item.product!.flashPrice || item.product!.price
: item.product.price : item.product!.price
).toString(), ).toString(),
}) })
); );
@ -274,79 +210,24 @@ const placeOrderUtil = async (params: {
isFirstOrder = false; isFirstOrder = false;
} }
const createdOrders = await db.transaction(async (tx) => { const createdOrders = await placeUserOrderTransaction({
let sharedPaymentInfoId: number | null = null; userId,
if (paymentMethod === "online") { ordersData,
const [paymentInfo] = await tx paymentMethod,
.insert(paymentInfoTable) totalWithDelivery,
.values({
status: "pending",
gateway: "razorpay",
merchantOrderId: `multi_order_${Date.now()}`,
})
.returning();
sharedPaymentInfoId = paymentInfo.id;
}
const ordersToInsert: Omit<typeof orders.$inferInsert, "id">[] = ordersData.map(
(od) => ({
...od.order,
paymentInfoId: sharedPaymentInfoId,
})
);
const insertedOrders = await tx.insert(orders).values(ordersToInsert).returning();
const allOrderItems: Omit<typeof orderItems.$inferInsert, "id">[] = [];
const allOrderStatuses: Omit<typeof orderStatus.$inferInsert, "id">[] = [];
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 deleteUserCartItemsForOrder(
await tx.insert(orderStatus).values(allOrderStatuses); userId,
if (paymentMethod === "online" && sharedPaymentInfoId) {
// const razorpayOrder = await RazorpayPaymentService.createOrder(
// sharedPaymentInfoId,
// totalWithDelivery.toString()
// );
// await RazorpayPaymentService.insertPaymentRecord(
// sharedPaymentInfoId,
// razorpayOrder,
// tx
// );
}
return insertedOrders;
});
await db.delete(cartItems).where(
and(
eq(cartItems.userId, userId),
inArray(
cartItems.productId,
selectedItems.map((item) => item.productId) selectedItems.map((item) => item.productId)
)
)
); );
if (appliedCoupon && createdOrders.length > 0) { if (appliedCoupon && createdOrders.length > 0) {
await db.insert(couponUsage).values({ await recordUserCouponUsage(
userId, userId,
couponId: appliedCoupon.id, appliedCoupon.id,
orderId: createdOrders[0].id as number, createdOrders[0].id
orderItemId: null, );
usedAt: new Date(),
});
} }
for (const order of createdOrders) { for (const order of createdOrders) {
@ -379,12 +260,8 @@ export const orderRouter = router({
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
// Check if user is suspended from placing orders const isSuspended = await checkUserSuspended(userId);
const userDetail = await db.query.userDetails.findFirst({ if (isSuspended) {
where: eq(userDetails.userId, userId),
});
if (userDetail?.isSuspended) {
throw new ApiError("Unable to place order", 403); throw new ApiError("Unable to place order", 403);
} }
@ -397,7 +274,6 @@ export const orderRouter = router({
isFlashDelivery, isFlashDelivery,
} = input; } = input;
// Check if flash delivery is enabled when placing a flash delivery order
if (isFlashDelivery) { if (isFlashDelivery) {
const isFlashDeliveryEnabled = await getConstant<boolean>(CONST_KEYS.isFlashDeliveryEnabled); const isFlashDeliveryEnabled = await getConstant<boolean>(CONST_KEYS.isFlashDeliveryEnabled);
if (!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) { if (!isFlashDelivery) {
const slotIds = [...new Set(selectedItems.filter(i => i.slotId !== null).map(i => i.slotId as number))]; const slotIds = [...new Set(selectedItems.filter(i => i.slotId !== null).map(i => i.slotId as number))];
for (const slotId of slotIds) { for (const slotId of slotIds) {
const slot = await getSlotById(slotId); const isCapacityFull = await getUserSlotCapacityStatus(slotId);
if (slot?.isCapacityFull) { if (isCapacityFull) {
throw new ApiError("Selected delivery slot is at full capacity. Please choose another slot.", 403); 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; let processedItems = selectedItems;
// Handle flash delivery slot resolution
if (isFlashDelivery) { if (isFlashDelivery) {
// For flash delivery, set slotId to null (no specific slot assigned)
processedItems = selectedItems.map(item => ({ processedItems = selectedItems.map(item => ({
...item, ...item,
slotId: null as any, // Type override for flash delivery slotId: null as any,
})); }));
} }
@ -447,35 +320,13 @@ export const orderRouter = router({
}) })
.optional() .optional()
) )
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }): Promise<UserOrdersResponse> => {
const { page = 1, pageSize = 10 } = input || {}; const { page = 1, pageSize = 10 } = input || {};
const userId = ctx.user.userId; const userId = ctx.user.userId;
const offset = (page - 1) * pageSize; const offset = (page - 1) * pageSize;
// Get total count for pagination const totalCount = await getUserOrderCount(userId);
const totalCountResult = await db.$count( const userOrders = await getUserOrdersWithRelations(userId, offset, pageSize);
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 mappedOrders = await Promise.all( const mappedOrders = await Promise.all(
userOrders.map(async (order) => { userOrders.map(async (order) => {
@ -515,7 +366,6 @@ export const orderRouter = router({
const items = await Promise.all( const items = await Promise.all(
order.orderItems.map(async (item) => { order.orderItems.map(async (item) => {
const signedImages = item.product.images const signedImages = item.product.images
? scaffoldAssetUrl( ? scaffoldAssetUrl(
item.product.images as string[] item.product.images as string[]
@ -571,44 +421,20 @@ export const orderRouter = router({
getOrderById: protectedProcedure getOrderById: protectedProcedure
.input(z.object({ orderId: z.string() })) .input(z.object({ orderId: z.string() }))
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }): Promise<UserOrderDetail> => {
const { orderId } = input; const { orderId } = input;
const userId = ctx.user.userId; const userId = ctx.user.userId;
const order = await db.query.orders.findFirst({ const order = await getUserOrderByIdWithRelations(parseInt(orderId), userId);
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,
},
});
if (!order) { if (!order) {
throw new Error("Order not found"); throw new Error("Order not found");
} }
// Get coupon usage for this specific order using new orderId field const couponUsageData = await getUserCouponUsageForOrder(order.id);
const couponUsageData = await db.query.couponUsage.findMany({
where: eq(couponUsage.orderId, order.id), // Use new orderId field
with: {
coupon: true,
},
});
let couponData = null; let couponData = null;
if (couponUsageData.length > 0) { if (couponUsageData.length > 0) {
// Calculate total discount from multiple coupons
let totalDiscountAmount = 0; let totalDiscountAmount = 0;
const orderTotal = parseFloat(order.totalAmount.toString()); const orderTotal = parseFloat(order.totalAmount.toString());
@ -624,7 +450,6 @@ export const orderRouter = router({
discountAmount = parseFloat(usage.coupon.flatDiscount.toString()); discountAmount = parseFloat(usage.coupon.flatDiscount.toString());
} }
// Apply max value limit if set
if ( if (
usage.coupon.maxValue && usage.coupon.maxValue &&
discountAmount > parseFloat(usage.coupon.maxValue.toString()) discountAmount > parseFloat(usage.coupon.maxValue.toString())
@ -651,7 +476,7 @@ export const orderRouter = router({
type OrderStatus = "cancelled" | "success"; type OrderStatus = "cancelled" | "success";
let deliveryStatus: DeliveryStatus; let deliveryStatus: DeliveryStatus;
let orderStatus: OrderStatus; let orderStatusResult: OrderStatus;
const allItemsPackaged = order.orderItems.every( const allItemsPackaged = order.orderItems.every(
(item) => item.is_packaged (item) => item.is_packaged
@ -659,16 +484,16 @@ export const orderRouter = router({
if (status?.isCancelled) { if (status?.isCancelled) {
deliveryStatus = "cancelled"; deliveryStatus = "cancelled";
orderStatus = "cancelled"; orderStatusResult = "cancelled";
} else if (status?.isDelivered) { } else if (status?.isDelivered) {
deliveryStatus = "success"; deliveryStatus = "success";
orderStatus = "success"; orderStatusResult = "success";
} else if (allItemsPackaged) { } else if (allItemsPackaged) {
deliveryStatus = "packaged"; deliveryStatus = "packaged";
orderStatus = "success"; orderStatusResult = "success";
} else { } else {
deliveryStatus = "pending"; deliveryStatus = "pending";
orderStatus = "success"; orderStatusResult = "success";
} }
const paymentMode = order.isCod ? "CoD" : "Online"; const paymentMode = order.isCod ? "CoD" : "Online";
@ -705,8 +530,8 @@ export const orderRouter = router({
orderDate: order.createdAt.toISOString(), orderDate: order.createdAt.toISOString(),
deliveryStatus, deliveryStatus,
deliveryDate: order.slot?.deliveryTime.toISOString(), deliveryDate: order.slot?.deliveryTime.toISOString(),
orderStatus: order.orderStatus, orderStatus: orderStatusResult,
cancellationStatus: orderStatus, cancellationStatus: orderStatusResult,
cancelReason: status?.cancelReason || null, cancelReason: status?.cancelReason || null,
paymentMode, paymentMode,
paymentStatus, paymentStatus,
@ -720,29 +545,24 @@ export const orderRouter = router({
orderAmount: parseFloat(order.totalAmount.toString()), orderAmount: parseFloat(order.totalAmount.toString()),
isFlashDelivery: order.isFlashDelivery, isFlashDelivery: order.isFlashDelivery,
createdAt: order.createdAt.toISOString(), createdAt: order.createdAt.toISOString(),
totalAmount: parseFloat(order.totalAmount.toString()),
deliveryCharge: parseFloat(order.deliveryCharge.toString()),
}; };
}), }),
cancelOrder: protectedProcedure cancelOrder: protectedProcedure
.input( .input(
z.object({ z.object({
// id: z.string().regex(/^ORD\d+$/, "Invalid order ID format"),
id: z.number(), id: z.number(),
reason: z.string().min(1, "Cancellation reason is required"), reason: z.string().min(1, "Cancellation reason is required"),
}) })
) )
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<UserCancelOrderResponse> => {
try { try {
const userId = ctx.user.userId; const userId = ctx.user.userId;
const { id, reason } = input; const { id, reason } = input;
// Check if order exists and belongs to user const order = await getUserOrderBasic(id);
const order = await db.query.orders.findFirst({
where: eq(orders.id, Number(id)),
with: {
orderStatus: true,
},
});
if (!order) { if (!order) {
console.error("Order not found:", id); console.error("Order not found:", id);
@ -775,39 +595,11 @@ export const orderRouter = router({
throw new ApiError("Cannot cancel delivered order", 400); throw new ApiError("Cannot cancel delivered order", 400);
} }
// Perform database operations in transaction await cancelUserOrderTransaction(id, status.id, reason, order.isCod);
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));
// Determine refund status based on payment method await sendOrderCancelledNotification(userId, id.toString());
const refundStatus = order.isCod ? "na" : "pending";
// Insert refund record await publishCancellation(id, 'user', reason);
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);
return { success: true, message: "Order cancelled successfully" }; return { success: true, message: "Order cancelled successfully" };
} catch (e) { } catch (e) {
@ -823,25 +615,11 @@ export const orderRouter = router({
userNotes: z.string(), userNotes: z.string(),
}) })
) )
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<UserUpdateNotesResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
const { id, userNotes } = input; const { id, userNotes } = input;
// Extract readable ID from orderId (e.g., ORD001 -> 1) const order = await getUserOrderBasic(id);
// 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,
},
});
if (!order) { if (!order) {
console.error("Order not found:", id); console.error("Order not found:", id);
@ -863,7 +641,6 @@ export const orderRouter = router({
throw new ApiError("Order status not found", 400); throw new ApiError("Order status not found", 400);
} }
// Only allow updating notes for orders that are not delivered or cancelled
if (status.isDelivered) { if (status.isDelivered) {
console.error("Cannot update notes for delivered order:", id); console.error("Cannot update notes for delivered order:", id);
throw new ApiError("Cannot update notes for delivered order", 400); 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); throw new ApiError("Cannot update notes for cancelled order", 400);
} }
// Update user notes await updateUserOrderNotes(id, userNotes);
await db
.update(orders)
.set({
userNotes: userNotes || null,
})
.where(eq(orders.id, order.id));
return { success: true, message: "Notes updated successfully" }; return { success: true, message: "Notes updated successfully" };
}), }),
@ -893,72 +664,27 @@ export const orderRouter = router({
}) })
.optional() .optional()
) )
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }): Promise<UserRecentProductsResponse> => {
const { limit = 20 } = input || {}; const { limit = 20 } = input || {};
const userId = ctx.user.userId; const userId = ctx.user.userId;
// Get user's recent delivered orders (last 30 days)
const thirtyDaysAgo = new Date(); const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const recentOrders = await db const recentOrderIds = await getUserRecentlyDeliveredOrderIds(userId, 10, thirtyDaysAgo);
.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
if (recentOrders.length === 0) { if (recentOrderIds.length === 0) {
return { success: true, products: [] }; return { success: true, products: [] };
} }
const orderIds = recentOrders.map((order) => order.id); const productIds = await getUserProductIdsFromOrders(recentOrderIds);
// 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)),
];
if (productIds.length === 0) { if (productIds.length === 0) {
return { success: true, products: [] }; return { success: true, products: [] };
} }
// Get product details const productsWithUnits = await getUserProductsForRecentOrders(productIds, limit);
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);
// Generate signed URLs for product images
const formattedProducts = await Promise.all( const formattedProducts = await Promise.all(
productsWithUnits.map(async (product) => { productsWithUnits.map(async (product) => {
const nextDeliveryDate = await getNextDeliveryDate(product.id); const nextDeliveryDate = await getNextDeliveryDate(product.id);

View file

@ -1,14 +1,23 @@
import { router, protectedProcedure } from '@/src/trpc/trpc-index'; import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'; import { z } from 'zod'
import { db } from '@/src/db/db_index'; import { ApiError } from '@/src/lib/api-error'
import { orders, payments, orderStatus } from '@/src/db/schema'; import crypto from 'crypto'
import { eq } from 'drizzle-orm'; import { razorpayId, razorpaySecret } from "@/src/lib/env-exporter"
import { ApiError } from '@/src/lib/api-error'; import { RazorpayPaymentService } from "@/src/lib/payments-utils"
import crypto from 'crypto'; import {
import { razorpayId, razorpaySecret } from "@/src/lib/env-exporter"; getUserPaymentOrderById as getUserPaymentOrderByIdInDb,
import { DiskPersistedSet } from "@/src/lib/disk-persisted-set"; getUserPaymentByOrderId as getUserPaymentByOrderIdInDb,
import { RazorpayPaymentService } from "@/src/lib/payments-utils"; 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({ .input(z.object({
orderId: z.string(), orderId: z.string(),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<UserPaymentOrderResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
const { orderId } = input; 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({ const order = await db.query.orders.findFirst({
where: eq(orders.id, parseInt(orderId)), where: eq(orders.id, parseInt(orderId)),
}); });
*/
if (!order) { if (!order) {
throw new ApiError("Order not found", 404); throw new ApiError("Order not found", 404)
} }
if (order.userId !== userId) { 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 // Check for existing pending payment
const existingPayment = await getUserPaymentByOrderIdInDb(parseInt(orderId))
/*
// Old implementation - direct DB queries:
const existingPayment = await db.query.payments.findFirst({ const existingPayment = await db.query.payments.findFirst({
where: eq(payments.orderId, parseInt(orderId)), where: eq(payments.orderId, parseInt(orderId)),
}); });
*/
if (existingPayment && existingPayment.status === 'pending') { if (existingPayment && existingPayment.status === 'pending') {
return { return {
@ -53,9 +71,8 @@ export const paymentRouter = router({
return { return {
razorpayOrderId: 0, razorpayOrderId: 0,
// razorpayOrderId: razorpayOrder.id,
key: razorpayId, key: razorpayId,
}; }
}), }),
@ -66,7 +83,7 @@ export const paymentRouter = router({
razorpay_order_id: z.string(), razorpay_order_id: z.string(),
razorpay_signature: z.string(), razorpay_signature: z.string(),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<UserPaymentVerifyResponse> => {
const { razorpay_payment_id, razorpay_order_id, razorpay_signature } = input; const { razorpay_payment_id, razorpay_order_id, razorpay_signature } = input;
// Verify signature // Verify signature
@ -80,9 +97,14 @@ export const paymentRouter = router({
} }
// Get current payment record // Get current payment record
const currentPayment = await getUserPaymentByMerchantOrderIdInDb(razorpay_order_id)
/*
// Old implementation - direct DB queries:
const currentPayment = await db.query.payments.findFirst({ const currentPayment = await db.query.payments.findFirst({
where: eq(payments.merchantOrderId, razorpay_order_id), where: eq(payments.merchantOrderId, razorpay_order_id),
}); });
*/
if (!currentPayment) { if (!currentPayment) {
throw new ApiError("Payment record not found", 404); throw new ApiError("Payment record not found", 404);
@ -95,6 +117,10 @@ export const paymentRouter = router({
signature: razorpay_signature, signature: razorpay_signature,
}; };
const updatedPayment = await updateUserPaymentSuccessInDb(razorpay_order_id, updatedPayload)
/*
// Old implementation - direct DB queries:
const [updatedPayment] = await db const [updatedPayment] = await db
.update(payments) .update(payments)
.set({ .set({
@ -104,56 +130,77 @@ export const paymentRouter = router({
.where(eq(payments.merchantOrderId, razorpay_order_id)) .where(eq(payments.merchantOrderId, razorpay_order_id))
.returning(); .returning();
// Update order status to mark payment as processed
await db await db
.update(orderStatus) .update(orderStatus)
.set({ .set({
paymentStatus: 'success', paymentStatus: 'success',
}) })
.where(eq(orderStatus.orderId, updatedPayment.orderId)); .where(eq(orderStatus.orderId, updatedPayment.orderId));
*/
if (!updatedPayment) {
throw new ApiError("Payment record not found", 404)
}
await updateUserOrderPaymentStatusInDb(updatedPayment.orderId, 'success')
return { return {
success: true, success: true,
message: "Payment verified successfully", message: "Payment verified successfully",
}; }
}), }),
markPaymentFailed: protectedProcedure markPaymentFailed: protectedProcedure
.input(z.object({ .input(z.object({
merchantOrderId: z.string(), merchantOrderId: z.string(),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<UserPaymentFailResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
const { merchantOrderId } = input; const { merchantOrderId } = input;
// Find payment by merchantOrderId // Find payment by merchantOrderId
const payment = await getUserPaymentByMerchantOrderIdInDb(merchantOrderId)
/*
// Old implementation - direct DB queries:
const payment = await db.query.payments.findFirst({ const payment = await db.query.payments.findFirst({
where: eq(payments.merchantOrderId, merchantOrderId), where: eq(payments.merchantOrderId, merchantOrderId),
}); });
*/
if (!payment) { if (!payment) {
throw new ApiError("Payment not found", 404); throw new ApiError("Payment not found", 404);
} }
// Check if payment belongs to user's order // 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({ const order = await db.query.orders.findFirst({
where: eq(orders.id, payment.orderId), where: eq(orders.id, payment.orderId),
}); });
*/
if (!order || order.userId !== userId) { if (!order || order.userId !== userId) {
throw new ApiError("Payment does not belong to user", 403); throw new ApiError("Payment does not belong to user", 403);
} }
// Update payment status to failed // Update payment status to failed
await markUserPaymentFailedInDb(payment.id)
/*
// Old implementation - direct DB queries:
await db await db
.update(payments) .update(payments)
.set({ status: 'failed' }) .set({ status: 'failed' })
.where(eq(payments.id, payment.id)); .where(eq(payments.id, payment.id));
*/
return { return {
success: true, success: true,
message: "Payment marked as failed", message: "Payment marked as failed",
}; }
}), }),
}); });

View file

@ -1,39 +1,34 @@
import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index'; import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'; import { z } from 'zod'
import { db } from '@/src/db/db_index'; import { claimUploadUrl, extractKeyFromPresignedUrl, scaffoldAssetUrl } from '@/src/lib/s3-client'
import { productInfo, units, productSlots, deliverySlotInfo, specialDeals, storeInfo, productTagInfo, productTags, productReviews, users } from '@/src/db/schema'; import { ApiError } from '@/src/lib/api-error'
import { claimUploadUrl, extractKeyFromPresignedUrl, scaffoldAssetUrl } from '@/src/lib/s3-client'; import { getProductById as getProductByIdFromCache, getAllProducts as getAllProductsFromCache } from '@/src/stores/product-store'
import { ApiError } from '@/src/lib/api-error'; import dayjs from 'dayjs'
import { eq, and, gt, sql, inArray, desc } from 'drizzle-orm'; import {
import { getProductById as getProductByIdFromCache, getAllProducts as getAllProductsFromCache } from '@/src/stores/product-store'; getUserProductDetailById as getUserProductDetailByIdInDb,
import dayjs from 'dayjs'; 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 const signProductImages = (product: UserProductDetailData): UserProductDetail => ({
interface Product { ...product,
id: number; images: scaffoldAssetUrl(product.images || []),
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 }>;
}
export const productRouter = router({ export const productRouter = router({
getProductDetails: publicProcedure getProductDetails: publicProcedure
.input(z.object({ .input(z.object({
id: z.string().regex(/^\d+$/, 'Invalid product ID'), id: z.string().regex(/^\d+$/, 'Invalid product ID'),
})) }))
.query(async ({ input }): Promise<Product> => { .query(async ({ input }): Promise<UserProductDetail> => {
const { id } = input; const { id } = input;
const productId = parseInt(id); const productId = parseInt(id);
@ -60,6 +55,10 @@ export const productRouter = router({
} }
// If not in cache, fetch from database (fallback) // If not in cache, fetch from database (fallback)
const productData = await getUserProductDetailByIdInDb(productId)
/*
// Old implementation - direct DB queries:
const productData = await db const productData = await db
.select({ .select({
id: productInfo.id, id: productInfo.id,
@ -81,82 +80,13 @@ export const productRouter = router({
.innerJoin(units, eq(productInfo.unitId, units.id)) .innerJoin(units, eq(productInfo.unitId, units.id))
.where(eq(productInfo.id, productId)) .where(eq(productInfo.id, productId))
.limit(1); .limit(1);
*/
if (productData.length === 0) { if (!productData) {
throw new Error('Product not found'); throw new Error('Product not found')
} }
const product = productData[0]; return signProductImages(productData)
// 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;
}), }),
getProductReviews: publicProcedure getProductReviews: publicProcedure
@ -165,9 +95,13 @@ export const productRouter = router({
limit: z.number().int().min(1).max(50).optional().default(10), limit: z.number().int().min(1).max(50).optional().default(10),
offset: z.number().int().min(0).optional().default(0), offset: z.number().int().min(0).optional().default(0),
})) }))
.query(async ({ input }) => { .query(async ({ input }): Promise<UserProductReviewsResponse> => {
const { productId, limit, offset } = input; const { productId, limit, offset } = input;
const { reviews, totalCount } = await getUserProductReviewsInDb(productId, limit, offset)
/*
// Old implementation - direct DB queries:
const reviews = await db const reviews = await db
.select({ .select({
id: productReviews.id, id: productReviews.id,
@ -184,15 +118,6 @@ export const productRouter = router({
.limit(limit) .limit(limit)
.offset(offset); .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 const totalCountResult = await db
.select({ count: sql`count(*)` }) .select({ count: sql`count(*)` })
.from(productReviews) .from(productReviews)
@ -200,8 +125,16 @@ export const productRouter = router({
const totalCount = Number(totalCountResult[0].count); const totalCount = Number(totalCountResult[0].count);
const hasMore = offset + limit < totalCount; 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 createReview: protectedProcedure
@ -212,11 +145,20 @@ export const productRouter = router({
imageUrls: z.array(z.string()).optional().default([]), imageUrls: z.array(z.string()).optional().default([]),
uploadUrls: z.array(z.string()).optional().default([]), uploadUrls: z.array(z.string()).optional().default([]),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<UserCreateReviewResponse> => {
const { productId, reviewBody, ratings, imageUrls, uploadUrls } = input; const { productId, reviewBody, ratings, imageUrls, uploadUrls } = input;
const userId = ctx.user.userId; 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({ const product = await db.query.productInfo.findFirst({
where: eq(productInfo.id, productId), where: eq(productInfo.id, productId),
}); });
@ -224,7 +166,6 @@ export const productRouter = router({
throw new ApiError('Product not found', 404); throw new ApiError('Product not found', 404);
} }
// Insert review
const [newReview] = await db.insert(productReviews).values({ const [newReview] = await db.insert(productReviews).values({
userId, userId,
productId, productId,
@ -232,6 +173,7 @@ export const productRouter = router({
ratings, ratings,
imageUrls: uploadUrls.map(item => extractKeyFromPresignedUrl(item)), imageUrls: uploadUrls.map(item => extractKeyFromPresignedUrl(item)),
}).returning(); }).returning();
*/
// Claim upload URLs // Claim upload URLs
if (uploadUrls && uploadUrls.length > 0) { 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 getAllProductsSummary: publicProcedure
.query(async (): Promise<Product[]> => { .query(async (): Promise<UserProductDetail[]> => {
// Get all products from cache // Get all products from cache
const allCachedProducts = await getAllProductsFromCache(); const allCachedProducts = await getAllProductsFromCache();
// Transform the cached products to match the expected summary format // Transform the cached products to match the expected summary format
// (with empty deliverySlots and specialDeals arrays for summary view) // (with empty deliverySlots and specialDeals arrays for summary view)
const transformedProducts = allCachedProducts.map(product => ({ const transformedProducts: UserProductDetail[] = allCachedProducts.map(product => ({
...product, ...product,
deliverySlots: [], // Empty for summary view images: product.images || [],
specialDeals: [], // Empty for summary view deliverySlots: [],
})); specialDeals: [],
}))
return transformedProducts; return transformedProducts
}), }),
}); });

View file

@ -1,15 +1,9 @@
import { router, publicProcedure } from "@/src/trpc/trpc-index"; import { router, publicProcedure } from "@/src/trpc/trpc-index"
import { z } from "zod"; import { z } from "zod"
import { db } from "@/src/db/db_index"; import { getAllSlots as getAllSlotsFromCache, getSlotById as getSlotByIdFromCache } from "@/src/stores/slot-store"
import { import dayjs from 'dayjs'
deliverySlotInfo, import { getUserActiveSlotsList as getUserActiveSlotsListInDb, getUserProductAvailability as getUserProductAvailabilityInDb } from '@/src/dbService'
productSlots, import type { UserSlotData, UserSlotsListResponse, UserSlotsWithProductsResponse } from '@packages/shared'
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';
// Helper method to get formatted slot data by ID // Helper method to get formatted slot data by ID
async function getSlotData(slotId: number) { async function getSlotData(slotId: number) {
@ -32,7 +26,7 @@ async function getSlotData(slotId: number) {
}; };
} }
export async function scaffoldSlotsWithProducts() { export async function scaffoldSlotsWithProducts(): Promise<UserSlotsWithProductsResponse> {
const allSlots = await getAllSlotsFromCache(); const allSlots = await getAllSlotsFromCache();
const currentTime = new Date(); const currentTime = new Date();
const validSlots = allSlots const validSlots = allSlots
@ -43,7 +37,10 @@ export async function scaffoldSlotsWithProducts() {
}) })
.sort((a, b) => dayjs(a.deliveryTime).valueOf() - dayjs(b.deliveryTime).valueOf()); .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 const allProducts = await db
.select({ .select({
id: productInfo.id, id: productInfo.id,
@ -60,6 +57,7 @@ export async function scaffoldSlotsWithProducts() {
isOutOfStock: product.isOutOfStock, isOutOfStock: product.isOutOfStock,
isFlashAvailable: product.isFlashAvailable, isFlashAvailable: product.isFlashAvailable,
})); }));
*/
return { return {
slots: validSlots, slots: validSlots,
@ -69,24 +67,30 @@ export async function scaffoldSlotsWithProducts() {
} }
export const slotsRouter = router({ export const slotsRouter = router({
getSlots: publicProcedure.query(async () => { getSlots: publicProcedure.query(async (): Promise<UserSlotsListResponse> => {
const slots = await getUserActiveSlotsListInDb()
/*
// Old implementation - direct DB query:
const slots = await db.query.deliverySlotInfo.findMany({ const slots = await db.query.deliverySlotInfo.findMany({
where: eq(deliverySlotInfo.isActive, true), where: eq(deliverySlotInfo.isActive, true),
}); });
*/
return { return {
slots, slots,
count: slots.length, count: slots.length,
}; }
}), }),
getSlotsWithProducts: publicProcedure.query(async () => { getSlotsWithProducts: publicProcedure.query(async (): Promise<UserSlotsWithProductsResponse> => {
const response = await scaffoldSlotsWithProducts(); const response = await scaffoldSlotsWithProducts();
return response; return response;
}), }),
getSlotById: publicProcedure getSlotById: publicProcedure
.input(z.object({ slotId: z.number() })) .input(z.object({ slotId: z.number() }))
.query(async ({ input }) => { .query(async ({ input }): Promise<UserSlotData | null> => {
return await getSlotData(input.slotId); return await getSlotData(input.slotId);
}), }),
}); });

View file

@ -1,13 +1,23 @@
import { router, publicProcedure } from '@/src/trpc/trpc-index'; import { router, publicProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'; import { z } from 'zod'
import { db } from '@/src/db/db_index'; import { scaffoldAssetUrl } from '@/src/lib/s3-client'
import { storeInfo, productInfo, units } from '@/src/db/schema'; import { ApiError } from '@/src/lib/api-error'
import { eq, and, sql } from 'drizzle-orm'; import { getTagsByStoreId } from '@/src/stores/product-tag-store'
import { scaffoldAssetUrl } from '@/src/lib/s3-client'; import {
import { ApiError } from '@/src/lib/api-error'; getUserStoreSummaries as getUserStoreSummariesInDb,
import { getTagsByStoreId } from '@/src/stores/product-tag-store'; getUserStoreDetail as getUserStoreDetailInDb,
} from '@/src/dbService'
import type {
UserStoresResponse,
UserStoreDetail,
UserStoreSummary,
} from '@packages/shared'
export async function scaffoldStores() { export async function scaffoldStores(): Promise<UserStoresResponse> {
const storesData = await getUserStoreSummariesInDb()
/*
// Old implementation - direct DB queries:
const storesData = await db const storesData = await db
.select({ .select({
id: storeInfo.id, id: storeInfo.id,
@ -22,34 +32,17 @@ export async function scaffoldStores() {
and(eq(productInfo.storeId, storeInfo.id), eq(productInfo.isSuspended, false)) and(eq(productInfo.storeId, storeInfo.id), eq(productInfo.isSuspended, false))
) )
.groupBy(storeInfo.id); .groupBy(storeInfo.id);
*/
// Generate signed URLs for store images and fetch sample products const storesWithDetails: UserStoreSummary[] = storesData.map((store) => {
const storesWithDetails = await Promise.all( const signedImageUrl = store.imageUrl ? scaffoldAssetUrl(store.imageUrl) : null
storesData.map(async (store) => { const sampleProducts = store.sampleProducts.map((product) => ({
const signedImageUrl = store.imageUrl ? scaffoldAssetUrl(store.imageUrl) : 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, id: product.id,
name: product.name, name: product.name,
signedImageUrl: (images && images.length > 0) ? scaffoldAssetUrl(images[0]) : null, signedImageUrl: product.images && product.images.length > 0
}; ? scaffoldAssetUrl(product.images[0])
}) : null,
); }))
return { return {
id: store.id, id: store.id,
@ -57,18 +50,20 @@ export async function scaffoldStores() {
description: store.description, description: store.description,
signedImageUrl, signedImageUrl,
productCount: store.productCount, productCount: store.productCount,
sampleProducts: productsWithSignedUrls, sampleProducts,
}; }
}) })
);
return { return {
stores: storesWithDetails, stores: storesWithDetails,
}; }
} }
export async function scaffoldStoreWithProducts(storeId: number) { export async function scaffoldStoreWithProducts(storeId: number): Promise<UserStoreDetail> {
// Fetch store info const storeDetail = await getUserStoreDetailInDb(storeId)
/*
// Old implementation - direct DB queries:
const storeData = await db.query.storeInfo.findFirst({ const storeData = await db.query.storeInfo.findFirst({
where: eq(storeInfo.id, storeId), where: eq(storeInfo.id, storeId),
columns: { columns: {
@ -83,10 +78,8 @@ export async function scaffoldStoreWithProducts(storeId: number) {
throw new ApiError('Store not found', 404); throw new ApiError('Store not found', 404);
} }
// Generate signed URL for store image
const signedImageUrl = storeData.imageUrl ? scaffoldAssetUrl(storeData.imageUrl) : null; const signedImageUrl = storeData.imageUrl ? scaffoldAssetUrl(storeData.imageUrl) : null;
// Fetch products for this store
const productsData = await db const productsData = await db
.select({ .select({
id: productInfo.id, id: productInfo.id,
@ -105,8 +98,6 @@ export async function scaffoldStoreWithProducts(storeId: number) {
.innerJoin(units, eq(productInfo.unitId, units.id)) .innerJoin(units, eq(productInfo.unitId, units.id))
.where(and(eq(productInfo.storeId, storeId), eq(productInfo.isSuspended, false))); .where(and(eq(productInfo.storeId, storeId), eq(productInfo.isSuspended, false)));
// Generate signed URLs for product images
const productsWithSignedUrls = await Promise.all( const productsWithSignedUrls = await Promise.all(
productsData.map(async (product) => ({ productsData.map(async (product) => ({
id: product.id, id: product.id,
@ -141,11 +132,53 @@ export async function scaffoldStoreWithProducts(storeId: number) {
productIds: tag.productIds, 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({ export const storesRouter = router({
getStores: publicProcedure getStores: publicProcedure
.query(async () => { .query(async (): Promise<UserStoresResponse> => {
const response = await scaffoldStores(); const response = await scaffoldStores();
return response; return response;
}), }),
@ -154,7 +187,7 @@ export const storesRouter = router({
.input(z.object({ .input(z.object({
storeId: z.number(), storeId: z.number(),
})) }))
.query(async ({ input }) => { .query(async ({ input }): Promise<UserStoreDetail> => {
const { storeId } = input; const { storeId } = input;
const response = await scaffoldStoreWithProducts(storeId); const response = await scaffoldStoreWithProducts(storeId);
return response; return response;

View file

@ -1,27 +1,23 @@
import { router, protectedProcedure, publicProcedure } from '@/src/trpc/trpc-index'; import { router, protectedProcedure, publicProcedure } from '@/src/trpc/trpc-index'
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken'
import { eq, and } from 'drizzle-orm'; import { z } from 'zod'
import { z } from 'zod'; import { ApiError } from '@/src/lib/api-error'
import { db } from '@/src/db/db_index'; import { jwtSecret } from '@/src/lib/env-exporter'
import { users, userDetails, userCreds, notifCreds, unloggedUserTokens } from '@/src/db/schema'; import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client'
import { ApiError } from '@/src/lib/api-error'; import {
import { jwtSecret } from '@/src/lib/env-exporter'; getUserProfileById as getUserProfileByIdInDb,
import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client'; getUserProfileDetailById as getUserProfileDetailByIdInDb,
getUserWithCreds as getUserWithCredsInDb,
interface AuthResponse { upsertUserNotifCred as upsertUserNotifCredInDb,
token: string; deleteUserUnloggedToken as deleteUserUnloggedTokenInDb,
user: { getUserUnloggedToken as getUserUnloggedTokenInDb,
id: number; upsertUserUnloggedToken as upsertUserUnloggedTokenInDb,
name: string | null; } from '@/src/dbService'
email: string | null; import type {
mobile: string | null; UserSelfDataResponse,
profileImage?: string | null; UserProfileCompleteResponse,
bio?: string | null; UserSavePushTokenResponse,
dateOfBirth?: string | null; } from '@packages/shared'
gender?: string | null;
occupation?: string | null;
};
}
const generateToken = (userId: number): string => { const generateToken = (userId: number): string => {
const secret = jwtSecret; const secret = jwtSecret;
@ -34,36 +30,30 @@ const generateToken = (userId: number): string => {
export const userRouter = router({ export const userRouter = router({
getSelfData: protectedProcedure getSelfData: protectedProcedure
.query(async ({ ctx }) => { .query(async ({ ctx }): Promise<UserSelfDataResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
if (!userId) { if (!userId) {
throw new ApiError('User not authenticated', 401); throw new ApiError('User not authenticated', 401);
} }
const [user] = await db const user = await getUserProfileByIdInDb(userId)
.select()
.from(users)
.where(eq(users.id, userId))
.limit(1);
if (!user) { if (!user) {
throw new ApiError('User not found', 404); throw new ApiError('User not found', 404);
} }
// Get user details for profile image // Get user details for profile image
const [userDetail] = await db const userDetail = await getUserProfileDetailByIdInDb(userId)
.select()
.from(userDetails)
.where(eq(userDetails.userId, userId))
.limit(1);
// Generate signed URL for profile image if it exists // Generate signed URL for profile image if it exists
const profileImageSignedUrl = userDetail?.profileImage const profileImageSignedUrl = userDetail?.profileImage
? await generateSignedUrlFromS3Url(userDetail.profileImage) ? await generateSignedUrlFromS3Url(userDetail.profileImage)
: null; : null;
const response: Omit<AuthResponse, 'token'> = { return {
success: true,
data: {
user: { user: {
id: user.id, id: user.id,
name: user.name, name: user.name,
@ -75,96 +65,52 @@ export const userRouter = router({
gender: userDetail?.gender || null, gender: userDetail?.gender || null,
occupation: userDetail?.occupation || null, occupation: userDetail?.occupation || null,
}, },
}; },
}
return {
success: true,
data: response,
};
}), }),
checkProfileComplete: protectedProcedure checkProfileComplete: protectedProcedure
.query(async ({ ctx }) => { .query(async ({ ctx }): Promise<UserProfileCompleteResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
if (!userId) { if (!userId) {
throw new ApiError('User not authenticated', 401); throw new ApiError('User not authenticated', 401);
} }
const result = await db const result = await getUserWithCredsInDb(userId)
.select()
.from(users)
.leftJoin(userCreds, eq(users.id, userCreds.userId))
.where(eq(users.id, userId))
.limit(1);
if (result.length === 0) { if (!result) {
throw new ApiError('User not found', 404); throw new ApiError('User not found', 404)
} }
const { users: user, user_creds: creds } = result[0];
return { return {
isComplete: !!(user.name && user.email && creds), isComplete: !!(result.user.name && result.user.email && result.creds),
}; };
}), }),
savePushToken: publicProcedure savePushToken: publicProcedure
.input(z.object({ token: z.string() })) .input(z.object({ token: z.string() }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<UserSavePushTokenResponse> => {
const { token } = input; const { token } = input;
const userId = ctx.user?.userId; const userId = ctx.user?.userId;
if (userId) { if (userId) {
// AUTHENTICATED USER // AUTHENTICATED USER
// Check if token exists in notif_creds for this user // Check if token exists in notif_creds for this user
const existing = await db.query.notifCreds.findFirst({ await upsertUserNotifCredInDb(userId, token)
where: and( await deleteUserUnloggedTokenInDb(token)
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));
} else { } else {
// UNAUTHENTICATED USER // UNAUTHENTICATED USER
// Save/update in unlogged_user_tokens // Save/update in unlogged_user_tokens
const existing = await db.query.unloggedUserTokens.findFirst({ const existing = await getUserUnloggedTokenInDb(token)
where: eq(unloggedUserTokens.token, token),
});
if (existing) { if (existing) {
await db await upsertUserUnloggedTokenInDb(token)
.update(unloggedUserTokens)
.set({ lastVerified: new Date() })
.where(eq(unloggedUserTokens.id, existing.id));
} else { } else {
await db.insert(unloggedUserTokens).values({ await upsertUserUnloggedTokenInDb(token)
token,
lastVerified: new Date(),
});
} }
} }
return { success: true }; return { success: true }
}), }),
}); });

View file

@ -165,7 +165,123 @@ export {
getVendorOrders, getVendorOrders,
} from './src/admin-apis/vendor-snippets'; } from './src/admin-apis/vendor-snippets';
// Note: User API helpers are available in their respective files export {
// but not exported from main index to avoid naming conflicts // User Address
// Import them directly from the file paths if needed: getDefaultAddress as getUserDefaultAddress,
// import { getAllProducts } from '@packages/db_helper_postgres/src/user-apis/product' 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';

View file

@ -1,23 +1,148 @@
import { db } from '../db/db_index'; import { db } from '../db/db_index'
import { addresses, addressAreas, addressZones } from '../db/schema'; import { addresses, deliverySlotInfo, orders, orderStatus } from '../db/schema'
import { eq, desc } from 'drizzle-orm'; import { and, eq, gte } from 'drizzle-orm'
import type { InferSelectModel } from 'drizzle-orm'
import type { UserAddress } from '@packages/shared'
export async function getZones(): Promise<any[]> { type AddressRow = InferSelectModel<typeof addresses>
const zones = await db.select().from(addressZones).orderBy(desc(addressZones.addedAt));
return zones; 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<UserAddress | null> {
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<any[]> { export async function getUserAddresses(userId: number): Promise<UserAddress[]> {
const areas = await db.select().from(addressAreas).orderBy(desc(addressAreas.createdAt)); const userAddresses = await db.select().from(addresses).where(eq(addresses.userId, userId))
return areas; return userAddresses.map(mapUserAddress)
} }
export async function createZone(zoneName: string): Promise<any> { export async function getUserAddressById(userId: number, addressId: number): Promise<UserAddress | null> {
const [zone] = await db.insert(addressZones).values({ zoneName }).returning(); const [address] = await db
return zone; .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<any> { export async function clearDefaultAddress(userId: number): Promise<void> {
const [area] = await db.insert(addressAreas).values({ placeName, zoneId }).returning(); await db.update(addresses).set({ isDefault: false }).where(eq(addresses.userId, userId))
return area; }
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<UserAddress> {
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<UserAddress | null> {
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<boolean> {
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<boolean> {
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
} }

View file

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

View file

@ -1,11 +1,29 @@
import { db } from '../db/db_index'; import { db } from '../db/db_index'
import { homeBanners } from '../db/schema'; import { homeBanners } from '../db/schema'
import { eq, and, desc, sql } from 'drizzle-orm'; import { asc, isNotNull } from 'drizzle-orm'
import type { InferSelectModel } from 'drizzle-orm'
import type { UserBanner } from '@packages/shared'
export async function getActiveBanners(): Promise<any[]> { type BannerRow = InferSelectModel<typeof homeBanners>
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<UserBanner[]> {
const banners = await db.query.homeBanners.findMany({ const banners = await db.query.homeBanners.findMany({
where: eq(homeBanners.isActive, true), where: isNotNull(homeBanners.serialNum),
orderBy: desc(homeBanners.createdAt), orderBy: asc(homeBanners.serialNum),
}); })
return banners;
return banners.map(mapBanner)
} }

View file

@ -1,41 +1,95 @@
import { db } from '../db/db_index'; import { db } from '../db/db_index'
import { cartItems, productInfo } from '../db/schema'; import { cartItems, productInfo, units } from '../db/schema'
import { eq, and } from 'drizzle-orm'; import { and, eq, sql } from 'drizzle-orm'
import type { UserCartItem } from '@packages/shared'
export async function getCartItems(userId: number): Promise<any[]> { const getStringArray = (value: unknown): string[] => {
return await db.query.cartItems.findMany({ if (!Array.isArray(value)) return []
where: eq(cartItems.userId, userId), return value.map((item) => String(item))
with: {
product: {
with: {
unit: true,
},
},
},
});
} }
export async function addToCart(userId: number, productId: number, quantity: number): Promise<any> { export async function getCartItemsWithProducts(userId: number): Promise<UserCartItem[]> {
const [item] = await db.insert(cartItems).values({ 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<void> {
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<void> {
await db.insert(cartItems).values({
userId, userId,
productId, productId,
quantity, quantity: quantity.toString(),
}).returning(); })
return item;
} }
export async function updateCartItem(itemId: number, quantity: number): Promise<any> { export async function updateCartItemQuantity(userId: number, itemId: number, quantity: number) {
const [item] = await db.update(cartItems) const [updatedItem] = await db.update(cartItems)
.set({ quantity }) .set({ quantity: quantity.toString() })
.where(eq(cartItems.id, itemId)) .where(and(eq(cartItems.id, itemId), eq(cartItems.userId, userId)))
.returning(); .returning({ id: cartItems.id })
return item;
return !!updatedItem
} }
export async function removeFromCart(itemId: number): Promise<void> { export async function deleteCartItem(userId: number, itemId: number): Promise<boolean> {
await db.delete(cartItems).where(eq(cartItems.id, itemId)); 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<void> { export async function clearUserCart(userId: number): Promise<void> {
await db.delete(cartItems).where(eq(cartItems.userId, userId)); await db.delete(cartItems).where(eq(cartItems.userId, userId))
} }

View file

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

View file

@ -1,43 +1,146 @@
import { db } from '../db/db_index'; import { db } from '../db/db_index'
import { coupons, couponUsage } from '../db/schema'; import {
import { eq, and } from 'drizzle-orm'; 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<any> { type CouponRow = InferSelectModel<typeof coupons>
const coupon = await db.query.coupons.findFirst({ type CouponUsageRow = InferSelectModel<typeof couponUsage>
where: eq(coupons.couponCode, code.toUpperCase()), type CouponApplicableUserRow = InferSelectModel<typeof couponApplicableUsers>
}); type CouponApplicableProductRow = InferSelectModel<typeof couponApplicableProducts>
type ReservedCouponRow = InferSelectModel<typeof reservedCoupons>
if (!coupon || coupon.isInvalidated) { const mapCoupon = (coupon: CouponRow): UserCoupon => ({
return { valid: false, message: 'Invalid coupon' }; 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()) { const mapUsage = (usage: CouponUsageRow): UserCouponUsage => ({
return { valid: false, message: 'Coupon expired' }; id: usage.id,
} userId: usage.userId,
couponId: usage.couponId,
orderId: usage.orderId ?? null,
orderItemId: usage.orderItemId ?? null,
usedAt: usage.usedAt,
})
let discountAmount = 0; const mapApplicableUser = (applicable: CouponApplicableUserRow): UserCouponApplicableUser => ({
if (coupon.discountPercent) { id: applicable.id,
discountAmount = (orderAmount * parseFloat(coupon.discountPercent)) / 100; couponId: applicable.couponId,
} else if (coupon.flatDiscount) { userId: applicable.userId,
discountAmount = parseFloat(coupon.flatDiscount); })
}
if (coupon.maxValue) { const mapApplicableProduct = (applicable: CouponApplicableProductRow): UserCouponApplicableProduct => ({
const maxDiscount = parseFloat(coupon.maxValue); id: applicable.id,
if (discountAmount > maxDiscount) { couponId: applicable.couponId,
discountAmount = maxDiscount; productId: applicable.productId,
} })
}
return { const mapCouponWithRelations = (coupon: CouponRow & {
valid: true, usages: CouponUsageRow[]
discountAmount, 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<UserCouponWithRelations[]> {
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 getAllCouponsWithRelations(userId: number): Promise<UserCouponWithRelations[]> {
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<ReservedCouponRow | null> {
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<UserCoupon> {
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, couponId: coupon.id,
}; userId,
} })
export async function getUserCoupons(userId: number): Promise<any[]> { await tx.update(reservedCoupons).set({
return await db.query.coupons.findMany({ isRedeemed: true,
where: eq(coupons.userId, userId), redeemedBy: userId,
}); redeemedAt: new Date(),
}).where(eq(reservedCoupons.id, reservedCoupon.id))
return coupon
})
return mapCoupon(couponResult)
} }

View file

@ -1,59 +1,624 @@
import { db } from '../db/db_index'; import { db } from '../db/db_index'
import { orders, orderItems, orderStatus } from '../db/schema'; import {
import { eq, desc } from 'drizzle-orm'; 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<any[]> { export interface OrderItemInput {
return await db.query.orders.findMany({ 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<CouponValidationResult | null> {
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<boolean> {
const userDetail = await db.query.userDetails.findFirst({
where: eq(userDetails.userId, userId),
})
return userDetail?.isSuspended ?? false
}
export async function getSlotCapacityStatus(slotId: number): Promise<boolean> {
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<typeof orders.$inferInsert, 'id'>
orderItems: Omit<typeof orderItems.$inferInsert, 'id'>[]
orderStatus: Omit<typeof orderStatus.$inferInsert, 'id'>
}>
paymentMethod: 'online' | 'cod'
totalWithDelivery: number
}): Promise<PlacedOrder[]> {
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<typeof orders.$inferInsert, 'id'>[] =
ordersData.map((od) => ({
...od.order,
paymentInfoId: sharedPaymentInfoId,
}))
const insertedOrders = await tx.insert(orders).values(ordersToInsert).returning()
const allOrderItems: Omit<typeof orderItems.$inferInsert, 'id'>[] = []
const allOrderStatuses: Omit<typeof orderStatus.$inferInsert, 'id'>[] = []
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<void> {
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<void> {
await db.insert(couponUsage).values({
userId,
couponId,
orderId,
orderItemId: null,
usedAt: new Date(),
})
}
export async function getOrdersWithRelations(
userId: number,
offset: number,
pageSize: number
): Promise<OrderWithRelations[]> {
return db.query.orders.findMany({
where: eq(orders.userId, userId), where: eq(orders.userId, userId),
with: { with: {
orderItems: { orderItems: {
with: { with: {
product: { product: {
with: { columns: {
unit: true, id: true,
name: true,
images: true,
}, },
}, },
}, },
}, },
orderStatus: true, slot: {
slot: true, columns: {
deliveryTime: true,
}, },
orderBy: desc(orders.createdAt), },
}); 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: (orders, { desc }) => [desc(orders.createdAt)],
limit: pageSize,
offset: offset,
}) as Promise<OrderWithRelations[]>
} }
export async function getOrderById(orderId: number, userId: number): Promise<any | null> { export async function getOrderCount(userId: number): Promise<number> {
return await db.query.orders.findFirst({ const result = await db
where: eq(orders.id, orderId), .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<OrderDetailWithRelations | null> {
const order = await db.query.orders.findFirst({
where: and(eq(orders.id, orderId), eq(orders.userId, userId)),
with: { with: {
orderItems: { orderItems: {
with: { with: {
product: true, product: {
columns: {
id: true,
name: true,
images: true,
}, },
}, },
orderStatus: true,
address: 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,
},
with: {
refundCoupon: {
columns: {
id: true,
couponCode: true,
},
},
},
},
refunds: {
columns: {
refundStatus: true,
refundAmount: true,
},
},
},
})
return order as OrderDetailWithRelations | null
} }
export async function createOrder(orderData: any, orderItemsData: any[]): Promise<any> { export async function getCouponUsageForOrder(
return await db.transaction(async (tx) => { orderId: number
const [order] = await tx.insert(orders).values(orderData).returning(); ): Promise<CouponUsageWithCoupon[]> {
return db.query.couponUsage.findMany({
for (const item of orderItemsData) { where: eq(couponUsage.orderId, orderId),
await tx.insert(orderItems).values({ with: {
...item, coupon: {
orderId: order.id, columns: {
}); id: true,
} couponCode: true,
discountPercent: true,
await tx.insert(orderStatus).values({ flatDiscount: true,
orderId: order.id, maxValue: true,
paymentStatus: 'pending', },
}); },
},
return order; }) as Promise<CouponUsageWithCoupon[]>
}); }
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<void> {
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<void> {
await db
.update(orders)
.set({
userNotes: userNotes || null,
})
.where(eq(orders.id, orderId))
}
export async function getRecentlyDeliveredOrderIds(
userId: number,
limit: number,
since: Date
): Promise<number[]> {
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<number[]> {
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<RecentProductData[]> {
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)
} }

View file

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

View file

@ -1,41 +1,181 @@
import { db } from '../db/db_index'; import { db } from '../db/db_index'
import { productInfo, productReviews } from '../db/schema'; import { deliverySlotInfo, productInfo, productReviews, productSlots, specialDeals, storeInfo, units, users } from '../db/schema'
import { eq, desc } from 'drizzle-orm'; import { and, desc, eq, gt, sql } from 'drizzle-orm'
import type { UserProductDetailData, UserProductReview } from '@packages/shared'
export async function getAllProducts(): Promise<any[]> { const getStringArray = (value: unknown): string[] | null => {
return await db.query.productInfo.findMany({ if (!Array.isArray(value)) return null
with: { return value.map((item) => String(item))
unit: true,
store: true,
specialDeals: true,
},
orderBy: productInfo.name,
});
} }
export async function getProductById(id: number): Promise<any | null> { export async function getProductDetailById(productId: number): Promise<UserProductDetailData | null> {
return await db.query.productInfo.findFirst({ const productData = await db
where: eq(productInfo.id, id), .select({
with: { id: productInfo.id,
unit: true, name: productInfo.name,
store: true, shortDescription: productInfo.shortDescription,
specialDeals: true, longDescription: productInfo.longDescription,
productReviews: { price: productInfo.price,
with: { marketPrice: productInfo.marketPrice,
user: true, images: productInfo.images,
}, isOutOfStock: productInfo.isOutOfStock,
orderBy: desc(productReviews.createdAt), 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<any> { export async function getProductReviews(productId: number, limit: number, offset: number) {
const [review] = await db.insert(productReviews).values({ 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<UserProductReview> {
const [newReview] = await db.insert(productReviews).values({
userId, userId,
productId, productId,
rating, reviewBody,
comment, ratings,
}).returning(); imageUrls,
return review; }).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,
}
} }

View file

@ -1,17 +1,46 @@
import { db } from '../db/db_index'; import { db } from '../db/db_index'
import { deliverySlotInfo } from '../db/schema'; import { deliverySlotInfo, productInfo } from '../db/schema'
import { eq, gte, and } from 'drizzle-orm'; import { asc, eq } from 'drizzle-orm'
import type { InferSelectModel } from 'drizzle-orm'
import type { UserDeliverySlot, UserSlotAvailability } from '@packages/shared'
export async function getAvailableSlots(): Promise<any[]> { type SlotRow = InferSelectModel<typeof deliverySlotInfo>
const now = new Date();
return await db.query.deliverySlotInfo.findMany({ const mapSlot = (slot: SlotRow): UserDeliverySlot => ({
where: gte(deliverySlotInfo.freezeTime, now), id: slot.id,
orderBy: deliverySlotInfo.deliveryTime, 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<UserDeliverySlot[]> {
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<any | null> { export async function getProductAvailability(): Promise<UserSlotAvailability[]> {
return await db.query.deliverySlotInfo.findFirst({ const products = await db
where: eq(deliverySlotInfo.id, id), .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,
}))
} }

View file

@ -1,20 +1,127 @@
import { db } from '../db/db_index'; import { db } from '../db/db_index'
import { storeInfo } from '../db/schema'; import { productInfo, storeInfo, units } from '../db/schema'
import { eq } from 'drizzle-orm'; 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<any[]> { type StoreRow = InferSelectModel<typeof storeInfo>
return await db.query.storeInfo.findMany({ type StoreProductRow = {
with: { id: number
owner: true, 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<any | null> { const getStringArray = (value: unknown): string[] | null => {
return await db.query.storeInfo.findFirst({ if (!Array.isArray(value)) return null
where: eq(storeInfo.id, id), return value.map((item) => String(item))
with: { }
owner: true,
}, export async function getStoreSummaries(): Promise<UserStoreSummaryData[]> {
}); const storesData = await db
.select({
id: storeInfo.id,
name: storeInfo.name,
description: storeInfo.description,
imageUrl: storeInfo.imageUrl,
productCount: sql<number>`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<UserStoreDetailData | null> {
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,
}
} }

View file

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

View file

@ -1,44 +1,75 @@
import { db } from '../db/db_index'; import { db } from '../db/db_index'
import { users, userDetails, addresses } from '../db/schema'; import { notifCreds, unloggedUserTokens, userCreds, userDetails, users } from '../db/schema'
import { eq, desc } from 'drizzle-orm'; import { and, eq } from 'drizzle-orm'
export async function getCurrentUser(userId: number): Promise<any | null> { export async function getUserById(userId: number) {
return await db.query.users.findFirst({ const [user] = await db.select().from(users).where(eq(users.id, userId)).limit(1)
where: eq(users.id, userId), return user || null
with: {
userDetails: true,
},
});
} }
export async function updateUser(userId: number, updates: any): Promise<any> { export async function getUserDetailByUserId(userId: number) {
const [user] = await db.update(users) const [detail] = await db.select().from(userDetails).where(eq(userDetails.userId, userId)).limit(1)
.set(updates) 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)) .where(eq(users.id, userId))
.returning(); .limit(1)
return user;
if (result.length === 0) return null
return {
user: result[0].users,
creds: result[0].user_creds,
}
} }
export async function getUserAddresses(userId: number): Promise<any[]> { export async function getNotifCred(userId: number, token: string) {
return await db.query.addresses.findMany({ return db.query.notifCreds.findFirst({
where: eq(addresses.userId, userId), where: and(eq(notifCreds.userId, userId), eq(notifCreds.token, token)),
orderBy: desc(addresses.isDefault), })
});
} }
export async function createAddress(addressData: any): Promise<any> { export async function upsertNotifCred(userId: number, token: string): Promise<void> {
const [address] = await db.insert(addresses).values(addressData).returning(); const existing = await getNotifCred(userId, token)
return address; 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<any> { export async function deleteUnloggedToken(token: string): Promise<void> {
const [address] = await db.update(addresses) await db.delete(unloggedUserTokens).where(eq(unloggedUserTokens.token, token))
.set(updates)
.where(eq(addresses.id, addressId))
.returning();
return address;
} }
export async function deleteAddress(addressId: number): Promise<void> { export async function getUnloggedToken(token: string) {
await db.delete(addresses).where(eq(addresses.id, addressId)); return db.query.unloggedUserTokens.findFirst({
where: eq(unloggedUserTokens.token, token),
})
}
export async function upsertUnloggedToken(token: string): Promise<void> {
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(),
})
} }

View file

@ -28,6 +28,41 @@ export interface Address {
isDefault: boolean; 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 { export interface Product {
id: number; id: number;
name: string; name: string;
@ -80,3 +115,531 @@ export interface Payment {
amount: string; amount: string;
createdAt: Date; 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[];
}