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