enh
This commit is contained in:
parent
4414f9f64b
commit
fe05769343
28 changed files with 3189 additions and 1280 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
}),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
}),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
}),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
};
|
||||
}
|
||||
}),
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}),
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
// },
|
||||
// },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue