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,
getSlotDeliverySequence,
updateSlotDeliverySequence,
// User address methods
getUserDefaultAddress,
getUserAddresses,
getUserAddressById,
clearUserDefaultAddress,
createUserAddress,
updateUserAddress,
deleteUserAddress,
hasOngoingOrdersForAddress,
getUserActiveBanners,
getUserCartItemsWithProducts,
getUserProductById,
getUserCartItemByUserProduct,
incrementUserCartItemQuantity,
insertUserCartItem,
updateUserCartItemQuantity,
deleteUserCartItem,
clearUserCart,
getUserComplaints,
createUserComplaint,
getUserStoreSummaries,
getUserStoreDetail,
getUserProductDetailById,
getUserProductReviews,
getUserProductByIdBasic,
createUserProductReview,
getUserActiveSlotsList,
getUserProductAvailability,
getUserPaymentOrderById,
getUserPaymentByOrderId,
getUserPaymentByMerchantOrderId,
updateUserPaymentSuccess,
updateUserOrderPaymentStatus,
markUserPaymentFailed,
getUserAuthByEmail,
getUserAuthByMobile,
getUserAuthById,
getUserAuthCreds,
getUserAuthDetails,
createUserAuthWithCreds,
createUserAuthWithMobile,
upsertUserAuthPassword,
deleteUserAuthAccount,
getUserActiveCouponsWithRelations,
getUserAllCouponsWithRelations,
getUserReservedCouponByCode,
redeemUserReservedCoupon,
getUserProfileById,
getUserProfileDetailById,
getUserWithCreds,
getUserNotifCred,
upsertUserNotifCred,
deleteUserUnloggedToken,
getUserUnloggedToken,
upsertUserUnloggedToken,
// Order methods
updateOrderNotes,
updateOrderPackaged,
@ -137,6 +192,26 @@ export {
rebalanceSlots,
cancelOrder,
deleteOrderById,
// User Order helpers
validateAndGetUserCoupon,
applyDiscountToUserOrder,
getUserAddressByIdAndUser,
getOrderProductById,
checkUserSuspended,
getUserSlotCapacityStatus,
placeUserOrderTransaction,
deleteUserCartItemsForOrder,
recordUserCouponUsage,
getUserOrdersWithRelations,
getUserOrderCount,
getUserOrderByIdWithRelations,
getUserCouponUsageForOrder,
getUserOrderBasic,
cancelUserOrderTransaction,
updateUserOrderNotes,
getUserRecentlyDeliveredOrderIds,
getUserProductIdsFromOrders,
getUserProductsForRecentOrders,
} from 'postgresService'
export async function getOrderDetails(orderId: number): Promise<AdminOrderDetails | null> {
@ -220,6 +295,72 @@ export type {
AdminVendorOrderSummary,
AdminUpcomingSlotsResult,
AdminVendorUpdatePackagingResult,
UserAddress,
UserAddressResponse,
UserAddressesResponse,
UserAddressDeleteResponse,
UserBanner,
UserBannersResponse,
UserCartProduct,
UserCartItem,
UserCartResponse,
UserComplaint,
UserComplaintsResponse,
UserRaiseComplaintResponse,
UserStoreSummary,
UserStoreSummaryData,
UserStoresResponse,
UserStoreSampleProduct,
UserStoreSampleProductData,
UserStoreDetail,
UserStoreDetailData,
UserStoreProduct,
UserStoreProductData,
UserTagSummary,
UserProductDetail,
UserProductDetailData,
UserProductReview,
UserProductReviewWithSignedUrls,
UserProductReviewsResponse,
UserCreateReviewResponse,
UserSlotProduct,
UserSlotWithProducts,
UserSlotData,
UserSlotAvailability,
UserDeliverySlot,
UserSlotsResponse,
UserSlotsWithProductsResponse,
UserSlotsListResponse,
UserPaymentOrderResponse,
UserPaymentVerifyResponse,
UserPaymentFailResponse,
UserAuthProfile,
UserAuthResponse,
UserAuthResult,
UserOtpVerifyResponse,
UserPasswordUpdateResponse,
UserProfileResponse,
UserDeleteAccountResponse,
UserCouponUsage,
UserCouponApplicableUser,
UserCouponApplicableProduct,
UserCoupon,
UserCouponWithRelations,
UserEligibleCouponsResponse,
UserCouponDisplay,
UserMyCouponsResponse,
UserRedeemCouponResponse,
UserSelfDataResponse,
UserProfileCompleteResponse,
UserSavePushTokenResponse,
UserOrderItemSummary,
UserOrderSummary,
UserOrdersResponse,
UserOrderDetail,
UserCancelOrderResponse,
UserUpdateNotesResponse,
UserRecentProduct,
UserRecentProductsResponse,
} from '@packages/shared';
export type {

View file

@ -1,30 +1,52 @@
import { router, protectedProcedure } from '@/src/trpc/trpc-index';
import { z } from 'zod';
import { db } from '@/src/db/db_index';
import { addresses, orders, orderStatus, deliverySlotInfo } from '@/src/db/schema';
import { eq, and, gte } from 'drizzle-orm';
import dayjs from 'dayjs';
import { extractCoordsFromRedirectUrl } from '@/src/lib/license-util';
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'
import { extractCoordsFromRedirectUrl } from '@/src/lib/license-util'
import {
getUserDefaultAddress as getDefaultAddressInDb,
getUserAddresses as getUserAddressesInDb,
getUserAddressById as getUserAddressByIdInDb,
clearUserDefaultAddress as clearDefaultAddressInDb,
createUserAddress as createUserAddressInDb,
updateUserAddress as updateUserAddressInDb,
deleteUserAddress as deleteUserAddressInDb,
hasOngoingOrdersForAddress as hasOngoingOrdersForAddressInDb,
} from '@/src/dbService'
import type {
UserAddressResponse,
UserAddressesResponse,
UserAddressDeleteResponse,
} from '@packages/shared'
export const addressRouter = router({
getDefaultAddress: protectedProcedure
.query(async ({ ctx }) => {
.query(async ({ ctx }): Promise<UserAddressResponse> => {
const userId = ctx.user.userId;
const defaultAddress = await getDefaultAddressInDb(userId)
/*
// Old implementation - direct DB queries:
const [defaultAddress] = await db
.select()
.from(addresses)
.where(and(eq(addresses.userId, userId), eq(addresses.isDefault, true)))
.limit(1);
*/
return { success: true, data: defaultAddress || null };
return { success: true, data: defaultAddress }
}),
getUserAddresses: protectedProcedure
.query(async ({ ctx }) => {
.query(async ({ ctx }): Promise<UserAddressesResponse> => {
const userId = ctx.user.userId;
const userAddresses = await getUserAddressesInDb(userId)
/*
// Old implementation - direct DB queries:
const userAddresses = await db.select().from(addresses).where(eq(addresses.userId, userId));
return { success: true, data: userAddresses };
*/
return { success: true, data: userAddresses }
}),
createAddress: protectedProcedure
@ -41,7 +63,7 @@ export const addressRouter = router({
longitude: z.number().optional(),
googleMapsUrl: z.string().optional(),
}))
.mutation(async ({ input, ctx }) => {
.mutation(async ({ input, ctx }): Promise<UserAddressResponse> => {
const userId = ctx.user.userId;
const { name, phone, addressLine1, addressLine2, city, state, pincode, isDefault, googleMapsUrl } = input;
@ -61,6 +83,27 @@ export const addressRouter = router({
}
// If setting as default, unset other defaults
if (isDefault) {
await clearDefaultAddressInDb(userId)
}
const newAddress = await createUserAddressInDb({
userId,
name,
phone,
addressLine1,
addressLine2,
city,
state,
pincode,
isDefault: isDefault || false,
latitude,
longitude,
googleMapsUrl,
})
/*
// Old implementation - direct DB queries:
if (isDefault) {
await db.update(addresses).set({ isDefault: false }).where(eq(addresses.userId, userId));
}
@ -79,8 +122,9 @@ export const addressRouter = router({
longitude,
googleMapsUrl,
}).returning();
*/
return { success: true, data: newAddress };
return { success: true, data: newAddress }
}),
updateAddress: protectedProcedure
@ -98,7 +142,7 @@ export const addressRouter = router({
longitude: z.number().optional(),
googleMapsUrl: z.string().optional(),
}))
.mutation(async ({ input, ctx }) => {
.mutation(async ({ input, ctx }): Promise<UserAddressResponse> => {
const userId = ctx.user.userId;
const { id, name, phone, addressLine1, addressLine2, city, state, pincode, isDefault, googleMapsUrl } = input;
@ -113,12 +157,34 @@ export const addressRouter = router({
}
// Check if address exists and belongs to user
const existingAddress = await db.select().from(addresses).where(and(eq(addresses.id, id), eq(addresses.userId, userId))).limit(1);
if (existingAddress.length === 0) {
throw new Error('Address not found');
const existingAddress = await getUserAddressByIdInDb(userId, id)
if (!existingAddress) {
throw new Error('Address not found')
}
// If setting as default, unset other defaults
if (isDefault) {
await clearDefaultAddressInDb(userId)
}
const updatedAddress = await updateUserAddressInDb({
userId,
addressId: id,
name,
phone,
addressLine1,
addressLine2,
city,
state,
pincode,
isDefault: isDefault || false,
googleMapsUrl,
latitude,
longitude,
})
/*
// Old implementation - direct DB queries:
if (isDefault) {
await db.update(addresses).set({ isDefault: false }).where(eq(addresses.userId, userId));
}
@ -143,25 +209,42 @@ export const addressRouter = router({
}
const [updatedAddress] = await db.update(addresses).set(updateData).where(and(eq(addresses.id, id), eq(addresses.userId, userId))).returning();
*/
return { success: true, data: updatedAddress };
return { success: true, data: updatedAddress }
}),
deleteAddress: protectedProcedure
.input(z.object({
id: z.number().int().positive(),
}))
.mutation(async ({ input, ctx }) => {
.mutation(async ({ input, ctx }): Promise<UserAddressDeleteResponse> => {
const userId = ctx.user.userId;
const { id } = input;
// Check if address exists and belongs to user
const existingAddress = await getUserAddressByIdInDb(userId, id)
if (!existingAddress) {
throw new Error('Address not found or does not belong to user')
}
const hasOngoingOrders = await hasOngoingOrdersForAddressInDb(id)
if (hasOngoingOrders) {
throw new Error('Address is attached to an ongoing order. Please cancel the order first.')
}
if (existingAddress.isDefault) {
throw new Error('Cannot delete default address. Please set another address as default first.')
}
const deleted = await deleteUserAddressInDb(userId, id)
/*
// Old implementation - direct DB queries:
const existingAddress = await db.select().from(addresses).where(and(eq(addresses.id, id), eq(addresses.userId, userId))).limit(1);
if (existingAddress.length === 0) {
throw new Error('Address not found or does not belong to user');
}
// Check if address is attached to any ongoing orders using joins
const ongoingOrders = await db.select({
order: orders,
status: orderStatus,
@ -181,14 +264,17 @@ export const addressRouter = router({
throw new Error('Address is attached to an ongoing order. Please cancel the order first.');
}
// Prevent deletion of default address
if (existingAddress[0].isDefault) {
throw new Error('Cannot delete default address. Please set another address as default first.');
}
// Delete the address
await db.delete(addresses).where(and(eq(addresses.id, id), eq(addresses.userId, userId)));
*/
return { success: true, message: 'Address deleted successfully' };
if (!deleted) {
throw new Error('Address not found or does not belong to user')
}
return { success: true, message: 'Address deleted successfully' }
}),
});

View file

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

View file

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

View file

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

View file

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

View file

@ -1,31 +1,20 @@
import { router, protectedProcedure } from '@/src/trpc/trpc-index';
import { z } from 'zod';
import { db } from '@/src/db/db_index';
import { coupons, couponUsage, couponApplicableUsers, reservedCoupons, couponApplicableProducts } from '@/src/db/schema';
import { eq, and, or, gt, isNull, sql } from 'drizzle-orm';
import { ApiError } from '@/src/lib/api-error';
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'
import { ApiError } from '@/src/lib/api-error'
import {
getUserActiveCouponsWithRelations as getUserActiveCouponsWithRelationsInDb,
getUserAllCouponsWithRelations as getUserAllCouponsWithRelationsInDb,
getUserReservedCouponByCode as getUserReservedCouponByCodeInDb,
redeemUserReservedCoupon as redeemUserReservedCouponInDb,
} from '@/src/dbService'
import type {
UserCouponDisplay,
UserEligibleCouponsResponse,
UserMyCouponsResponse,
UserRedeemCouponResponse,
} from '@packages/shared'
import { users } from '@/src/db/schema';
type CouponWithRelations = typeof coupons.$inferSelect & {
applicableUsers: (typeof couponApplicableUsers.$inferSelect & { user: typeof users.$inferSelect })[];
usages: typeof couponUsage.$inferSelect[];
};
export interface EligibleCoupon {
id: number;
code: string;
discountType: 'percentage' | 'flat';
discountValue: number;
maxValue?: number;
minOrder?: number;
description: string;
exclusiveApply?: boolean;
isEligible: boolean;
ineligibilityReason?: string;
}
const generateCouponDescription = (coupon: any): string => {
const generateCouponDescription = (coupon: { discountPercent?: string | null; flatDiscount?: string | null; minOrder?: string | null; maxValue?: string | null }): string => {
let desc = '';
if (coupon.discountPercent) {
@ -45,29 +34,17 @@ const generateCouponDescription = (coupon: any): string => {
return desc;
};
export interface CouponDisplay {
id: number;
code: string;
discountType: 'percentage' | 'flat';
discountValue: number;
maxValue?: number;
minOrder?: number;
description: string;
validTill?: Date;
usageCount: number;
maxLimitForUser?: number;
isExpired: boolean;
isUsedUp: boolean;
}
export const userCouponRouter = router({
getEligible: protectedProcedure
.query(async ({ ctx }) => {
.query(async ({ ctx }): Promise<UserEligibleCouponsResponse> => {
try {
const userId = ctx.user.userId;
// Get all active, non-expired coupons
const allCoupons = await getUserActiveCouponsWithRelationsInDb(userId)
/*
// Old implementation - direct DB queries:
const allCoupons = await db.query.coupons.findMany({
where: and(
eq(coupons.isInvalidated, false),
@ -92,6 +69,7 @@ export const userCouponRouter = router({
},
}
});
*/
// Filter to only coupons applicable to current user
const applicableCoupons = allCoupons.filter(coupon => {
@ -110,11 +88,15 @@ export const userCouponRouter = router({
getProductCoupons: protectedProcedure
.input(z.object({ productId: z.number().int().positive() }))
.query(async ({ input, ctx }) => {
.query(async ({ input, ctx }): Promise<UserEligibleCouponsResponse> => {
const userId = ctx.user.userId;
const { productId } = input;
// Get all active, non-expired coupons
const allCoupons = await getUserActiveCouponsWithRelationsInDb(userId)
/*
// Old implementation - direct DB queries:
const allCoupons = await db.query.coupons.findMany({
where: and(
eq(coupons.isInvalidated, false),
@ -139,6 +121,7 @@ export const userCouponRouter = router({
},
}
});
*/
// Filter to only coupons applicable to current user and product
const applicableCoupons = allCoupons.filter(coupon => {
@ -155,10 +138,13 @@ export const userCouponRouter = router({
}),
getMyCoupons: protectedProcedure
.query(async ({ ctx }) => {
.query(async ({ ctx }): Promise<UserMyCouponsResponse> => {
const userId = ctx.user.userId;
// Get all coupons
const allCoupons = await getUserAllCouponsWithRelationsInDb(userId)
/*
// Old implementation - direct DB queries:
const allCoupons = await db.query.coupons.findMany({
with: {
usages: {
@ -171,9 +157,10 @@ export const userCouponRouter = router({
}
}
});
*/
// Filter coupons in JS: not invalidated, applicable to user, and not expired
const applicableCoupons = (allCoupons as CouponWithRelations[]).filter(coupon => {
const applicableCoupons = allCoupons.filter(coupon => {
const isNotInvalidated = !coupon.isInvalidated;
const applicableUsers = coupon.applicableUsers || [];
const isApplicable = coupon.isApplyForAll || applicableUsers.some(au => au.userId === userId);
@ -182,15 +169,15 @@ export const userCouponRouter = router({
});
// Categorize coupons
const personalCoupons: CouponDisplay[] = [];
const generalCoupons: CouponDisplay[] = [];
const personalCoupons: UserCouponDisplay[] = [];
const generalCoupons: UserCouponDisplay[] = [];
applicableCoupons.forEach(coupon => {
const usageCount = coupon.usages.length;
const isExpired = false; // Already filtered out expired coupons
const isUsedUp = Boolean(coupon.maxLimitForUser && usageCount >= coupon.maxLimitForUser);
const couponDisplay: CouponDisplay = {
const couponDisplay: UserCouponDisplay = {
id: coupon.id,
code: coupon.couponCode,
discountType: coupon.discountPercent ? 'percentage' : 'flat',
@ -225,17 +212,21 @@ export const userCouponRouter = router({
redeemReservedCoupon: protectedProcedure
.input(z.object({ secretCode: z.string() }))
.mutation(async ({ input, ctx }) => {
.mutation(async ({ input, ctx }): Promise<UserRedeemCouponResponse> => {
const userId = ctx.user.userId;
const { secretCode } = input;
// Find the reserved coupon
const reservedCoupon = await getUserReservedCouponByCodeInDb(secretCode)
/*
// Old implementation - direct DB queries:
const reservedCoupon = await db.query.reservedCoupons.findFirst({
where: and(
eq(reservedCoupons.secretCode, secretCode.toUpperCase()),
eq(reservedCoupons.isRedeemed, false)
),
});
*/
if (!reservedCoupon) {
throw new ApiError("Invalid or already redeemed coupon code", 400);
@ -246,9 +237,11 @@ export const userCouponRouter = router({
throw new ApiError("You have already redeemed this coupon", 400);
}
// Create the coupon in the main table
const couponResult = await redeemUserReservedCouponInDb(userId, reservedCoupon)
/*
// Old implementation - direct DB queries:
const couponResult = await db.transaction(async (tx) => {
// Insert into coupons
const couponInsert = await tx.insert(coupons).values({
couponCode: reservedCoupon.couponCode,
isUserBased: true,
@ -266,22 +259,11 @@ export const userCouponRouter = router({
const coupon = couponInsert[0];
// Insert into couponApplicableUsers
await tx.insert(couponApplicableUsers).values({
couponId: coupon.id,
userId,
});
// Copy applicable products
if (reservedCoupon.productIds && Array.isArray(reservedCoupon.productIds) && reservedCoupon.productIds.length > 0) {
// Assuming productIds are the IDs, but wait, in schema, productIds is jsonb, but in relations, couponApplicableProducts has productId
// For simplicity, since reservedCoupons has productIds as jsonb, but to match, perhaps insert into couponApplicableProducts if needed
// But in createReservedCoupon, I inserted applicableProducts into couponApplicableProducts
// So for reserved, perhaps do the same, but since it's jsonb, maybe not.
// For now, skip, as the coupon will have productIds in coupons table.
}
// Update reserved coupon as redeemed
await tx.update(reservedCoupons).set({
isRedeemed: true,
redeemedBy: userId,
@ -290,6 +272,7 @@ export const userCouponRouter = router({
return coupon;
});
*/
return { success: true, coupon: couponResult };
}),

View file

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

View file

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

View file

@ -1,39 +1,34 @@
import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index';
import { z } from 'zod';
import { db } from '@/src/db/db_index';
import { productInfo, units, productSlots, deliverySlotInfo, specialDeals, storeInfo, productTagInfo, productTags, productReviews, users } from '@/src/db/schema';
import { claimUploadUrl, extractKeyFromPresignedUrl, scaffoldAssetUrl } from '@/src/lib/s3-client';
import { ApiError } from '@/src/lib/api-error';
import { eq, and, gt, sql, inArray, desc } from 'drizzle-orm';
import { getProductById as getProductByIdFromCache, getAllProducts as getAllProductsFromCache } from '@/src/stores/product-store';
import dayjs from 'dayjs';
import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'
import { claimUploadUrl, extractKeyFromPresignedUrl, scaffoldAssetUrl } from '@/src/lib/s3-client'
import { ApiError } from '@/src/lib/api-error'
import { getProductById as getProductByIdFromCache, getAllProducts as getAllProductsFromCache } from '@/src/stores/product-store'
import dayjs from 'dayjs'
import {
getUserProductDetailById as getUserProductDetailByIdInDb,
getUserProductReviews as getUserProductReviewsInDb,
getUserProductByIdBasic as getUserProductByIdBasicInDb,
createUserProductReview as createUserProductReviewInDb,
} from '@/src/dbService'
import type {
UserProductDetail,
UserProductDetailData,
UserProductReviewsResponse,
UserCreateReviewResponse,
UserProductReviewWithSignedUrls,
} from '@packages/shared'
// Uniform Product Type
interface Product {
id: number;
name: string;
shortDescription: string | null;
longDescription: string | null;
price: string;
marketPrice: string | null;
unitNotation: string;
images: string[];
isOutOfStock: boolean;
store: { id: number; name: string; description: string | null } | null;
incrementStep: number;
productQuantity: number;
isFlashAvailable: boolean;
flashPrice: string | null;
deliverySlots: Array<{ id: number; deliveryTime: Date; freezeTime: Date }>;
specialDeals: Array<{ quantity: string; price: string; validTill: Date }>;
}
const signProductImages = (product: UserProductDetailData): UserProductDetail => ({
...product,
images: scaffoldAssetUrl(product.images || []),
})
export const productRouter = router({
getProductDetails: publicProcedure
.input(z.object({
id: z.string().regex(/^\d+$/, 'Invalid product ID'),
}))
.query(async ({ input }): Promise<Product> => {
.query(async ({ input }): Promise<UserProductDetail> => {
const { id } = input;
const productId = parseInt(id);
@ -60,6 +55,10 @@ export const productRouter = router({
}
// If not in cache, fetch from database (fallback)
const productData = await getUserProductDetailByIdInDb(productId)
/*
// Old implementation - direct DB queries:
const productData = await db
.select({
id: productInfo.id,
@ -81,82 +80,13 @@ export const productRouter = router({
.innerJoin(units, eq(productInfo.unitId, units.id))
.where(eq(productInfo.id, productId))
.limit(1);
*/
if (productData.length === 0) {
throw new Error('Product not found');
if (!productData) {
throw new Error('Product not found')
}
const product = productData[0];
// Fetch store info for this product
const storeData = product.storeId ? await db.query.storeInfo.findFirst({
where: eq(storeInfo.id, product.storeId),
columns: { id: true, name: true, description: true },
}) : null;
// Fetch delivery slots for this product
const deliverySlotsData = await db
.select({
id: deliverySlotInfo.id,
deliveryTime: deliverySlotInfo.deliveryTime,
freezeTime: deliverySlotInfo.freezeTime,
})
.from(productSlots)
.innerJoin(deliverySlotInfo, eq(productSlots.slotId, deliverySlotInfo.id))
.where(
and(
eq(productSlots.productId, productId),
eq(deliverySlotInfo.isActive, true),
eq(deliverySlotInfo.isCapacityFull, false),
gt(deliverySlotInfo.deliveryTime, sql`NOW()`),
gt(deliverySlotInfo.freezeTime, sql`NOW()`)
)
)
.orderBy(deliverySlotInfo.deliveryTime);
// Fetch special deals for this product
const specialDealsData = await db
.select({
quantity: specialDeals.quantity,
price: specialDeals.price,
validTill: specialDeals.validTill,
})
.from(specialDeals)
.where(
and(
eq(specialDeals.productId, productId),
gt(specialDeals.validTill, sql`NOW()`)
)
)
.orderBy(specialDeals.quantity);
// Generate signed URLs for images
const signedImages = scaffoldAssetUrl((product.images as string[]) || []);
const response: Product = {
id: product.id,
name: product.name,
shortDescription: product.shortDescription,
longDescription: product.longDescription,
price: product.price.toString(),
marketPrice: product.marketPrice?.toString() || null,
unitNotation: product.unitShortNotation,
images: signedImages,
isOutOfStock: product.isOutOfStock,
store: storeData ? {
id: storeData.id,
name: storeData.name,
description: storeData.description,
} : null,
incrementStep: product.incrementStep,
productQuantity: product.productQuantity,
isFlashAvailable: product.isFlashAvailable,
flashPrice: product.flashPrice?.toString() || null,
deliverySlots: deliverySlotsData,
specialDeals: specialDealsData.map(d => ({ quantity: d.quantity.toString(), price: d.price.toString(), validTill: d.validTill })),
};
return response;
return signProductImages(productData)
}),
getProductReviews: publicProcedure
@ -165,9 +95,13 @@ export const productRouter = router({
limit: z.number().int().min(1).max(50).optional().default(10),
offset: z.number().int().min(0).optional().default(0),
}))
.query(async ({ input }) => {
.query(async ({ input }): Promise<UserProductReviewsResponse> => {
const { productId, limit, offset } = input;
const { reviews, totalCount } = await getUserProductReviewsInDb(productId, limit, offset)
/*
// Old implementation - direct DB queries:
const reviews = await db
.select({
id: productReviews.id,
@ -184,15 +118,6 @@ export const productRouter = router({
.limit(limit)
.offset(offset);
// Generate signed URLs for images
const reviewsWithSignedUrls = await Promise.all(
reviews.map(async (review) => ({
...review,
signedImageUrls: scaffoldAssetUrl((review.imageUrls as string[]) || []),
}))
);
// Check if more reviews exist
const totalCountResult = await db
.select({ count: sql`count(*)` })
.from(productReviews)
@ -200,8 +125,16 @@ export const productRouter = router({
const totalCount = Number(totalCountResult[0].count);
const hasMore = offset + limit < totalCount;
*/
return { reviews: reviewsWithSignedUrls, hasMore };
const reviewsWithSignedUrls: UserProductReviewWithSignedUrls[] = reviews.map((review) => ({
...review,
signedImageUrls: scaffoldAssetUrl(review.imageUrls || []),
}))
const hasMore = offset + limit < totalCount
return { reviews: reviewsWithSignedUrls, hasMore }
}),
createReview: protectedProcedure
@ -212,11 +145,20 @@ export const productRouter = router({
imageUrls: z.array(z.string()).optional().default([]),
uploadUrls: z.array(z.string()).optional().default([]),
}))
.mutation(async ({ input, ctx }) => {
.mutation(async ({ input, ctx }): Promise<UserCreateReviewResponse> => {
const { productId, reviewBody, ratings, imageUrls, uploadUrls } = input;
const userId = ctx.user.userId;
// Optional: Check if product exists
const product = await getUserProductByIdBasicInDb(productId)
if (!product) {
throw new ApiError('Product not found', 404)
}
const imageKeys = uploadUrls.map(item => extractKeyFromPresignedUrl(item))
const newReview = await createUserProductReviewInDb(userId, productId, reviewBody, ratings, imageKeys)
/*
// Old implementation - direct DB queries:
const product = await db.query.productInfo.findFirst({
where: eq(productInfo.id, productId),
});
@ -224,7 +166,6 @@ export const productRouter = router({
throw new ApiError('Product not found', 404);
}
// Insert review
const [newReview] = await db.insert(productReviews).values({
userId,
productId,
@ -232,6 +173,7 @@ export const productRouter = router({
ratings,
imageUrls: uploadUrls.map(item => extractKeyFromPresignedUrl(item)),
}).returning();
*/
// Claim upload URLs
if (uploadUrls && uploadUrls.length > 0) {
@ -243,24 +185,25 @@ export const productRouter = router({
}
}
return { success: true, review: newReview };
return { success: true, review: newReview }
}),
getAllProductsSummary: publicProcedure
.query(async (): Promise<Product[]> => {
.query(async (): Promise<UserProductDetail[]> => {
// Get all products from cache
const allCachedProducts = await getAllProductsFromCache();
// Transform the cached products to match the expected summary format
// (with empty deliverySlots and specialDeals arrays for summary view)
const transformedProducts = allCachedProducts.map(product => ({
const transformedProducts: UserProductDetail[] = allCachedProducts.map(product => ({
...product,
deliverySlots: [], // Empty for summary view
specialDeals: [], // Empty for summary view
}));
images: product.images || [],
deliverySlots: [],
specialDeals: [],
}))
return transformedProducts;
return transformedProducts
}),
});

View file

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

View file

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

View file

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

View file

@ -165,7 +165,123 @@ export {
getVendorOrders,
} from './src/admin-apis/vendor-snippets';
// Note: User API helpers are available in their respective files
// but not exported from main index to avoid naming conflicts
// Import them directly from the file paths if needed:
// import { getAllProducts } from '@packages/db_helper_postgres/src/user-apis/product'
export {
// User Address
getDefaultAddress as getUserDefaultAddress,
getUserAddresses,
getUserAddressById,
clearDefaultAddress as clearUserDefaultAddress,
createUserAddress,
updateUserAddress,
deleteUserAddress,
hasOngoingOrdersForAddress,
} from './src/user-apis/address';
export {
// User Banners
getActiveBanners as getUserActiveBanners,
} from './src/user-apis/banners';
export {
// User Cart
getCartItemsWithProducts as getUserCartItemsWithProducts,
getProductById as getUserProductById,
getCartItemByUserProduct as getUserCartItemByUserProduct,
incrementCartItemQuantity as incrementUserCartItemQuantity,
insertCartItem as insertUserCartItem,
updateCartItemQuantity as updateUserCartItemQuantity,
deleteCartItem as deleteUserCartItem,
clearUserCart,
} from './src/user-apis/cart';
export {
// User Complaint
getUserComplaints as getUserComplaints,
createComplaint as createUserComplaint,
} from './src/user-apis/complaint';
export {
// User Stores
getStoreSummaries as getUserStoreSummaries,
getStoreDetail as getUserStoreDetail,
} from './src/user-apis/stores';
export {
// User Product
getProductDetailById as getUserProductDetailById,
getProductReviews as getUserProductReviews,
getProductById as getUserProductByIdBasic,
createProductReview as createUserProductReview,
} from './src/user-apis/product';
export {
// User Slots
getActiveSlotsList as getUserActiveSlotsList,
getProductAvailability as getUserProductAvailability,
} from './src/user-apis/slots';
export {
// User Payments
getOrderById as getUserPaymentOrderById,
getPaymentByOrderId as getUserPaymentByOrderId,
getPaymentByMerchantOrderId as getUserPaymentByMerchantOrderId,
updatePaymentSuccess as updateUserPaymentSuccess,
updateOrderPaymentStatus as updateUserOrderPaymentStatus,
markPaymentFailed as markUserPaymentFailed,
} from './src/user-apis/payments';
export {
// User Auth
getUserByEmail as getUserAuthByEmail,
getUserByMobile as getUserAuthByMobile,
getUserById as getUserAuthById,
getUserCreds as getUserAuthCreds,
getUserDetails as getUserAuthDetails,
createUserWithCreds as createUserAuthWithCreds,
createUserWithMobile as createUserAuthWithMobile,
upsertUserPassword as upsertUserAuthPassword,
deleteUserAccount as deleteUserAuthAccount,
} from './src/user-apis/auth';
export {
// User Coupon
getActiveCouponsWithRelations as getUserActiveCouponsWithRelations,
getAllCouponsWithRelations as getUserAllCouponsWithRelations,
getReservedCouponByCode as getUserReservedCouponByCode,
redeemReservedCoupon as redeemUserReservedCoupon,
} from './src/user-apis/coupon';
export {
// User Profile
getUserById as getUserProfileById,
getUserDetailByUserId as getUserProfileDetailById,
getUserWithCreds as getUserWithCreds,
getNotifCred as getUserNotifCred,
upsertNotifCred as upsertUserNotifCred,
deleteUnloggedToken as deleteUserUnloggedToken,
getUnloggedToken as getUserUnloggedToken,
upsertUnloggedToken as upsertUserUnloggedToken,
} from './src/user-apis/user';
export {
// User Order
validateAndGetCoupon as validateAndGetUserCoupon,
applyDiscountToOrder as applyDiscountToUserOrder,
getAddressByIdAndUser as getUserAddressByIdAndUser,
getProductById as getOrderProductById,
checkUserSuspended,
getSlotCapacityStatus as getUserSlotCapacityStatus,
placeOrderTransaction as placeUserOrderTransaction,
deleteCartItemsForOrder as deleteUserCartItemsForOrder,
recordCouponUsage as recordUserCouponUsage,
getOrdersWithRelations as getUserOrdersWithRelations,
getOrderCount as getUserOrderCount,
getOrderByIdWithRelations as getUserOrderByIdWithRelations,
getCouponUsageForOrder as getUserCouponUsageForOrder,
getOrderBasic as getUserOrderBasic,
cancelOrderTransaction as cancelUserOrderTransaction,
updateOrderNotes as updateUserOrderNotes,
getRecentlyDeliveredOrderIds as getUserRecentlyDeliveredOrderIds,
getProductIdsFromOrders as getUserProductIdsFromOrders,
getProductsForRecentOrders as getUserProductsForRecentOrders,
} from './src/user-apis/order';

View file

@ -1,23 +1,148 @@
import { db } from '../db/db_index';
import { addresses, addressAreas, addressZones } from '../db/schema';
import { eq, desc } from 'drizzle-orm';
import { db } from '../db/db_index'
import { addresses, deliverySlotInfo, orders, orderStatus } from '../db/schema'
import { and, eq, gte } from 'drizzle-orm'
import type { InferSelectModel } from 'drizzle-orm'
import type { UserAddress } from '@packages/shared'
export async function getZones(): Promise<any[]> {
const zones = await db.select().from(addressZones).orderBy(desc(addressZones.addedAt));
return zones;
type AddressRow = InferSelectModel<typeof addresses>
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[]> {
const areas = await db.select().from(addressAreas).orderBy(desc(addressAreas.createdAt));
return areas;
export async function getUserAddresses(userId: number): Promise<UserAddress[]> {
const userAddresses = await db.select().from(addresses).where(eq(addresses.userId, userId))
return userAddresses.map(mapUserAddress)
}
export async function createZone(zoneName: string): Promise<any> {
const [zone] = await db.insert(addressZones).values({ zoneName }).returning();
return zone;
export async function getUserAddressById(userId: number, addressId: number): Promise<UserAddress | null> {
const [address] = await db
.select()
.from(addresses)
.where(and(eq(addresses.id, addressId), eq(addresses.userId, userId)))
.limit(1)
return address ? mapUserAddress(address) : null
}
export async function createArea(placeName: string, zoneId: number | null): Promise<any> {
const [area] = await db.insert(addressAreas).values({ placeName, zoneId }).returning();
return area;
export async function clearDefaultAddress(userId: number): Promise<void> {
await db.update(addresses).set({ isDefault: false }).where(eq(addresses.userId, userId))
}
export async function createUserAddress(input: {
userId: number
name: string
phone: string
addressLine1: string
addressLine2?: string
city: string
state: string
pincode: string
isDefault: boolean
latitude?: number
longitude?: number
googleMapsUrl?: string
}): Promise<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 { users } from '../db/schema';
import { eq } from 'drizzle-orm';
import { db } from '../db/db_index'
import {
users,
userCreds,
userDetails,
addresses,
cartItems,
complaints,
couponApplicableUsers,
couponUsage,
notifCreds,
notifications,
orderItems,
orderStatus,
orders,
payments,
refunds,
productReviews,
reservedCoupons,
} from '../db/schema'
import { eq } from 'drizzle-orm'
export async function getUserByMobile(mobile: string): Promise<any | null> {
return await db.query.users.findFirst({
where: eq(users.mobile, mobile),
});
export async function getUserByEmail(email: string) {
const [user] = await db.select().from(users).where(eq(users.email, email)).limit(1)
return user || null
}
export async function createUser(userData: any): Promise<any> {
const [user] = await db.insert(users).values(userData).returning();
return user;
export async function getUserByMobile(mobile: string) {
const [user] = await db.select().from(users).where(eq(users.mobile, mobile)).limit(1)
return user || null
}
export async function getUserById(userId: number) {
const [user] = await db.select().from(users).where(eq(users.id, userId)).limit(1)
return user || null
}
export async function getUserCreds(userId: number) {
const [creds] = await db.select().from(userCreds).where(eq(userCreds.userId, userId)).limit(1)
return creds || null
}
export async function getUserDetails(userId: number) {
const [details] = await db.select().from(userDetails).where(eq(userDetails.userId, userId)).limit(1)
return details || null
}
export async function createUserWithCreds(input: {
name: string
email: string
mobile: string
hashedPassword: string
}) {
return db.transaction(async (tx) => {
const [user] = await tx.insert(users).values({
name: input.name,
email: input.email,
mobile: input.mobile,
}).returning()
await tx.insert(userCreds).values({
userId: user.id,
userPassword: input.hashedPassword,
})
return user
})
}
export async function createUserWithMobile(mobile: string) {
const [user] = await db.insert(users).values({
name: null,
email: null,
mobile,
}).returning()
return user
}
export async function upsertUserPassword(userId: number, hashedPassword: string) {
try {
await db.insert(userCreds).values({
userId,
userPassword: hashedPassword,
})
return
} catch (error: any) {
if (error.code === '23505') {
await db.update(userCreds).set({
userPassword: hashedPassword,
}).where(eq(userCreds.userId, userId))
return
}
throw error
}
}
export async function deleteUserAccount(userId: number) {
await db.transaction(async (tx) => {
await tx.delete(notifCreds).where(eq(notifCreds.userId, userId))
await tx.delete(couponApplicableUsers).where(eq(couponApplicableUsers.userId, userId))
await tx.delete(couponUsage).where(eq(couponUsage.userId, userId))
await tx.delete(complaints).where(eq(complaints.userId, userId))
await tx.delete(cartItems).where(eq(cartItems.userId, userId))
await tx.delete(notifications).where(eq(notifications.userId, userId))
await tx.delete(productReviews).where(eq(productReviews.userId, userId))
await tx.update(reservedCoupons)
.set({ redeemedBy: null })
.where(eq(reservedCoupons.redeemedBy, userId))
const userOrders = await tx
.select({ id: orders.id })
.from(orders)
.where(eq(orders.userId, userId))
for (const order of userOrders) {
await tx.delete(orderItems).where(eq(orderItems.orderId, order.id))
await tx.delete(orderStatus).where(eq(orderStatus.orderId, order.id))
await tx.delete(payments).where(eq(payments.orderId, order.id))
await tx.delete(refunds).where(eq(refunds.orderId, order.id))
await tx.delete(couponUsage).where(eq(couponUsage.orderId, order.id))
await tx.delete(complaints).where(eq(complaints.orderId, order.id))
}
await tx.delete(orders).where(eq(orders.userId, userId))
await tx.delete(addresses).where(eq(addresses.userId, userId))
await tx.delete(userDetails).where(eq(userDetails.userId, userId))
await tx.delete(userCreds).where(eq(userCreds.userId, userId))
await tx.delete(users).where(eq(users.id, userId))
})
}

View file

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

View file

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

View file

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

View file

@ -1,43 +1,146 @@
import { db } from '../db/db_index';
import { coupons, couponUsage } from '../db/schema';
import { eq, and } from 'drizzle-orm';
import { db } from '../db/db_index'
import {
couponApplicableProducts,
couponApplicableUsers,
couponUsage,
coupons,
reservedCoupons,
} from '../db/schema'
import { and, eq, gt, isNull, or } from 'drizzle-orm'
import type { InferSelectModel } from 'drizzle-orm'
import type { UserCoupon, UserCouponApplicableProduct, UserCouponApplicableUser, UserCouponUsage, UserCouponWithRelations } from '@packages/shared'
export async function validateUserCoupon(code: string, userId: number, orderAmount: number): Promise<any> {
const coupon = await db.query.coupons.findFirst({
where: eq(coupons.couponCode, code.toUpperCase()),
});
type CouponRow = InferSelectModel<typeof coupons>
type CouponUsageRow = InferSelectModel<typeof couponUsage>
type CouponApplicableUserRow = InferSelectModel<typeof couponApplicableUsers>
type CouponApplicableProductRow = InferSelectModel<typeof couponApplicableProducts>
type ReservedCouponRow = InferSelectModel<typeof reservedCoupons>
if (!coupon || coupon.isInvalidated) {
return { valid: false, message: 'Invalid coupon' };
const mapCoupon = (coupon: CouponRow): UserCoupon => ({
id: coupon.id,
couponCode: coupon.couponCode,
isUserBased: coupon.isUserBased,
discountPercent: coupon.discountPercent ? coupon.discountPercent.toString() : null,
flatDiscount: coupon.flatDiscount ? coupon.flatDiscount.toString() : null,
minOrder: coupon.minOrder ? coupon.minOrder.toString() : null,
productIds: coupon.productIds,
maxValue: coupon.maxValue ? coupon.maxValue.toString() : null,
isApplyForAll: coupon.isApplyForAll,
validTill: coupon.validTill ?? null,
maxLimitForUser: coupon.maxLimitForUser ?? null,
isInvalidated: coupon.isInvalidated,
exclusiveApply: coupon.exclusiveApply,
createdAt: coupon.createdAt,
})
const mapUsage = (usage: CouponUsageRow): UserCouponUsage => ({
id: usage.id,
userId: usage.userId,
couponId: usage.couponId,
orderId: usage.orderId ?? null,
orderItemId: usage.orderItemId ?? null,
usedAt: usage.usedAt,
})
const mapApplicableUser = (applicable: CouponApplicableUserRow): UserCouponApplicableUser => ({
id: applicable.id,
couponId: applicable.couponId,
userId: applicable.userId,
})
const mapApplicableProduct = (applicable: CouponApplicableProductRow): UserCouponApplicableProduct => ({
id: applicable.id,
couponId: applicable.couponId,
productId: applicable.productId,
})
const mapCouponWithRelations = (coupon: CouponRow & {
usages: CouponUsageRow[]
applicableUsers: CouponApplicableUserRow[]
applicableProducts: CouponApplicableProductRow[]
}): UserCouponWithRelations => ({
...mapCoupon(coupon),
usages: coupon.usages.map(mapUsage),
applicableUsers: coupon.applicableUsers.map(mapApplicableUser),
applicableProducts: coupon.applicableProducts.map(mapApplicableProduct),
})
export async function getActiveCouponsWithRelations(userId: number): Promise<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)
}
if (coupon.validTill && new Date(coupon.validTill) < new Date()) {
return { valid: false, message: 'Coupon expired' };
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)
}
let discountAmount = 0;
if (coupon.discountPercent) {
discountAmount = (orderAmount * parseFloat(coupon.discountPercent)) / 100;
} else if (coupon.flatDiscount) {
discountAmount = parseFloat(coupon.flatDiscount);
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
}
if (coupon.maxValue) {
const maxDiscount = parseFloat(coupon.maxValue);
if (discountAmount > maxDiscount) {
discountAmount = maxDiscount;
}
}
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()
return {
valid: true,
discountAmount,
await tx.insert(couponApplicableUsers).values({
couponId: coupon.id,
};
}
userId,
})
export async function getUserCoupons(userId: number): Promise<any[]> {
return await db.query.coupons.findMany({
where: eq(coupons.userId, userId),
});
await tx.update(reservedCoupons).set({
isRedeemed: true,
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 { orders, orderItems, orderStatus } from '../db/schema';
import { eq, desc } from 'drizzle-orm';
import { db } from '../db/db_index'
import {
orders,
orderItems,
orderStatus,
addresses,
productInfo,
paymentInfoTable,
coupons,
couponUsage,
cartItems,
refunds,
units,
userDetails,
deliverySlotInfo,
} from '../db/schema'
import { and, eq, inArray, desc, gte, lte } from 'drizzle-orm'
import type {
UserOrderSummary,
UserOrderDetail,
UserRecentProduct,
} from '@packages/shared'
export async function getUserOrders(userId: number): Promise<any[]> {
return await db.query.orders.findMany({
export interface OrderItemInput {
productId: number
quantity: number
slotId: number | null
}
export interface PlaceOrderInput {
userId: number
selectedItems: OrderItemInput[]
addressId: number
paymentMethod: 'online' | 'cod'
couponId?: number
userNotes?: string
isFlash?: boolean
}
export interface OrderGroupData {
slotId: number | null
items: Array<{
productId: number
quantity: number
slotId: number | null
product: typeof productInfo.$inferSelect
}>
}
export interface PlacedOrder {
id: number
userId: number
addressId: number
slotId: number | null
totalAmount: string
deliveryCharge: string
isCod: boolean
isOnlinePayment: boolean
paymentInfoId: number | null
readableId: number
userNotes: string | null
orderGroupId: string
orderGroupProportion: string
isFlashDelivery: boolean
createdAt: Date
}
export interface OrderWithRelations {
id: number
userId: number
addressId: number
slotId: number | null
totalAmount: string
deliveryCharge: string
isCod: boolean
isOnlinePayment: boolean
isFlashDelivery: boolean
userNotes: string | null
createdAt: Date
orderItems: Array<{
id: number
productId: number
quantity: string
price: string
discountedPrice: string | null
is_packaged: boolean
product: {
id: number
name: string
images: unknown
}
}>
slot: {
deliveryTime: Date
} | null
paymentInfo: {
id: number
status: string
} | null
orderStatus: Array<{
id: number
isCancelled: boolean
isDelivered: boolean
paymentStatus: string
cancelReason: string | null
}>
refunds: Array<{
refundStatus: string
refundAmount: string | null
}>
}
export interface OrderDetailWithRelations {
id: number
userId: number
addressId: number
slotId: number | null
totalAmount: string
deliveryCharge: string
isCod: boolean
isOnlinePayment: boolean
isFlashDelivery: boolean
userNotes: string | null
createdAt: Date
orderItems: Array<{
id: number
productId: number
quantity: string
price: string
discountedPrice: string | null
is_packaged: boolean
product: {
id: number
name: string
images: unknown
}
}>
slot: {
deliveryTime: Date
} | null
paymentInfo: {
id: number
status: string
} | null
orderStatus: Array<{
id: number
isCancelled: boolean
isDelivered: boolean
paymentStatus: string
cancelReason: string | null
}>
refunds: Array<{
refundStatus: string
refundAmount: string | null
}>
}
export interface CouponValidationResult {
id: number
couponCode: string
isInvalidated: boolean
validTill: Date | null
maxLimitForUser: number | null
minOrder: string | null
discountPercent: string | null
flatDiscount: string | null
maxValue: string | null
usages: Array<{
id: number
userId: number
}>
}
export interface CouponUsageWithCoupon {
id: number
couponId: number
orderId: number | null
coupon: {
id: number
couponCode: string
discountPercent: string | null
flatDiscount: string | null
maxValue: string | null
}
}
export async function validateAndGetCoupon(
couponId: number | undefined,
userId: number,
totalAmount: number
): Promise<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),
with: {
orderItems: {
with: {
product: {
with: {
unit: true,
columns: {
id: true,
name: true,
images: true,
},
},
},
},
orderStatus: true,
slot: true,
slot: {
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> {
return await db.query.orders.findFirst({
where: eq(orders.id, orderId),
export async function getOrderCount(userId: number): Promise<number> {
const result = await db
.select({ count: db.$count(orders, eq(orders.userId, userId)) })
.from(orders)
.where(eq(orders.userId, userId))
return result[0]?.count ?? 0
}
export async function getOrderByIdWithRelations(
orderId: number,
userId: number
): Promise<OrderDetailWithRelations | null> {
const order = await db.query.orders.findFirst({
where: and(eq(orders.id, orderId), eq(orders.userId, userId)),
with: {
orderItems: {
with: {
product: true,
product: {
columns: {
id: true,
name: true,
images: true,
},
},
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> {
return await db.transaction(async (tx) => {
const [order] = await tx.insert(orders).values(orderData).returning();
for (const item of orderItemsData) {
await tx.insert(orderItems).values({
...item,
orderId: order.id,
});
export async function getCouponUsageForOrder(
orderId: number
): Promise<CouponUsageWithCoupon[]> {
return db.query.couponUsage.findMany({
where: eq(couponUsage.orderId, orderId),
with: {
coupon: {
columns: {
id: true,
couponCode: true,
discountPercent: true,
flatDiscount: true,
maxValue: true,
},
},
},
}) as Promise<CouponUsageWithCoupon[]>
}
await tx.insert(orderStatus).values({
orderId: order.id,
paymentStatus: 'pending',
});
return order;
});
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 { payments, orders, orderStatus } from '../db/schema';
import { eq } from 'drizzle-orm';
import { db } from '../db/db_index'
import { orders, payments, orderStatus } from '../db/schema'
import { eq } from 'drizzle-orm'
export async function createPayment(paymentData: any): Promise<any> {
const [payment] = await db.insert(payments).values(paymentData).returning();
return payment;
export async function getOrderById(orderId: number) {
return db.query.orders.findFirst({
where: eq(orders.id, orderId),
})
}
export async function updatePaymentStatus(paymentId: number, status: string): Promise<any> {
const [payment] = await db.update(payments)
.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({
export async function getPaymentByOrderId(orderId: number) {
return db.query.payments.findFirst({
where: eq(payments.orderId, orderId),
});
})
}
export async function getPaymentByMerchantOrderId(merchantOrderId: string) {
return db.query.payments.findFirst({
where: eq(payments.merchantOrderId, merchantOrderId),
})
}
export async function updatePaymentSuccess(merchantOrderId: string, payload: unknown) {
const [updatedPayment] = await db
.update(payments)
.set({
status: 'success',
payload,
})
.where(eq(payments.merchantOrderId, merchantOrderId))
.returning({
id: payments.id,
orderId: payments.orderId,
})
return updatedPayment || null
}
export async function updateOrderPaymentStatus(orderId: number, status: 'pending' | 'success' | 'cod' | 'failed') {
await db
.update(orderStatus)
.set({ paymentStatus: status })
.where(eq(orderStatus.orderId, orderId))
}
export async function markPaymentFailed(paymentId: number) {
await db
.update(payments)
.set({ status: 'failed' })
.where(eq(payments.id, paymentId))
}

View file

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

View file

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

View file

@ -1,20 +1,127 @@
import { db } from '../db/db_index';
import { storeInfo } from '../db/schema';
import { eq } from 'drizzle-orm';
import { db } from '../db/db_index'
import { productInfo, storeInfo, units } from '../db/schema'
import { and, eq, sql } from 'drizzle-orm'
import type { InferSelectModel } from 'drizzle-orm'
import type { UserStoreDetailData, UserStoreProductData, UserStoreSummaryData } from '@packages/shared'
export async function getAllStores(): Promise<any[]> {
return await db.query.storeInfo.findMany({
with: {
owner: true,
},
});
type StoreRow = InferSelectModel<typeof storeInfo>
type StoreProductRow = {
id: number
name: string
shortDescription: string | null
price: string
marketPrice: string | null
images: unknown
isOutOfStock: boolean
incrementStep: number
unitShortNotation: string
productQuantity: number
}
export async function getStoreById(id: number): Promise<any | null> {
return await db.query.storeInfo.findFirst({
where: eq(storeInfo.id, id),
with: {
owner: true,
},
});
const getStringArray = (value: unknown): string[] | null => {
if (!Array.isArray(value)) return null
return value.map((item) => String(item))
}
export async function getStoreSummaries(): Promise<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[]> {
return await db.query.productTags.findMany({
with: {
products: {
with: {
product: true,
},
},
// products: {
// with: {
// product: true,
// },
// },
},
});
}
@ -18,11 +18,11 @@ export async function getTagById(id: number): Promise<any | null> {
return await db.query.productTags.findFirst({
where: eq(productTags.id, id),
with: {
products: {
with: {
product: true,
},
},
// products: {
// with: {
// product: true,
// },
// },
},
});
}

View file

@ -1,44 +1,75 @@
import { db } from '../db/db_index';
import { users, userDetails, addresses } from '../db/schema';
import { eq, desc } from 'drizzle-orm';
import { db } from '../db/db_index'
import { notifCreds, unloggedUserTokens, userCreds, userDetails, users } from '../db/schema'
import { and, eq } from 'drizzle-orm'
export async function getCurrentUser(userId: number): Promise<any | null> {
return await db.query.users.findFirst({
where: eq(users.id, userId),
with: {
userDetails: true,
},
});
export async function getUserById(userId: number) {
const [user] = await db.select().from(users).where(eq(users.id, userId)).limit(1)
return user || null
}
export async function updateUser(userId: number, updates: any): Promise<any> {
const [user] = await db.update(users)
.set(updates)
export async function getUserDetailByUserId(userId: number) {
const [detail] = await db.select().from(userDetails).where(eq(userDetails.userId, userId)).limit(1)
return detail || null
}
export async function getUserWithCreds(userId: number) {
const result = await db
.select()
.from(users)
.leftJoin(userCreds, eq(users.id, userCreds.userId))
.where(eq(users.id, userId))
.returning();
return user;
.limit(1)
if (result.length === 0) return null
return {
user: result[0].users,
creds: result[0].user_creds,
}
}
export async function getUserAddresses(userId: number): Promise<any[]> {
return await db.query.addresses.findMany({
where: eq(addresses.userId, userId),
orderBy: desc(addresses.isDefault),
});
export async function getNotifCred(userId: number, token: string) {
return db.query.notifCreds.findFirst({
where: and(eq(notifCreds.userId, userId), eq(notifCreds.token, token)),
})
}
export async function createAddress(addressData: any): Promise<any> {
const [address] = await db.insert(addresses).values(addressData).returning();
return address;
export async function upsertNotifCred(userId: number, token: string): Promise<void> {
const existing = await getNotifCred(userId, token)
if (existing) {
await db.update(notifCreds)
.set({ lastVerified: new Date() })
.where(eq(notifCreds.id, existing.id))
return
}
export async function updateAddress(addressId: number, updates: any): Promise<any> {
const [address] = await db.update(addresses)
.set(updates)
.where(eq(addresses.id, addressId))
.returning();
return address;
await db.insert(notifCreds).values({
userId,
token,
lastVerified: new Date(),
})
}
export async function deleteAddress(addressId: number): Promise<void> {
await db.delete(addresses).where(eq(addresses.id, addressId));
export async function deleteUnloggedToken(token: string): Promise<void> {
await db.delete(unloggedUserTokens).where(eq(unloggedUserTokens.token, token))
}
export async function getUnloggedToken(token: string) {
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;
}
export interface UserAddress {
id: number;
userId: number;
name: string;
phone: string;
addressLine1: string;
addressLine2: string | null;
city: string;
state: string;
pincode: string;
isDefault: boolean;
latitude: number | null;
longitude: number | null;
googleMapsUrl: string | null;
adminLatitude: number | null;
adminLongitude: number | null;
zoneId: number | null;
createdAt: Date;
}
export interface UserAddressResponse {
success: boolean;
data: UserAddress | null;
}
export interface UserAddressesResponse {
success: boolean;
data: UserAddress[];
}
export interface UserAddressDeleteResponse {
success: boolean;
message: string;
}
export interface Product {
id: number;
name: string;
@ -80,3 +115,531 @@ export interface Payment {
amount: string;
createdAt: Date;
}
export interface UserBanner {
id: number;
name: string;
imageUrl: string;
description: string | null;
productIds: number[] | null;
redirectUrl: string | null;
serialNum: number | null;
isActive: boolean;
createdAt: Date;
lastUpdated: Date;
}
export interface UserBannersResponse {
banners: UserBanner[];
}
export interface UserCartProduct {
id: number;
name: string;
price: string;
productQuantity: number;
unit: string;
isOutOfStock: boolean;
images: string[];
}
export interface UserCartItem {
id: number;
productId: number;
quantity: number;
addedAt: Date;
product: UserCartProduct;
subtotal: number;
}
export interface UserCartResponse {
items: UserCartItem[];
totalItems: number;
totalAmount: number;
message?: string;
}
export interface UserComplaint {
id: number;
complaintBody: string;
response: string | null;
isResolved: boolean;
createdAt: Date;
orderId: number | null;
}
export interface UserComplaintsResponse {
complaints: UserComplaint[];
}
export interface UserRaiseComplaintResponse {
success: boolean;
message: string;
}
export interface UserTagSummary {
id: number;
tagName: string;
tagDescription: string | null;
imageUrl: string | null;
productIds: number[];
}
export interface UserStoreSampleProduct {
id: number;
name: string;
signedImageUrl: string | null;
}
export interface UserStoreSampleProductData {
id: number;
name: string;
images: string[] | null;
}
export interface UserStoreSummary {
id: number;
name: string;
description: string | null;
signedImageUrl: string | null;
productCount: number;
sampleProducts: UserStoreSampleProduct[];
}
export interface UserStoreSummaryData {
id: number;
name: string;
description: string | null;
imageUrl: string | null;
productCount: number;
sampleProducts: UserStoreSampleProductData[];
}
export interface UserStoresResponse {
stores: UserStoreSummary[];
}
export interface UserStoreProduct {
id: number;
name: string;
shortDescription: string | null;
price: string;
marketPrice: string | null;
incrementStep: number;
unit: string;
unitNotation: string;
images: string[];
isOutOfStock: boolean;
productQuantity: number;
}
export interface UserStoreProductData {
id: number;
name: string;
shortDescription: string | null;
price: string;
marketPrice: string | null;
incrementStep: number;
unit: string;
unitNotation: string;
images: string[] | null;
isOutOfStock: boolean;
productQuantity: number;
}
export interface UserStoreDetail {
store: {
id: number;
name: string;
description: string | null;
signedImageUrl: string | null;
};
products: UserStoreProduct[];
tags: UserTagSummary[];
}
export interface UserStoreDetailData {
store: {
id: number;
name: string;
description: string | null;
imageUrl: string | null;
};
products: UserStoreProductData[];
}
export interface UserProductStoreInfo {
id: number;
name: string;
description: string | null;
}
export interface UserProductDeliverySlot {
id: number;
deliveryTime: Date;
freezeTime: Date;
}
export interface UserProductSpecialDeal {
quantity: string;
price: string;
validTill: Date;
}
export interface UserProductDetailData {
id: number;
name: string;
shortDescription: string | null;
longDescription: string | null;
price: string;
marketPrice: string | null;
unitNotation: string;
images: string[] | null;
isOutOfStock: boolean;
store: UserProductStoreInfo | null;
incrementStep: number;
productQuantity: number;
isFlashAvailable: boolean;
flashPrice: string | null;
deliverySlots: UserProductDeliverySlot[];
specialDeals: UserProductSpecialDeal[];
}
export interface UserProductDetail extends UserProductDetailData {
images: string[];
}
export interface UserProductReview {
id: number;
reviewBody: string;
ratings: number;
imageUrls: string[] | null;
reviewTime: Date;
userName: string | null;
}
export interface UserProductReviewWithSignedUrls extends UserProductReview {
signedImageUrls: string[];
}
export interface UserProductReviewsResponse {
reviews: UserProductReviewWithSignedUrls[];
hasMore: boolean;
}
export interface UserCreateReviewResponse {
success: boolean;
review: UserProductReview;
}
export interface UserSlotProduct {
id: number;
name: string;
shortDescription: string | null;
productQuantity: number;
price: string;
marketPrice: string | null;
unit: string | null;
images: string[];
isOutOfStock: boolean;
storeId: number | null;
nextDeliveryDate: Date;
}
export interface UserSlotWithProducts {
id: number;
deliveryTime: Date;
freezeTime: Date;
isActive: boolean;
isCapacityFull: boolean;
products: UserSlotProduct[];
}
export interface UserSlotData {
slotId: number;
deliveryTime: Date;
freezeTime: Date;
products: UserSlotProduct[];
}
export interface UserSlotAvailability {
id: number;
name: string;
isOutOfStock: boolean;
isFlashAvailable: boolean;
}
export interface UserDeliverySlot {
id: number;
deliveryTime: Date;
freezeTime: Date;
isActive: boolean;
isFlash: boolean;
isCapacityFull: boolean;
deliverySequence: unknown;
groupIds: unknown;
}
export interface UserSlotsResponse {
slots: UserSlotWithProducts[];
count: number;
}
export interface UserSlotsListResponse {
slots: UserDeliverySlot[];
count: number;
}
export interface UserSlotsWithProductsResponse {
slots: UserSlotWithProducts[];
productAvailability: UserSlotAvailability[];
count: number;
}
export interface UserPaymentOrderResponse {
razorpayOrderId: string | number;
key: string | undefined;
}
export interface UserPaymentVerifyResponse {
success: boolean;
message: string;
}
export interface UserPaymentFailResponse {
success: boolean;
message: string;
}
export interface UserAuthProfile {
id: number;
name?: string | null;
email: string | null;
mobile: string | null;
createdAt: string;
profileImage: string | null;
bio?: string | null;
dateOfBirth?: string | null;
gender?: string | null;
occupation?: string | null;
}
export interface UserAuthResponse {
token: string;
user: UserAuthProfile;
}
export interface UserAuthResult {
success: boolean;
data: UserAuthResponse;
}
export interface UserOtpVerifyResponse {
success: boolean;
token: string;
user: {
id: number;
name: string | null;
email: string | null;
mobile: string | null;
createdAt: string;
profileImage: string | null;
};
}
export interface UserPasswordUpdateResponse {
success: boolean;
message: string;
}
export interface UserProfileResponse {
success: boolean;
data: {
id: number;
name: string | null;
email: string | null;
mobile: string | null;
};
}
export interface UserDeleteAccountResponse {
success: boolean;
message: string;
}
export interface UserCouponUsage {
id: number;
userId: number;
couponId: number;
orderId: number | null;
orderItemId: number | null;
usedAt: Date;
}
export interface UserCouponApplicableUser {
id: number;
couponId: number;
userId: number;
}
export interface UserCouponApplicableProduct {
id: number;
couponId: number;
productId: number;
}
export interface UserCoupon {
id: number;
couponCode: string;
isUserBased: boolean;
discountPercent: string | null;
flatDiscount: string | null;
minOrder: string | null;
productIds: unknown;
maxValue: string | null;
isApplyForAll: boolean;
validTill: Date | null;
maxLimitForUser: number | null;
isInvalidated: boolean;
exclusiveApply: boolean;
createdAt: Date;
}
export interface UserCouponWithRelations extends UserCoupon {
usages: UserCouponUsage[];
applicableUsers: UserCouponApplicableUser[];
applicableProducts: UserCouponApplicableProduct[];
}
export interface UserEligibleCouponsResponse {
success: boolean;
data: UserCouponWithRelations[];
}
export interface UserCouponDisplay {
id: number;
code: string;
discountType: 'percentage' | 'flat';
discountValue: number;
maxValue?: number;
minOrder?: number;
description: string;
validTill?: Date;
usageCount: number;
maxLimitForUser?: number;
isExpired: boolean;
isUsedUp: boolean;
}
export interface UserMyCouponsResponse {
success: boolean;
data: {
personal: UserCouponDisplay[];
general: UserCouponDisplay[];
};
}
export interface UserRedeemCouponResponse {
success: boolean;
coupon: UserCoupon;
}
export interface UserSelfDataResponse {
success: boolean;
data: {
user: {
id: number;
name: string | null;
email: string | null;
mobile: string | null;
profileImage?: string | null;
bio?: string | null;
dateOfBirth?: string | null;
gender?: string | null;
occupation?: string | null;
};
};
}
export interface UserProfileCompleteResponse {
isComplete: boolean;
}
export interface UserSavePushTokenResponse {
success: boolean;
}
export interface UserOrderItemSummary {
productName: string;
quantity: number;
price: number;
discountedPrice: number;
amount: number;
image: string | null;
}
export interface UserOrderSummary {
id: number;
orderId: string;
orderDate: string;
deliveryStatus: string;
deliveryDate?: string;
orderStatus: string;
cancelReason: string | null;
paymentMode: string;
totalAmount: number;
deliveryCharge: number;
paymentStatus: string;
refundStatus: string;
refundAmount: number | null;
userNotes: string | null;
items: UserOrderItemSummary[];
isFlashDelivery: boolean;
createdAt: string;
}
export interface UserOrdersResponse {
success: boolean;
data: UserOrderSummary[];
pagination: {
page: number;
pageSize: number;
totalCount: number;
totalPages: number;
};
}
export interface UserOrderDetail extends UserOrderSummary {
cancellationStatus: string;
couponCode: string | null;
couponDescription: string | null;
discountAmount: number | null;
orderAmount: number;
}
export interface UserCancelOrderResponse {
success: boolean;
message: string;
}
export interface UserUpdateNotesResponse {
success: boolean;
message: string;
}
export interface UserRecentProduct {
id: number;
name: string;
shortDescription: string | null;
price: string;
images: string[];
isOutOfStock: boolean;
unit: string;
incrementStep: number;
nextDeliveryDate: string | null;
}
export interface UserRecentProductsResponse {
success: boolean;
products: UserRecentProduct[];
}