From a23d3bf5b8ea19ce2449386e0600ac7e6b94d9de Mon Sep 17 00:00:00 2001 From: shafi54 <108669266+shafi-aviz@users.noreply.github.com> Date: Sun, 22 Mar 2026 20:20:28 +0530 Subject: [PATCH] enh --- verifier/admin-apis/apis/address.ts | 32 - verifier/admin-apis/apis/admin-trpc-index.ts | 39 - verifier/admin-apis/apis/banner.ts | 176 --- verifier/admin-apis/apis/cancelled-orders.ts | 179 --- verifier/admin-apis/apis/complaint.ts | 80 -- verifier/admin-apis/apis/const.ts | 61 - verifier/admin-apis/apis/coupon.ts | 711 ------------ verifier/admin-apis/apis/order.ts | 1018 ----------------- verifier/admin-apis/apis/payments.ts | 146 --- .../apis/product-availability-schedules.ts | 154 --- verifier/admin-apis/apis/product.ts | 758 ------------ verifier/admin-apis/apis/slots.ts | 610 ---------- verifier/admin-apis/apis/staff-user.ts | 242 ---- verifier/admin-apis/apis/store.ts | 211 ---- verifier/admin-apis/apis/tag.ts | 214 ---- verifier/admin-apis/apis/user.ts | 489 -------- verifier/admin-apis/apis/vendor-snippets.ts | 531 --------- verifier/admin-apis/dataAccessors/demo.txt | 0 verifier/apis/address.ts | 32 - verifier/apis/admin-trpc-index.ts | 39 - verifier/apis/banner.ts | 176 --- verifier/apis/cancelled-orders.ts | 179 --- verifier/apis/complaint.ts | 80 -- verifier/apis/const.ts | 61 - verifier/apis/coupon.ts | 711 ------------ verifier/apis/order.ts | 1018 ----------------- verifier/apis/payments.ts | 146 --- .../apis/product-availability-schedules.ts | 154 --- verifier/apis/product.ts | 758 ------------ verifier/apis/slots.ts | 610 ---------- verifier/apis/staff-user.ts | 242 ---- verifier/apis/store.ts | 211 ---- verifier/apis/tag.ts | 214 ---- verifier/apis/user.ts | 489 -------- verifier/apis/vendor-snippets.ts | 531 --------- verifier/dataAccessors/demo.txt | 0 verifier/user-apis/apis/address.ts | 194 ---- verifier/user-apis/apis/auth.ts | 581 ---------- verifier/user-apis/apis/banners.ts | 30 - verifier/user-apis/apis/cart.ts | 244 ---- verifier/user-apis/apis/complaint.ts | 70 -- verifier/user-apis/apis/coupon.ts | 296 ----- verifier/user-apis/apis/file-upload.ts | 55 - verifier/user-apis/apis/order.ts | 979 ---------------- verifier/user-apis/apis/product.ts | 266 ----- verifier/user-apis/apis/slots.ts | 92 -- verifier/user-apis/apis/stores.ts | 162 --- verifier/user-apis/apis/tags.ts | 28 - verifier/user-apis/apis/user-trpc-index.ts | 32 - verifier/user-apis/apis/user.ts | 164 --- verifier/user-apis/dataAccessors/demo.txt | 0 51 files changed, 14495 deletions(-) delete mode 100644 verifier/admin-apis/apis/address.ts delete mode 100644 verifier/admin-apis/apis/admin-trpc-index.ts delete mode 100644 verifier/admin-apis/apis/banner.ts delete mode 100644 verifier/admin-apis/apis/cancelled-orders.ts delete mode 100644 verifier/admin-apis/apis/complaint.ts delete mode 100644 verifier/admin-apis/apis/const.ts delete mode 100644 verifier/admin-apis/apis/coupon.ts delete mode 100644 verifier/admin-apis/apis/order.ts delete mode 100644 verifier/admin-apis/apis/payments.ts delete mode 100644 verifier/admin-apis/apis/product-availability-schedules.ts delete mode 100644 verifier/admin-apis/apis/product.ts delete mode 100644 verifier/admin-apis/apis/slots.ts delete mode 100644 verifier/admin-apis/apis/staff-user.ts delete mode 100644 verifier/admin-apis/apis/store.ts delete mode 100644 verifier/admin-apis/apis/tag.ts delete mode 100644 verifier/admin-apis/apis/user.ts delete mode 100644 verifier/admin-apis/apis/vendor-snippets.ts delete mode 100644 verifier/admin-apis/dataAccessors/demo.txt delete mode 100644 verifier/apis/address.ts delete mode 100644 verifier/apis/admin-trpc-index.ts delete mode 100644 verifier/apis/banner.ts delete mode 100644 verifier/apis/cancelled-orders.ts delete mode 100644 verifier/apis/complaint.ts delete mode 100644 verifier/apis/const.ts delete mode 100644 verifier/apis/coupon.ts delete mode 100644 verifier/apis/order.ts delete mode 100644 verifier/apis/payments.ts delete mode 100644 verifier/apis/product-availability-schedules.ts delete mode 100644 verifier/apis/product.ts delete mode 100644 verifier/apis/slots.ts delete mode 100644 verifier/apis/staff-user.ts delete mode 100644 verifier/apis/store.ts delete mode 100644 verifier/apis/tag.ts delete mode 100644 verifier/apis/user.ts delete mode 100644 verifier/apis/vendor-snippets.ts delete mode 100644 verifier/dataAccessors/demo.txt delete mode 100644 verifier/user-apis/apis/address.ts delete mode 100644 verifier/user-apis/apis/auth.ts delete mode 100644 verifier/user-apis/apis/banners.ts delete mode 100644 verifier/user-apis/apis/cart.ts delete mode 100644 verifier/user-apis/apis/complaint.ts delete mode 100644 verifier/user-apis/apis/coupon.ts delete mode 100644 verifier/user-apis/apis/file-upload.ts delete mode 100644 verifier/user-apis/apis/order.ts delete mode 100644 verifier/user-apis/apis/product.ts delete mode 100644 verifier/user-apis/apis/slots.ts delete mode 100644 verifier/user-apis/apis/stores.ts delete mode 100644 verifier/user-apis/apis/tags.ts delete mode 100644 verifier/user-apis/apis/user-trpc-index.ts delete mode 100644 verifier/user-apis/apis/user.ts delete mode 100644 verifier/user-apis/dataAccessors/demo.txt diff --git a/verifier/admin-apis/apis/address.ts b/verifier/admin-apis/apis/address.ts deleted file mode 100644 index 019b895..0000000 --- a/verifier/admin-apis/apis/address.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { z } from 'zod'; -import { addressZones, addressAreas } from '@/src/db/schema' -import { eq, desc } from 'drizzle-orm'; -import { db } from '@/src/db/db_index' -import { router,protectedProcedure } from '@/src/trpc/trpc-index' - -const addressRouter = router({ - getZones: protectedProcedure.query(async () => { - const zones = await db.select().from(addressZones).orderBy(desc(addressZones.addedAt)); - return zones - }), - - getAreas: protectedProcedure.query(async () => { - const areas = await db.select().from(addressAreas).orderBy(desc(addressAreas.createdAt)); - return areas - }), - - createZone: protectedProcedure.input(z.object({ zoneName: z.string().min(1) })).mutation(async ({ input }) => { - - const zone = await db.insert(addressZones).values({ zoneName: input.zoneName }).returning(); - return {zone: zone}; - }), - - createArea: protectedProcedure.input(z.object({ placeName: z.string().min(1), zoneId: z.number().nullable() })).mutation(async ({ input }) => { - const area = await db.insert(addressAreas).values({ placeName: input.placeName, zoneId: input.zoneId }).returning(); - return {area}; - }), - - // TODO: Add update and delete mutations if needed -}); - -export default addressRouter; \ No newline at end of file diff --git a/verifier/admin-apis/apis/admin-trpc-index.ts b/verifier/admin-apis/apis/admin-trpc-index.ts deleted file mode 100644 index 4e23b84..0000000 --- a/verifier/admin-apis/apis/admin-trpc-index.ts +++ /dev/null @@ -1,39 +0,0 @@ -// import { router } from '@/src/trpc/trpc-index'; -import { router } from '@/src/trpc/trpc-index' -import { complaintRouter } from '@/src/trpc/apis/admin-apis/apis/complaint' -import { couponRouter } from '@/src/trpc/apis/admin-apis/apis/coupon' -import { cancelledOrdersRouter } from '@/src/trpc/apis/admin-apis/apis/cancelled-orders' -import { orderRouter } from '@/src/trpc/apis/admin-apis/apis/order' -import { vendorSnippetsRouter } from '@/src/trpc/apis/admin-apis/apis/vendor-snippets' -import { slotsRouter } from '@/src/trpc/apis/admin-apis/apis/slots' -import { productRouter } from '@/src/trpc/apis/admin-apis/apis/product' -import { staffUserRouter } from '@/src/trpc/apis/admin-apis/apis/staff-user' -import { storeRouter } from '@/src/trpc/apis/admin-apis/apis/store' -import { adminPaymentsRouter } from '@/src/trpc/apis/admin-apis/apis/payments' -import addressRouter from '@/src/trpc/apis/admin-apis/apis/address' -import { bannerRouter } from '@/src/trpc/apis/admin-apis/apis/banner' -import { userRouter } from '@/src/trpc/apis/admin-apis/apis/user' -import { constRouter } from '@/src/trpc/apis/admin-apis/apis/const' -import { productAvailabilitySchedulesRouter } from '@/src/trpc/apis/admin-apis/apis/product-availability-schedules' -import { tagRouter } from '@/src/trpc/apis/admin-apis/apis/tag' - -export const adminRouter = router({ - complaint: complaintRouter, - coupon: couponRouter, - cancelledOrders: cancelledOrdersRouter, - order: orderRouter, - vendorSnippets: vendorSnippetsRouter, - slots: slotsRouter, - product: productRouter, - staffUser: staffUserRouter, - store: storeRouter, - payments: adminPaymentsRouter, - address: addressRouter, - banner: bannerRouter, - user: userRouter, - const: constRouter, - productAvailabilitySchedules: productAvailabilitySchedulesRouter, - tag: tagRouter, -}); - -export type AdminRouter = typeof adminRouter; diff --git a/verifier/admin-apis/apis/banner.ts b/verifier/admin-apis/apis/banner.ts deleted file mode 100644 index 5794143..0000000 --- a/verifier/admin-apis/apis/banner.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { z } from 'zod'; -import { db } from '@/src/db/db_index' -import { homeBanners } from '@/src/db/schema' -import { eq, and, desc, sql } from 'drizzle-orm'; -import { protectedProcedure, router } from '@/src/trpc/trpc-index' -import { extractKeyFromPresignedUrl, scaffoldAssetUrl } from '@/src/lib/s3-client' -import { ApiError } from '@/src/lib/api-error'; -import { scheduleStoreInitialization } from '@/src/stores/store-initializer' - - -export const bannerRouter = router({ - // Get all banners - getBanners: protectedProcedure - .query(async () => { - try { - - const banners = await db.query.homeBanners.findMany({ - orderBy: desc(homeBanners.createdAt), // Order by creation date instead - // Removed product relationship since we now use productIds array - }); - - // Convert S3 keys to signed URLs for client - const bannersWithSignedUrls = await Promise.all( - banners.map(async (banner) => { - try { - return { - ...banner, - imageUrl: banner.imageUrl ? scaffoldAssetUrl(banner.imageUrl) : banner.imageUrl, - // Ensure productIds is always an array - productIds: banner.productIds || [], - }; - } catch (error) { - console.error(`Failed to generate signed URL for banner ${banner.id}:`, error); - return { - ...banner, - imageUrl: banner.imageUrl, // Keep original on error - // Ensure productIds is always an array - productIds: banner.productIds || [], - }; - } - }) - ); - - return { - banners: bannersWithSignedUrls, - }; - } - catch(e:any) { - console.log(e) - - throw new ApiError(e.message); - } - }), - - // Get single banner by ID - getBanner: protectedProcedure - .input(z.object({ id: z.number() })) - .query(async ({ input }) => { - const banner = await db.query.homeBanners.findFirst({ - where: eq(homeBanners.id, input.id), - // Removed product relationship since we now use productIds array - }); - - if (banner) { - try { - // Convert S3 key to signed URL for client - if (banner.imageUrl) { - banner.imageUrl = scaffoldAssetUrl(banner.imageUrl); - } - } catch (error) { - console.error(`Failed to generate signed URL for banner ${banner.id}:`, error); - // Keep original imageUrl on error - } - - // Ensure productIds is always an array (handle migration compatibility) - if (!banner.productIds) { - banner.productIds = []; - } - } - - return banner; - }), - - // Create new banner - createBanner: protectedProcedure - .input(z.object({ - name: z.string().min(1), - imageUrl: z.string(), - description: z.string().optional(), - productIds: z.array(z.number()).optional(), - redirectUrl: z.string().url().optional(), - // serialNum removed completely - })) - .mutation(async ({ input }) => { - try { - const imageUrl = extractKeyFromPresignedUrl(input.imageUrl) - // const imageUrl = input.imageUrl - const [banner] = await db.insert(homeBanners).values({ - name: input.name, - imageUrl: imageUrl, - description: input.description, - productIds: input.productIds || [], - redirectUrl: input.redirectUrl, - serialNum: 999, // Default value, not used - isActive: false, // Default to inactive - }).returning(); - - // Reinitialize stores to reflect changes - scheduleStoreInitialization() - - return banner; - } catch (error) { - console.error('Error creating banner:', error); - throw error; // Re-throw to maintain tRPC error handling - } - }), - - // Update banner - updateBanner: protectedProcedure - .input(z.object({ - id: z.number(), - name: z.string().min(1).optional(), - imageUrl: z.string().url().optional(), - description: z.string().optional(), - productIds: z.array(z.number()).optional(), - redirectUrl: z.string().url().optional(), - serialNum: z.number().nullable().optional(), - isActive: z.boolean().optional(), - })) - .mutation(async ({ input }) => { - try { - - const { id, ...updateData } = input; - const incomingProductIds = input.productIds; - // Extract S3 key from presigned URL if imageUrl is provided - const processedData = { - ...updateData, - ...(updateData.imageUrl && { - imageUrl: extractKeyFromPresignedUrl(updateData.imageUrl) - }), - }; - - // Handle serialNum null case - const finalData: any = { ...processedData }; - if ('serialNum' in finalData && finalData.serialNum === null) { - // Set to null explicitly - finalData.serialNum = null; - } - - const [banner] = await db.update(homeBanners) - .set({ ...finalData, lastUpdated: new Date(), }) - .where(eq(homeBanners.id, id)) - .returning(); - - // Reinitialize stores to reflect changes - scheduleStoreInitialization() - - return banner; - } catch (error) { - console.error('Error updating banner:', error); - throw error; - } - }), - - // Delete banner - deleteBanner: protectedProcedure - .input(z.object({ id: z.number() })) - .mutation(async ({ input }) => { - await db.delete(homeBanners).where(eq(homeBanners.id, input.id)); - - // Reinitialize stores to reflect changes - scheduleStoreInitialization() - - return { success: true }; - }), -}); diff --git a/verifier/admin-apis/apis/cancelled-orders.ts b/verifier/admin-apis/apis/cancelled-orders.ts deleted file mode 100644 index 14c0b26..0000000 --- a/verifier/admin-apis/apis/cancelled-orders.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { router, protectedProcedure } from '@/src/trpc/trpc-index' -import { z } from 'zod'; -import { db } from '@/src/db/db_index' -import { orders, orderStatus, users, addresses, orderItems, productInfo, units, refunds } from '@/src/db/schema' -import { eq, desc } from 'drizzle-orm'; - -const updateCancellationReviewSchema = z.object({ - orderId: z.number(), - cancellationReviewed: z.boolean(), - adminNotes: z.string().optional(), -}); - -const updateRefundSchema = z.object({ - orderId: z.number(), - isRefundDone: z.boolean(), -}); - -export const cancelledOrdersRouter = router({ - getAll: protectedProcedure - .query(async () => { - // First get cancelled order statuses with order details - const cancelledOrderStatuses = await db.query.orderStatus.findMany({ - where: eq(orderStatus.isCancelled, true), - with: { - order: { - with: { - user: true, - address: true, - orderItems: { - with: { - product: { - with: { - unit: true, - }, - }, - }, - }, - refunds: true, - }, - }, - }, - orderBy: [desc(orderStatus.orderTime)], - }); - - const filteredStatuses = cancelledOrderStatuses.filter(status => { - return status.order.isCod || status.paymentStatus === 'success'; - }); - - return filteredStatuses.map(status => { - const refund = status.order.refunds[0]; - return { - id: status.order.id, - readableId: status.order.id, - customerName: `${status.order.user.name}`, - address: `${status.order.address.addressLine1}, ${status.order.address.city}`, - totalAmount: status.order.totalAmount, - cancellationReviewed: status.cancellationReviewed || false, - isRefundDone: refund?.refundStatus === 'processed' || false, - adminNotes: status.order.adminNotes, - cancelReason: status.cancelReason, - paymentMode: status.order.isCod ? 'COD' : 'Online', - paymentStatus: status.paymentStatus || 'pending', - items: status.order.orderItems.map(item => ({ - name: item.product.name, - quantity: item.quantity, - price: item.price, - unit: item.product.unit?.shortNotation, - amount: parseFloat(item.price.toString()) * parseFloat(item.quantity || '0'), - })), - createdAt: status.order.createdAt, - }; - }); - }), - - updateReview: protectedProcedure - .input(updateCancellationReviewSchema) - .mutation(async ({ input }) => { - const { orderId, cancellationReviewed, adminNotes } = input; - - const result = await db.update(orderStatus) - .set({ - cancellationReviewed, - cancellationAdminNotes: adminNotes || null, - cancellationReviewedAt: new Date(), - }) - .where(eq(orderStatus.orderId, orderId)) - .returning(); - - if (result.length === 0) { - throw new Error("Cancellation record not found"); - } - - return result[0]; - }), - - getById: protectedProcedure - .input(z.object({ id: z.number() })) - .query(async ({ input }) => { - const { id } = input; - - // Get cancelled order with full details - const cancelledOrderStatus = await db.query.orderStatus.findFirst({ - where: eq(orderStatus.id, id), - with: { - order: { - with: { - user: true, - address: true, - orderItems: { - with: { - product: { - with: { - unit: true, - }, - }, - }, - }, - }, - }, - }, - }); - - if (!cancelledOrderStatus || !cancelledOrderStatus.isCancelled) { - throw new Error("Cancelled order not found"); - } - - // Get refund details separately - const refund = await db.query.refunds.findFirst({ - where: eq(refunds.orderId, cancelledOrderStatus.orderId), - }); - - const order = cancelledOrderStatus.order; - - // Format the response similar to the getAll method - const formattedOrder = { - id: order.id, - readableId: order.id, - customerName: order.user.name, - address: `${order.address.addressLine1}${order.address.addressLine2 ? ', ' + order.address.addressLine2 : ''}, ${order.address.city}, ${order.address.state} ${order.address.pincode}`, - totalAmount: order.totalAmount, - cancellationReviewed: cancelledOrderStatus.cancellationReviewed || false, - isRefundDone: refund?.refundStatus === 'processed' || false, - adminNotes: cancelledOrderStatus.cancellationAdminNotes || null, - cancelReason: cancelledOrderStatus.cancelReason || null, - items: order.orderItems.map((item: any) => ({ - name: item.product.name, - quantity: item.quantity, - price: parseFloat(item.price.toString()), - unit: item.product.unit?.shortNotation || 'unit', - amount: parseFloat(item.price.toString()) * parseFloat(item.quantity), - image: item.product.images?.[0] || null, - })), - createdAt: order.createdAt.toISOString(), - }; - - return { order: formattedOrder }; - }), - - updateRefund: protectedProcedure - .input(updateRefundSchema) - .mutation(async ({ input }) => { - const { orderId, isRefundDone } = input; - - const refundStatus = isRefundDone ? 'processed' : 'none'; - const result = await db.update(refunds) - .set({ - refundStatus, - refundProcessedAt: isRefundDone ? new Date() : null, - }) - .where(eq(refunds.orderId, orderId)) - .returning(); - - if (result.length === 0) { - throw new Error("Cancellation record not found"); - } - - return result[0]; - }), -}); \ No newline at end of file diff --git a/verifier/admin-apis/apis/complaint.ts b/verifier/admin-apis/apis/complaint.ts deleted file mode 100644 index b1791a6..0000000 --- a/verifier/admin-apis/apis/complaint.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { router, protectedProcedure } from '@/src/trpc/trpc-index' -import { z } from 'zod'; -import { db } from '@/src/db/db_index' -import { complaints, users } from '@/src/db/schema' -import { eq, desc, lt, and } from 'drizzle-orm'; -import { scaffoldAssetUrl } from '@/src/lib/s3-client' - -export const complaintRouter = router({ - getAll: protectedProcedure - .input(z.object({ - cursor: z.number().optional(), - limit: z.number().default(20), - })) - .query(async ({ input }) => { - const { cursor, limit } = input; - - let whereCondition = cursor - ? lt(complaints.id, cursor) - : undefined; - - const complaintsData = await db - .select({ - id: complaints.id, - complaintBody: complaints.complaintBody, - userId: complaints.userId, - orderId: complaints.orderId, - isResolved: complaints.isResolved, - createdAt: complaints.createdAt, - userName: users.name, - userMobile: users.mobile, - images: complaints.images, - }) - .from(complaints) - .leftJoin(users, eq(complaints.userId, users.id)) - .where(whereCondition) - .orderBy(desc(complaints.id)) - .limit(limit + 1); - - const hasMore = complaintsData.length > limit; - const complaintsToReturn = hasMore ? complaintsData.slice(0, limit) : complaintsData; - - const complaintsWithSignedImages = await Promise.all( - complaintsToReturn.map(async (c) => { - const signedImages = c.images - ? scaffoldAssetUrl(c.images as string[]) - : []; - - return { - id: c.id, - text: c.complaintBody, - userId: c.userId, - userName: c.userName, - userMobile: c.userMobile, - orderId: c.orderId, - status: c.isResolved ? 'resolved' : 'pending', - createdAt: c.createdAt, - images: signedImages, - }; - }) - ); - - return { - complaints: complaintsWithSignedImages, - nextCursor: hasMore - ? complaintsToReturn[complaintsToReturn.length - 1].id - : undefined, - }; - }), - - resolve: protectedProcedure - .input(z.object({ id: z.string(), response: z.string().optional() })) - .mutation(async ({ input }) => { - await db - .update(complaints) - .set({ isResolved: true, response: input.response }) - .where(eq(complaints.id, parseInt(input.id))); - - return { message: 'Complaint resolved successfully' }; - }), -}); \ No newline at end of file diff --git a/verifier/admin-apis/apis/const.ts b/verifier/admin-apis/apis/const.ts deleted file mode 100644 index a426087..0000000 --- a/verifier/admin-apis/apis/const.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { router, protectedProcedure } from '@/src/trpc/trpc-index' -import { z } from 'zod'; -import { db } from '@/src/db/db_index' -import { keyValStore } from '@/src/db/schema' -import { computeConstants } from '@/src/lib/const-store' -import { CONST_KEYS } from '@/src/lib/const-keys' - -export const constRouter = router({ - getConstants: protectedProcedure - .query(async () => { - - const constants = await db.select().from(keyValStore); - - const resp = constants.map(c => ({ - key: c.key, - value: c.value, - })); - - return resp; - }), - - updateConstants: protectedProcedure - .input(z.object({ - constants: z.array(z.object({ - key: z.string(), - value: z.any(), - })), - })) - .mutation(async ({ input }) => { - const { constants } = input; - - const validKeys = Object.values(CONST_KEYS) as string[]; - const invalidKeys = constants - .filter(c => !validKeys.includes(c.key)) - .map(c => c.key); - - if (invalidKeys.length > 0) { - throw new Error(`Invalid constant keys: ${invalidKeys.join(', ')}`); - } - - await db.transaction(async (tx) => { - for (const { key, value } of constants) { - await tx.insert(keyValStore) - .values({ key, value }) - .onConflictDoUpdate({ - target: keyValStore.key, - set: { value }, - }); - } - }); - - // Refresh all constants in Redis after database update - await computeConstants(); - - return { - success: true, - updatedCount: constants.length, - keys: constants.map(c => c.key), - }; - }), -}); \ No newline at end of file diff --git a/verifier/admin-apis/apis/coupon.ts b/verifier/admin-apis/apis/coupon.ts deleted file mode 100644 index 4eb3017..0000000 --- a/verifier/admin-apis/apis/coupon.ts +++ /dev/null @@ -1,711 +0,0 @@ -import { router, protectedProcedure } from '@/src/trpc/trpc-index' -import { z } from 'zod'; -import { db } from '@/src/db/db_index' -import { coupons, users, staffUsers, orders, couponApplicableUsers, couponApplicableProducts, orderStatus, reservedCoupons } from '@/src/db/schema' -import { eq, and, like, or, inArray, lt } from 'drizzle-orm'; -import dayjs from 'dayjs'; - -const createCouponBodySchema = z.object({ - couponCode: z.string().optional(), - isUserBased: z.boolean().optional(), - discountPercent: z.number().optional(), - flatDiscount: z.number().optional(), - minOrder: z.number().optional(), - targetUser: z.number().optional(), - productIds: z.array(z.number()).optional().nullable(), - applicableUsers: z.array(z.number()).optional(), - applicableProducts: z.array(z.number()).optional(), - maxValue: z.number().optional(), - isApplyForAll: z.boolean().optional(), - validTill: z.string().optional(), - maxLimitForUser: z.number().optional(), - exclusiveApply: z.boolean().optional(), -}); - -const validateCouponBodySchema = z.object({ - code: z.string(), - userId: z.number(), - orderAmount: z.number(), -}); - -export const couponRouter = router({ - create: protectedProcedure - .input(createCouponBodySchema) - .mutation(async ({ input, ctx }) => { - const { couponCode, isUserBased, discountPercent, flatDiscount, minOrder, productIds, applicableUsers, applicableProducts, maxValue, isApplyForAll, validTill, maxLimitForUser, exclusiveApply } = input; - - // Validation: ensure at least one discount type is provided - if ((!discountPercent && !flatDiscount) || (discountPercent && flatDiscount)) { - throw new Error("Either discountPercent or flatDiscount must be provided (but not both)"); - } - - // If user-based, applicableUsers is required (unless it's apply for all) - if (isUserBased && (!applicableUsers || applicableUsers.length === 0) && !isApplyForAll) { - throw new Error("applicableUsers is required for user-based coupons (or set isApplyForAll to true)"); - } - - // Cannot be both user-based and apply for all - if (isUserBased && isApplyForAll) { - throw new Error("Cannot be both user-based and apply for all users"); - } - - // If applicableUsers is provided, verify users exist - if (applicableUsers && applicableUsers.length > 0) { - const existingUsers = await db.query.users.findMany({ - where: inArray(users.id, applicableUsers), - columns: { id: true }, - }); - if (existingUsers.length !== applicableUsers.length) { - throw new Error("Some applicable users not found"); - } - } - - // Get staff user ID from auth middleware - const staffUserId = ctx.staffUser?.id; - if (!staffUserId) { - throw new Error("Unauthorized"); - } - - // Generate coupon code if not provided - let finalCouponCode = couponCode; - if (!finalCouponCode) { - // Generate a unique coupon code - const timestamp = Date.now().toString().slice(-6); - const random = Math.random().toString(36).substring(2, 8).toUpperCase(); - finalCouponCode = `MF${timestamp}${random}`; - } - - // Check if coupon code already exists - const existingCoupon = await db.query.coupons.findFirst({ - where: eq(coupons.couponCode, finalCouponCode), - }); - - if (existingCoupon) { - throw new Error("Coupon code already exists"); - } - - const result = await db.insert(coupons).values({ - couponCode: finalCouponCode, - isUserBased: isUserBased || false, - discountPercent: discountPercent?.toString(), - flatDiscount: flatDiscount?.toString(), - minOrder: minOrder?.toString(), - productIds: productIds || null, - createdBy: staffUserId, - maxValue: maxValue?.toString(), - isApplyForAll: isApplyForAll || false, - validTill: validTill ? dayjs(validTill).toDate() : undefined, - maxLimitForUser: maxLimitForUser, - exclusiveApply: exclusiveApply || false, - }).returning(); - - const coupon = result[0]; - - // Insert applicable users - if (applicableUsers && applicableUsers.length > 0) { - await db.insert(couponApplicableUsers).values( - applicableUsers.map(userId => ({ - couponId: coupon.id, - userId, - })) - ); - } - - // Insert applicable products - if (applicableProducts && applicableProducts.length > 0) { - await db.insert(couponApplicableProducts).values( - applicableProducts.map(productId => ({ - couponId: coupon.id, - productId, - })) - ); - } - - return coupon; - }), - - getAll: protectedProcedure - .input(z.object({ - cursor: z.number().optional(), - limit: z.number().default(50), - search: z.string().optional(), - })) - .query(async ({ input }) => { - const { cursor, limit, search } = input; - - let whereCondition = undefined; - const conditions = []; - - if (cursor) { - conditions.push(lt(coupons.id, cursor)); - } - - if (search && search.trim()) { - conditions.push(like(coupons.couponCode, `%${search}%`)); - } - - if (conditions.length > 0) { - whereCondition = and(...conditions); - } - - const result = await db.query.coupons.findMany({ - where: whereCondition, - with: { - creator: true, - applicableUsers: { - with: { - user: true, - }, - }, - applicableProducts: { - with: { - product: true, - }, - }, - }, - orderBy: (coupons, { desc }) => [desc(coupons.createdAt)], - limit: limit + 1, - }); - - const hasMore = result.length > limit; - const couponsList = hasMore ? result.slice(0, limit) : result; - const nextCursor = hasMore ? result[result.length - 1].id : undefined; - - return { coupons: couponsList, nextCursor }; - }), - - getById: protectedProcedure - .input(z.object({ id: z.number() })) - .query(async ({ input }) => { - const couponId = input.id; - - const result = await db.query.coupons.findFirst({ - where: eq(coupons.id, couponId), - with: { - creator: true, - applicableUsers: { - with: { - user: true, - }, - }, - applicableProducts: { - with: { - product: true, - }, - }, - }, - }); - - if (!result) { - throw new Error("Coupon not found"); - } - - return { - ...result, - productIds: (result.productIds as number[]) || undefined, - applicableUsers: result.applicableUsers.map(au => au.user), - applicableProducts: result.applicableProducts.map(ap => ap.product), - }; - }), - - update: protectedProcedure - .input(z.object({ - id: z.number(), - updates: createCouponBodySchema.extend({ - isInvalidated: z.boolean().optional(), - }), - })) - .mutation(async ({ input }) => { - const { id, updates } = input; - - // Validation: ensure discount types are valid - if (updates.discountPercent !== undefined && updates.flatDiscount !== undefined) { - if (updates.discountPercent && updates.flatDiscount) { - throw new Error("Cannot have both discountPercent and flatDiscount"); - } - } - - // If updating to user-based, applicableUsers is required - if (updates.isUserBased && (!updates.applicableUsers || updates.applicableUsers.length === 0)) { - const existingCount = await db.$count(couponApplicableUsers, eq(couponApplicableUsers.couponId, id)); - if (existingCount === 0) { - throw new Error("applicableUsers is required for user-based coupons"); - } - } - - // If applicableUsers is provided, verify users exist - if (updates.applicableUsers && updates.applicableUsers.length > 0) { - const existingUsers = await db.query.users.findMany({ - where: inArray(users.id, updates.applicableUsers), - columns: { id: true }, - }); - if (existingUsers.length !== updates.applicableUsers.length) { - throw new Error("Some applicable users not found"); - } - } - - const updateData: any = { ...updates }; - delete updateData.applicableUsers; // Remove since we use couponApplicableUsers table - if (updates.discountPercent !== undefined) { - updateData.discountPercent = updates.discountPercent?.toString(); - } - if (updates.flatDiscount !== undefined) { - updateData.flatDiscount = updates.flatDiscount?.toString(); - } - if (updates.minOrder !== undefined) { - updateData.minOrder = updates.minOrder?.toString(); - } - if (updates.maxValue !== undefined) { - updateData.maxValue = updates.maxValue?.toString(); - } - if (updates.validTill !== undefined) { - updateData.validTill = updates.validTill ? dayjs(updates.validTill).toDate() : null; - } - - const result = await db.update(coupons) - .set(updateData) - .where(eq(coupons.id, id)) - .returning(); - - if (result.length === 0) { - throw new Error("Coupon not found"); - } - - console.log('updated coupon successfully') - - // Update applicable users: delete existing and insert new - if (updates.applicableUsers !== undefined) { - await db.delete(couponApplicableUsers).where(eq(couponApplicableUsers.couponId, id)); - if (updates.applicableUsers.length > 0) { - await db.insert(couponApplicableUsers).values( - updates.applicableUsers.map(userId => ({ - couponId: id, - userId, - })) - ); - } - } - - // Update applicable products: delete existing and insert new - if (updates.applicableProducts !== undefined) { - await db.delete(couponApplicableProducts).where(eq(couponApplicableProducts.couponId, id)); - if (updates.applicableProducts.length > 0) { - await db.insert(couponApplicableProducts).values( - updates.applicableProducts.map(productId => ({ - couponId: id, - productId, - })) - ); - } - } - - return result[0]; - }), - - delete: protectedProcedure - .input(z.object({ id: z.number() })) - .mutation(async ({ input }) => { - const { id } = input; - - const result = await db.update(coupons) - .set({ isInvalidated: true }) - .where(eq(coupons.id, id)) - .returning(); - - if (result.length === 0) { - throw new Error("Coupon not found"); - } - - return { message: "Coupon invalidated successfully" }; - }), - - validate: protectedProcedure - .input(validateCouponBodySchema) - .query(async ({ input }) => { - const { code, userId, orderAmount } = input; - - if (!code || typeof code !== 'string') { - return { valid: false, message: "Invalid coupon code" }; - } - - const coupon = await db.query.coupons.findFirst({ - where: and( - eq(coupons.couponCode, code.toUpperCase()), - eq(coupons.isInvalidated, false) - ), - }); - - if (!coupon) { - return { valid: false, message: "Coupon not found or invalidated" }; - } - - // Check expiry date - if (coupon.validTill && new Date(coupon.validTill) < new Date()) { - return { valid: false, message: "Coupon has expired" }; - } - - // Check if coupon applies to all users or specific user - if (!coupon.isApplyForAll && !coupon.isUserBased) { - return { valid: false, message: "Coupon is not available for use" }; - } - - // Check minimum order amount - const minOrderValue = coupon.minOrder ? parseFloat(coupon.minOrder) : 0; - if (minOrderValue > 0 && orderAmount < minOrderValue) { - return { valid: false, message: `Minimum order amount is ${minOrderValue}` }; - } - - // Calculate discount - let discountAmount = 0; - if (coupon.discountPercent) { - const percent = parseFloat(coupon.discountPercent); - discountAmount = (orderAmount * percent) / 100; - } else if (coupon.flatDiscount) { - discountAmount = parseFloat(coupon.flatDiscount); - } - - // Apply max value limit - const maxValueLimit = coupon.maxValue ? parseFloat(coupon.maxValue) : 0; - if (maxValueLimit > 0 && discountAmount > maxValueLimit) { - discountAmount = maxValueLimit; - } - - return { - valid: true, - discountAmount, - coupon: { - id: coupon.id, - discountPercent: coupon.discountPercent, - flatDiscount: coupon.flatDiscount, - maxValue: coupon.maxValue, - } - }; - }), - - generateCancellationCoupon: protectedProcedure - .input( - z.object({ - orderId: z.number(), - }) - ) - .mutation(async ({ input, ctx }) => { - const { orderId } = input; - - // Get staff user ID from auth middleware - const staffUserId = ctx.staffUser?.id; - if (!staffUserId) { - throw new Error("Unauthorized"); - } - - // Find the order with user and order status information - const order = await db.query.orders.findFirst({ - where: eq(orders.id, orderId), - with: { - user: true, - orderStatus: true, - }, - }); - - if (!order) { - throw new Error("Order not found"); - } - - // Check if order is cancelled (check if any status entry has isCancelled: true) - // const isOrderCancelled = order.orderStatus?.some(status => status.isCancelled) || false; - // if (!isOrderCancelled) { - // throw new Error("Order is not cancelled"); - // } - - // // Check if payment method is COD - // if (order.isCod) { - // throw new Error("Can't generate refund coupon for CoD Order"); - // } - - // Verify user exists - if (!order.user) { - throw new Error("User not found for this order"); - } - - // Generate coupon code: first 3 letters of user name or mobile + orderId - const userNamePrefix = (order.user.name || order.user.mobile || 'USR').substring(0, 3).toUpperCase(); - const couponCode = `${userNamePrefix}${orderId}`; - - // Check if coupon code already exists - const existingCoupon = await db.query.coupons.findFirst({ - where: eq(coupons.couponCode, couponCode), - }); - - if (existingCoupon) { - throw new Error("Coupon code already exists"); - } - - // Get order total amount - const orderAmount = parseFloat(order.totalAmount); - - // Calculate expiry date (30 days from now) - const expiryDate = new Date(); - expiryDate.setDate(expiryDate.getDate() + 30); - - // Create the coupon and update order status in a transaction - const coupon = await db.transaction(async (tx) => { - // Create the coupon - const result = await tx.insert(coupons).values({ - couponCode, - isUserBased: true, - flatDiscount: orderAmount.toString(), - minOrder: orderAmount.toString(), - maxValue: orderAmount.toString(), - validTill: expiryDate, - maxLimitForUser: 1, - createdBy: staffUserId, - isApplyForAll: false, - }).returning(); - - const coupon = result[0]; - - // Insert applicable users - await tx.insert(couponApplicableUsers).values({ - couponId: coupon.id, - userId: order.userId, - }); - - // Update order_status with refund coupon ID - await tx.update(orderStatus) - .set({ refundCouponId: coupon.id }) - .where(eq(orderStatus.orderId, orderId)); - - return coupon; - }); - - return coupon; - }), - - getReservedCoupons: protectedProcedure - .input(z.object({ - cursor: z.number().optional(), - limit: z.number().default(50), - search: z.string().optional(), - })) - .query(async ({ input }) => { - const { cursor, limit, search } = input; - - let whereCondition = undefined; - const conditions = []; - - if (cursor) { - conditions.push(lt(reservedCoupons.id, cursor)); - } - - if (search && search.trim()) { - conditions.push(or( - like(reservedCoupons.secretCode, `%${search}%`), - like(reservedCoupons.couponCode, `%${search}%`) - )); - } - - if (conditions.length > 0) { - whereCondition = and(...conditions); - } - - const result = await db.query.reservedCoupons.findMany({ - where: whereCondition, - with: { - redeemedUser: true, - creator: true, - }, - orderBy: (reservedCoupons, { desc }) => [desc(reservedCoupons.createdAt)], - limit: limit + 1, // Fetch one extra to check if there's more - }); - - const hasMore = result.length > limit; - const coupons = hasMore ? result.slice(0, limit) : result; - const nextCursor = hasMore ? result[result.length - 1].id : undefined; - - return { - coupons, - nextCursor, - }; - }), - - createReservedCoupon: protectedProcedure - .input(createCouponBodySchema) - .mutation(async ({ input, ctx }) => { - const { couponCode, isUserBased, discountPercent, flatDiscount, minOrder, productIds, applicableUsers, applicableProducts, maxValue, isApplyForAll, validTill, maxLimitForUser, exclusiveApply } = input; - - // Validation: ensure at least one discount type is provided - if ((!discountPercent && !flatDiscount) || (discountPercent && flatDiscount)) { - throw new Error("Either discountPercent or flatDiscount must be provided (but not both)"); - } - - // For reserved coupons, applicableUsers is not used, as it's redeemed by one user - - // Get staff user ID from auth middleware - const staffUserId = ctx.staffUser?.id; - if (!staffUserId) { - throw new Error("Unauthorized"); - } - - // Generate secret code if not provided (use couponCode as base) - let secretCode = couponCode || `SECRET${Date.now().toString().slice(-6)}${Math.random().toString(36).substring(2, 8).toUpperCase()}`; - - // Check if secret code already exists - const existing = await db.query.reservedCoupons.findFirst({ - where: eq(reservedCoupons.secretCode, secretCode), - }); - - if (existing) { - throw new Error("Secret code already exists"); - } - - const result = await db.insert(reservedCoupons).values({ - secretCode, - couponCode: couponCode || `RESERVED${Date.now().toString().slice(-6)}`, - discountPercent: discountPercent?.toString(), - flatDiscount: flatDiscount?.toString(), - minOrder: minOrder?.toString(), - productIds, - maxValue: maxValue?.toString(), - validTill: validTill ? dayjs(validTill).toDate() : undefined, - maxLimitForUser, - exclusiveApply: exclusiveApply || false, - createdBy: staffUserId, - }).returning(); - - const coupon = result[0]; - - // Insert applicable products if provided - if (applicableProducts && applicableProducts.length > 0) { - await db.insert(couponApplicableProducts).values( - applicableProducts.map(productId => ({ - couponId: coupon.id, - productId, - })) - ); - } - - return coupon; - }), - - getUsersMiniInfo: protectedProcedure - .input(z.object({ - search: z.string().optional(), - limit: z.number().min(1).max(50).default(20), - offset: z.number().min(0).default(0), - })) - .query(async ({ input }) => { - const { search, limit } = input; - - let whereCondition = undefined; - if (search && search.trim()) { - whereCondition = or( - like(users.name, `%${search}%`), - like(users.mobile, `%${search}%`) - ); - } - - const userList = await db.query.users.findMany({ - where: whereCondition, - columns: { - id: true, - name: true, - mobile: true, - }, - limit: limit, - offset: input.offset, - orderBy: (users, { asc }) => [asc(users.name)], - }); - - return { - users: userList.map(user => ({ - id: user.id, - name: user.name || 'Unknown', - mobile: user.mobile, - })) - }; - }), - - createCoupon: protectedProcedure - .input(z.object({ - mobile: z.string().min(1, 'Mobile number is required'), - })) - .mutation(async ({ input, ctx }) => { - const { mobile } = input; - - // Get staff user ID from auth middleware - const staffUserId = ctx.staffUser?.id; - if (!staffUserId) { - throw new Error("Unauthorized"); - } - - // Clean mobile number (remove non-digits) - const cleanMobile = mobile.replace(/\D/g, ''); - - // Validate: exactly 10 digits - if (cleanMobile.length !== 10) { - throw new Error("Mobile number must be exactly 10 digits"); - } - - // Check if user exists, create if not - let user = await db.query.users.findFirst({ - where: eq(users.mobile, cleanMobile), - }); - - if (!user) { - // Create new user - const [newUser] = await db.insert(users).values({ - name: null, - email: null, - mobile: cleanMobile, - }).returning(); - user = newUser; - } - - // Generate unique coupon code - const timestamp = Date.now().toString().slice(-6); - const random = Math.random().toString(36).substring(2, 6).toUpperCase(); - const couponCode = `MF${cleanMobile.slice(-4)}${timestamp}${random}`; - - // Check if coupon code already exists (very unlikely but safe) - const existingCode = await db.query.coupons.findFirst({ - where: eq(coupons.couponCode, couponCode), - }); - - if (existingCode) { - throw new Error("Generated coupon code already exists - please try again"); - } - - // Create the coupon - const [coupon] = await db.insert(coupons).values({ - couponCode, - isUserBased: true, - discountPercent: "20", // 20% discount - minOrder: "1000", // ₹1000 minimum order - maxValue: "500", // ₹500 maximum discount - maxLimitForUser: 1, // One-time use - isApplyForAll: false, - exclusiveApply: false, - createdBy: staffUserId, - validTill: dayjs().add(90, 'days').toDate(), // 90 days from now - }).returning(); - - // Associate coupon with user - await db.insert(couponApplicableUsers).values({ - couponId: coupon.id, - userId: user.id, - }); - - return { - success: true, - coupon: { - id: coupon.id, - couponCode: coupon.couponCode, - userId: user.id, - userMobile: user.mobile, - discountPercent: 20, - minOrder: 1000, - maxValue: 500, - maxLimitForUser: 1, - }, - }; - }), -}); diff --git a/verifier/admin-apis/apis/order.ts b/verifier/admin-apis/apis/order.ts deleted file mode 100644 index 707f642..0000000 --- a/verifier/admin-apis/apis/order.ts +++ /dev/null @@ -1,1018 +0,0 @@ -import { router, protectedProcedure } from "@/src/trpc/trpc-index" -import { z } from "zod"; -import { db } from "@/src/db/db_index" -import { - orders, - orderItems, - orderStatus, - users, - addresses, - refunds, - coupons, - couponUsage, - complaints, - payments, -} from "@/src/db/schema"; -import { eq, and, gte, lt, desc, SQL, inArray } from "drizzle-orm"; -import dayjs from "dayjs"; -import utc from "dayjs/plugin/utc"; -import { ApiError } from "@/src/lib/api-error" -import { - sendOrderPackagedNotification, - sendOrderDeliveredNotification, -} from "@/src/lib/notif-job"; -import { publishCancellation } from "@/src/lib/post-order-handler" -import { getMultipleUserNegativityScores } from "@/src/stores/user-negativity-store" - -const updateOrderNotesSchema = z.object({ - orderId: z.number(), - adminNotes: z.string(), -}); - -const getFullOrderSchema = z.object({ - orderId: z.number(), -}); - -const getOrderDetailsSchema = z.object({ - orderId: z.number(), -}); - -const updatePackagedSchema = z.object({ - orderId: z.string(), - isPackaged: z.boolean(), -}); - -const updateDeliveredSchema = z.object({ - orderId: z.string(), - isDelivered: z.boolean(), -}); - -const updateOrderItemPackagingSchema = z.object({ - orderItemId: z.number(), - isPackaged: z.boolean().optional(), - isPackageVerified: z.boolean().optional(), -}); - -const getSlotOrdersSchema = z.object({ - slotId: z.string(), -}); - -const getTodaysOrdersSchema = z.object({ - slotId: z.string().optional(), -}); - -const getAllOrdersSchema = z.object({ - cursor: z.number().optional(), - limit: z.number().default(20), - slotId: z.number().optional().nullable(), - packagedFilter: z - .enum(["all", "packaged", "not_packaged"]) - .optional() - .default("all"), - deliveredFilter: z - .enum(["all", "delivered", "not_delivered"]) - .optional() - .default("all"), - cancellationFilter: z - .enum(["all", "cancelled", "not_cancelled"]) - .optional() - .default("all"), - flashDeliveryFilter: z - .enum(["all", "flash", "regular"]) - .optional() - .default("all"), -}); - -export const orderRouter = router({ - updateNotes: protectedProcedure - .input(updateOrderNotesSchema) - .mutation(async ({ input }) => { - const { orderId, adminNotes } = input; - - const result = await db - .update(orders) - .set({ - adminNotes: adminNotes || null, - }) - .where(eq(orders.id, orderId)) - .returning(); - - if (result.length === 0) { - throw new Error("Order not found"); - } - - return result[0]; - }), - - getFullOrder: protectedProcedure - .input(getFullOrderSchema) - .query(async ({ input }) => { - const { orderId } = input; - - const orderData = await db.query.orders.findFirst({ - where: eq(orders.id, orderId), - with: { - user: true, - address: true, - slot: true, - orderItems: { - with: { - product: { - with: { - unit: true, - }, - }, - }, - }, - payment: true, - paymentInfo: true, - }, - }); - - if (!orderData) { - throw new Error("Order not found"); - } - - // Get order status separately - const statusRecord = await db.query.orderStatus.findFirst({ - where: eq(orderStatus.orderId, orderId), - }); - - let status: "pending" | "delivered" | "cancelled" = "pending"; - if (statusRecord?.isCancelled) { - status = "cancelled"; - } else if (statusRecord?.isDelivered) { - status = "delivered"; - } - - // Get refund details if order is cancelled - let refund = null; - if (status === "cancelled") { - refund = await db.query.refunds.findFirst({ - where: eq(refunds.orderId, orderId), - }); - } - - return { - id: orderData.id, - readableId: orderData.id, - customerName: `${orderData.user.name}`, - customerEmail: orderData.user.email, - customerMobile: orderData.user.mobile, - address: { - line1: orderData.address.addressLine1, - line2: orderData.address.addressLine2, - city: orderData.address.city, - state: orderData.address.state, - pincode: orderData.address.pincode, - phone: orderData.address.phone, - }, - slotInfo: orderData.slot - ? { - time: orderData.slot.deliveryTime.toISOString(), - sequence: orderData.slot.deliverySequence, - } - : null, - isCod: orderData.isCod, - isOnlinePayment: orderData.isOnlinePayment, - totalAmount: orderData.totalAmount, - adminNotes: orderData.adminNotes, - userNotes: orderData.userNotes, - createdAt: orderData.createdAt, - status, - isPackaged: - orderData.orderItems.every((item) => item.is_packaged) || false, - isDelivered: statusRecord?.isDelivered || false, - items: orderData.orderItems.map((item) => ({ - id: item.id, - name: item.product.name, - quantity: item.quantity, - price: item.price, - unit: item.product.unit?.shortNotation, - amount: - parseFloat(item.price.toString()) * - parseFloat(item.quantity || "0"), - })), - payment: orderData.payment - ? { - status: orderData.payment.status, - gateway: orderData.payment.gateway, - merchantOrderId: orderData.payment.merchantOrderId, - } - : null, - paymentInfo: orderData.paymentInfo - ? { - status: orderData.paymentInfo.status, - gateway: orderData.paymentInfo.gateway, - merchantOrderId: orderData.paymentInfo.merchantOrderId, - } - : null, - // Cancellation details (only present for cancelled orders) - cancelReason: statusRecord?.cancelReason || null, - cancellationReviewed: statusRecord?.cancellationReviewed || false, - isRefundDone: refund?.refundStatus === "processed" || false, - }; - }), - - getOrderDetails: protectedProcedure - .input(getOrderDetailsSchema) - .query(async ({ input }) => { - const { orderId } = input; - - // Single optimized query with all relations - const orderData = await db.query.orders.findFirst({ - where: eq(orders.id, orderId), - with: { - user: true, - address: true, - slot: true, - orderItems: { - with: { - product: { - with: { - unit: true, - }, - }, - }, - }, - payment: true, - paymentInfo: true, - orderStatus: true, // Include in main query - refunds: true, // Include in main query - }, - }); - - if (!orderData) { - throw new Error("Order not found"); - } - - // Get coupon usage for this specific order using new orderId field - const couponUsageData = await db.query.couponUsage.findMany({ - where: eq(couponUsage.orderId, orderData.id), // Use new orderId field - with: { - coupon: true, - }, - }); - - let couponData = null; - if (couponUsageData.length > 0) { - // Calculate total discount from multiple coupons - let totalDiscountAmount = 0; - const orderTotal = parseFloat(orderData.totalAmount.toString()); - - for (const usage of couponUsageData) { - let discountAmount = 0; - - if (usage.coupon.discountPercent) { - discountAmount = - (orderTotal * - parseFloat(usage.coupon.discountPercent.toString())) / - 100; - } else if (usage.coupon.flatDiscount) { - discountAmount = parseFloat(usage.coupon.flatDiscount.toString()); - } - - // Apply max value limit if set - if ( - usage.coupon.maxValue && - discountAmount > parseFloat(usage.coupon.maxValue.toString()) - ) { - discountAmount = parseFloat(usage.coupon.maxValue.toString()); - } - - totalDiscountAmount += discountAmount; - } - - couponData = { - couponCode: couponUsageData - .map((u) => u.coupon.couponCode) - .join(", "), - couponDescription: `${couponUsageData.length} coupons applied`, - discountAmount: totalDiscountAmount, - }; - } - - // Status determination from included relation - const statusRecord = orderData.orderStatus?.[0]; - let status: "pending" | "delivered" | "cancelled" = "pending"; - if (statusRecord?.isCancelled) { - status = "cancelled"; - } else if (statusRecord?.isDelivered) { - status = "delivered"; - } - - // Always include refund data (will be null/undefined if not cancelled) - const refund = orderData.refunds?.[0]; - - return { - id: orderData.id, - readableId: orderData.id, - userId: orderData.user.id, - customerName: `${orderData.user.name}`, - customerEmail: orderData.user.email, - customerMobile: orderData.user.mobile, - address: { - name: orderData.address.name, - line1: orderData.address.addressLine1, - line2: orderData.address.addressLine2, - city: orderData.address.city, - state: orderData.address.state, - pincode: orderData.address.pincode, - phone: orderData.address.phone, - }, - slotInfo: orderData.slot - ? { - time: orderData.slot.deliveryTime.toISOString(), - sequence: orderData.slot.deliverySequence, - } - : null, - isCod: orderData.isCod, - isOnlinePayment: orderData.isOnlinePayment, - totalAmount: parseFloat(orderData.totalAmount?.toString() || '0') - parseFloat(orderData.deliveryCharge?.toString() || '0'), - deliveryCharge: parseFloat(orderData.deliveryCharge?.toString() || '0'), - adminNotes: orderData.adminNotes, - userNotes: orderData.userNotes, - createdAt: orderData.createdAt, - status, - isPackaged: statusRecord?.isPackaged || false, - isDelivered: statusRecord?.isDelivered || false, - items: orderData.orderItems.map((item) => ({ - id: item.id, - name: item.product.name, - quantity: item.quantity, - productSize: item.product.productQuantity, - price: item.price, - unit: item.product.unit?.shortNotation, - amount: - parseFloat(item.price.toString()) * - parseFloat(item.quantity || "0"), - isPackaged: item.is_packaged, - isPackageVerified: item.is_package_verified, - })), - payment: orderData.payment - ? { - status: orderData.payment.status, - gateway: orderData.payment.gateway, - merchantOrderId: orderData.payment.merchantOrderId, - } - : null, - paymentInfo: orderData.paymentInfo - ? { - status: orderData.paymentInfo.status, - gateway: orderData.paymentInfo.gateway, - merchantOrderId: orderData.paymentInfo.merchantOrderId, - } - : null, - // Cancellation details (always included, null if not cancelled) - cancelReason: statusRecord?.cancelReason || null, - cancellationReviewed: statusRecord?.cancellationReviewed || false, - isRefundDone: refund?.refundStatus === "processed" || false, - refundStatus: refund?.refundStatus as RefundStatus, - refundAmount: refund?.refundAmount - ? parseFloat(refund.refundAmount.toString()) - : null, - // Coupon information - couponData: couponData, - couponCode: couponData?.couponCode || null, - couponDescription: couponData?.couponDescription || null, - discountAmount: couponData?.discountAmount || null, - orderStatus: statusRecord, - refundRecord: refund, - isFlashDelivery: orderData.isFlashDelivery, - }; - }), - - updatePackaged: protectedProcedure - .input(updatePackagedSchema) - .mutation(async ({ input }) => { - const { orderId, isPackaged } = input; - - // Update all order items to the specified packaged state - await db - .update(orderItems) - .set({ is_packaged: isPackaged }) - .where(eq(orderItems.orderId, parseInt(orderId))); - - // Also update the order status table for backward compatibility - if (!isPackaged) { - await db - .update(orderStatus) - .set({ isPackaged, isDelivered: false }) - .where(eq(orderStatus.orderId, parseInt(orderId))); - } else { - await db - .update(orderStatus) - .set({ isPackaged }) - .where(eq(orderStatus.orderId, parseInt(orderId))); - } - - const order = await db.query.orders.findFirst({ - where: eq(orders.id, parseInt(orderId)), - }); - if (order) await sendOrderPackagedNotification(order.userId, orderId); - - return { success: true }; - }), - - updateDelivered: protectedProcedure - .input(updateDeliveredSchema) - .mutation(async ({ input }) => { - const { orderId, isDelivered } = input; - - await db - .update(orderStatus) - .set({ isDelivered }) - .where(eq(orderStatus.orderId, parseInt(orderId))); - - const order = await db.query.orders.findFirst({ - where: eq(orders.id, parseInt(orderId)), - }); - if (order) await sendOrderDeliveredNotification(order.userId, orderId); - - return { success: true }; - }), - - updateOrderItemPackaging: protectedProcedure - .input(updateOrderItemPackagingSchema) - .mutation(async ({ input }) => { - const { orderItemId, isPackaged, isPackageVerified } = input; - - // Validate that orderItem exists - const orderItem = await db.query.orderItems.findFirst({ - where: eq(orderItems.id, orderItemId), - }); - - if (!orderItem) { - throw new ApiError("Order item not found", 404); - } - - // Build update object with only provided fields - const updateData: any = {}; - if (isPackaged !== undefined) { - updateData.is_packaged = isPackaged; - } - if (isPackageVerified !== undefined) { - updateData.is_package_verified = isPackageVerified; - } - - // Update the order item - await db - .update(orderItems) - .set(updateData) - .where(eq(orderItems.id, orderItemId)); - - return { success: true }; - }), - - removeDeliveryCharge: protectedProcedure - .input(z.object({ orderId: z.number() })) - .mutation(async ({ input }) => { - const { orderId } = input; - - const order = await db.query.orders.findFirst({ - where: eq(orders.id, orderId), - }); - - if (!order) { - throw new Error('Order not found'); - } - - const currentDeliveryCharge = parseFloat(order.deliveryCharge?.toString() || '0'); - const currentTotalAmount = parseFloat(order.totalAmount?.toString() || '0'); - const newTotalAmount = currentTotalAmount - currentDeliveryCharge; - - await db - .update(orders) - .set({ - deliveryCharge: '0', - totalAmount: newTotalAmount.toString() - }) - .where(eq(orders.id, orderId)); - - return { success: true, message: 'Delivery charge removed' }; - }), - - getSlotOrders: protectedProcedure - .input(getSlotOrdersSchema) - .query(async ({ input }) => { - const { slotId } = input; - - const slotOrders = await db.query.orders.findMany({ - where: eq(orders.slotId, parseInt(slotId)), - with: { - user: true, - address: true, - slot: true, - orderItems: { - with: { - product: { - with: { - unit: true, - }, - }, - }, - }, - orderStatus: true, - }, - }); - - const filteredOrders = slotOrders.filter((order) => { - const statusRecord = order.orderStatus[0]; - return ( - order.isCod || - (statusRecord && statusRecord.paymentStatus === "success") - ); - }); - - const formattedOrders = filteredOrders.map((order) => { - const statusRecord = order.orderStatus[0]; // assuming one status per order - let status: "pending" | "delivered" | "cancelled" = "pending"; - if (statusRecord?.isCancelled) { - status = "cancelled"; - } else if (statusRecord?.isDelivered) { - status = "delivered"; - } - - const items = order.orderItems.map((item) => ({ - id: item.id, - name: item.product.name, - quantity: parseFloat(item.quantity), - price: parseFloat(item.price.toString()), - amount: parseFloat(item.quantity) * parseFloat(item.price.toString()), - unit: item.product.unit?.shortNotation || "", - isPackaged: item.is_packaged, - isPackageVerified: item.is_package_verified, - })); - - return { - id: order.id, - readableId: order.id, - customerName: order.user.name, - address: `${order.address.addressLine1}${ - order.address.addressLine2 ? `, ${order.address.addressLine2}` : "" - }, ${order.address.city}, ${order.address.state} - ${ - order.address.pincode - }, Phone: ${order.address.phone}`, - addressId: order.addressId, - latitude: order.address.adminLatitude ?? order.address.latitude, - longitude: order.address.adminLongitude ?? order.address.longitude, - totalAmount: parseFloat(order.totalAmount), - items, - deliveryTime: order.slot?.deliveryTime.toISOString() || null, - status, - isPackaged: - order.orderItems.every((item) => item.is_packaged) || false, - isDelivered: statusRecord?.isDelivered || false, - isCod: order.isCod, - paymentMode: order.isCod ? "COD" : "Online", - paymentStatus: statusRecord?.paymentStatus || "pending", - slotId: order.slotId, - adminNotes: order.adminNotes, - userNotes: order.userNotes, - }; - }); - - return { success: true, data: formattedOrders }; - }), - - getTodaysOrders: protectedProcedure - .input(getTodaysOrdersSchema) - .query(async ({ input }) => { - const { slotId } = input; - const start = dayjs().startOf("day").toDate(); - const end = dayjs().endOf("day").toDate(); - - let whereCondition = and( - gte(orders.createdAt, start), - lt(orders.createdAt, end) - ); - - if (slotId) { - whereCondition = and( - whereCondition, - eq(orders.slotId, parseInt(slotId)) - ); - } - - const todaysOrders = await db.query.orders.findMany({ - where: whereCondition, - with: { - user: true, - address: true, - slot: true, - orderItems: { - with: { - product: { - with: { - unit: true, - }, - }, - }, - }, - orderStatus: true, - }, - }); - - const filteredOrders = todaysOrders.filter((order) => { - const statusRecord = order.orderStatus[0]; - return ( - order.isCod || - (statusRecord && statusRecord.paymentStatus === "success") - ); - }); - - const formattedOrders = filteredOrders.map((order) => { - const statusRecord = order.orderStatus[0]; // assuming one status per order - let status: "pending" | "delivered" | "cancelled" = "pending"; - if (statusRecord?.isCancelled) { - status = "cancelled"; - } else if (statusRecord?.isDelivered) { - status = "delivered"; - } - - const items = order.orderItems.map((item) => ({ - name: item.product.name, - quantity: parseFloat(item.quantity), - price: parseFloat(item.price.toString()), - amount: parseFloat(item.quantity) * parseFloat(item.price.toString()), - unit: item.product.unit?.shortNotation || "", - })); - - return { - orderId: order.id.toString(), - readableId: order.id, - customerName: order.user.name, - address: `${order.address.addressLine1}${ - order.address.addressLine2 ? `, ${order.address.addressLine2}` : "" - }, ${order.address.city}, ${order.address.state} - ${ - order.address.pincode - }`, - totalAmount: parseFloat(order.totalAmount), - items, - deliveryTime: order.slot?.deliveryTime.toISOString() || null, - status, - isPackaged: - order.orderItems.every((item) => item.is_packaged) || false, - isDelivered: statusRecord?.isDelivered || false, - isCod: order.isCod, - paymentMode: order.isCod ? "COD" : "Online", - paymentStatus: statusRecord?.paymentStatus || "pending", - slotId: order.slotId, - adminNotes: order.adminNotes, - userNotes: order.userNotes, - }; - }); - - return { success: true, data: formattedOrders }; - }), - - updateAddressCoords: protectedProcedure - .input( - z.object({ - addressId: z.number(), - latitude: z.number(), - longitude: z.number(), - }) - ) - .mutation(async ({ input }) => { - const { addressId, latitude, longitude } = input; - - const result = await db - .update(addresses) - .set({ - adminLatitude: latitude, - adminLongitude: longitude, - }) - .where(eq(addresses.id, addressId)) - .returning(); - - if (result.length === 0) { - throw new ApiError("Address not found", 404); - } - - return { success: true }; - }), - - getAll: protectedProcedure - .input(getAllOrdersSchema) - .query(async ({ input }) => { - try { - const { - cursor, - limit, - slotId, - packagedFilter, - deliveredFilter, - cancellationFilter, - flashDeliveryFilter, - } = input; - - let whereCondition: SQL | undefined = eq(orders.id, orders.id); // always true - if (cursor) { - whereCondition = and(whereCondition, lt(orders.id, cursor)); - } - if (slotId) { - whereCondition = and(whereCondition, eq(orders.slotId, slotId)); - } - if (packagedFilter === "packaged") { - whereCondition = and( - whereCondition, - eq(orderStatus.isPackaged, true) - ); - } else if (packagedFilter === "not_packaged") { - whereCondition = and( - whereCondition, - eq(orderStatus.isPackaged, false) - ); - } - if (deliveredFilter === "delivered") { - whereCondition = and( - whereCondition, - eq(orderStatus.isDelivered, true) - ); - } else if (deliveredFilter === "not_delivered") { - whereCondition = and( - whereCondition, - eq(orderStatus.isDelivered, false) - ); - } - if (cancellationFilter === "cancelled") { - whereCondition = and( - whereCondition, - eq(orderStatus.isCancelled, true) - ); - } else if (cancellationFilter === "not_cancelled") { - whereCondition = and( - whereCondition, - eq(orderStatus.isCancelled, false) - ); - } - if (flashDeliveryFilter === "flash") { - whereCondition = and( - whereCondition, - eq(orders.isFlashDelivery, true) - ); - } else if (flashDeliveryFilter === "regular") { - whereCondition = and( - whereCondition, - eq(orders.isFlashDelivery, false) - ); - } - - const allOrders = await db.query.orders.findMany({ - where: whereCondition, - orderBy: desc(orders.createdAt), - limit: limit + 1, // fetch one extra to check if there's more - with: { - user: true, - address: true, - slot: true, - orderItems: { - with: { - product: { - with: { - unit: true, - }, - }, - }, - }, - orderStatus: true, - }, - }); - - const hasMore = allOrders.length > limit; - const ordersToReturn = hasMore ? allOrders.slice(0, limit) : allOrders; - - const userIds = [...new Set(ordersToReturn.map(o => o.userId))]; - const negativityScores = await getMultipleUserNegativityScores(userIds); - - const filteredOrders = ordersToReturn.filter((order) => { - const statusRecord = order.orderStatus[0]; - return ( - order.isCod || - (statusRecord && statusRecord.paymentStatus === "success") - ); - }); - - const formattedOrders = filteredOrders.map((order) => { - const statusRecord = order.orderStatus[0]; - let status: "pending" | "delivered" | "cancelled" = "pending"; - if (statusRecord?.isCancelled) { - status = "cancelled"; - } else if (statusRecord?.isDelivered) { - status = "delivered"; - } - - const items = order.orderItems - .map((item) => ({ - id: item.id, - name: item.product.name, - quantity: parseFloat(item.quantity), - price: parseFloat(item.price.toString()), - amount: - parseFloat(item.quantity) * parseFloat(item.price.toString()), - unit: item.product.unit?.shortNotation || "", - productSize: item.product.productQuantity, - isPackaged: item.is_packaged, - isPackageVerified: item.is_package_verified, - })) - .sort((first, second) => first.id - second.id); - dayjs.extend(utc); - return { - id: order.id, - orderId: order.id.toString(), - readableId: order.id, - customerName: order.user.name, - customerMobile: order.user.mobile, - address: `${order.address.addressLine1}${ - order.address.addressLine2 - ? `, ${order.address.addressLine2}` - : "" - }, ${order.address.city}, ${order.address.state} - ${ - order.address.pincode - }, Phone: ${order.address.phone}`, - addressId: order.addressId, - latitude: order.address.adminLatitude ?? order.address.latitude, - longitude: order.address.adminLongitude ?? order.address.longitude, - totalAmount: parseFloat(order.totalAmount), - deliveryCharge: parseFloat(order.deliveryCharge || "0"), - items, - createdAt: order.createdAt, - // deliveryTime: order.slot ? dayjs.utc(order.slot.deliveryTime).format('ddd, MMM D • h:mm A') : 'Not scheduled', - deliveryTime: order.slot?.deliveryTime.toISOString() || null, - status, - isPackaged: - order.orderItems.every((item) => item.is_packaged) || false, - isDelivered: statusRecord?.isDelivered || false, - isCod: order.isCod, - isFlashDelivery: order.isFlashDelivery, - userNotes: order.userNotes, - adminNotes: order.adminNotes, - userNegativityScore: negativityScores[order.userId] || 0, - }; - }); - - return { - orders: formattedOrders, - nextCursor: hasMore - ? ordersToReturn[ordersToReturn.length - 1].id - : undefined, - }; - } catch (e) { - console.log({ e }); - } - }), - - rebalanceSlots: protectedProcedure - .input(z.object({ slotIds: z.array(z.number()).min(1).max(50) })) - .mutation(async ({ input }) => { - const slotIds = input.slotIds; - - const ordersList = await db.query.orders.findMany({ - where: inArray(orders.slotId, slotIds), - with: { - orderItems: { - with: { - product: true - } - }, - couponUsages: { - with: { - coupon: true - } - }, - } - }); - - const processedOrdersData = ordersList.map((order) => { - - let newTotal = order.orderItems.reduce((acc,item) => { - const latestPrice = +item.product.price; - const amount = (latestPrice * Number(item.quantity)); - return acc+amount; - },0) - - order.orderItems.forEach(item => { - item.price = item.product.price; - item.discountedPrice = item.product.price - }) - - const coupon = order.couponUsages[0]?.coupon; - - let discount = 0; - if(coupon && !coupon.isInvalidated && (!coupon.validTill || new Date(coupon.validTill) > new Date())) { - const proportion = Number(order.orderGroupProportion || 1); - if(coupon.discountPercent) { - const maxDiscount = Number(coupon.maxValue || Infinity) * proportion; - discount = Math.min((newTotal * parseFloat(coupon.discountPercent)) / 100, maxDiscount); - } - else { - discount = Number(coupon.flatDiscount) * proportion; - } - } - newTotal -= discount - - const { couponUsages, orderItems: orderItemsRaw, ...rest} = order; - const updatedOrderItems = orderItemsRaw.map(item => { - const { product, ...rawOrderItem } = item; - return rawOrderItem; - }) - return {order: rest, updatedOrderItems, newTotal } - }) - - const updatedOrderIds: number[] = []; - await db.transaction(async (tx) => { - for (const { order, updatedOrderItems, newTotal } of processedOrdersData) { - await tx.update(orders).set({ totalAmount: newTotal.toString() }).where(eq(orders.id, order.id)); - updatedOrderIds.push(order.id); - - for (const item of updatedOrderItems) { - await tx.update(orderItems).set({ - price: item.price, - discountedPrice: item.discountedPrice - }).where(eq(orderItems.id, item.id)); - } - } - }); - - return { success: true, updatedOrders: updatedOrderIds, message: `Rebalanced ${updatedOrderIds.length} orders.` }; - }), - - cancelOrder: protectedProcedure - .input(z.object({ - orderId: z.number(), - reason: z.string().min(1, "Cancellation reason is required"), - })) - .mutation(async ({ input }) => { - const { orderId, reason } = input; - - const order = await db.query.orders.findFirst({ - where: eq(orders.id, orderId), - with: { - orderStatus: true, - }, - }); - - if (!order) { - throw new ApiError("Order not found", 404); - } - - const status = order.orderStatus[0]; - if (!status) { - throw new ApiError("Order status not found", 400); - } - - if (status.isCancelled) { - throw new ApiError("Order is already cancelled", 400); - } - - if (status.isDelivered) { - throw new ApiError("Cannot cancel delivered order", 400); - } - - const result = await db.transaction(async (tx) => { - await tx - .update(orderStatus) - .set({ - isCancelled: true, - isCancelledByAdmin: true, - cancelReason: reason, - cancellationAdminNotes: reason, - cancellationReviewed: true, - cancellationReviewedAt: new Date(), - }) - .where(eq(orderStatus.id, status.id)); - - const refundStatus = order.isCod ? "na" : "pending"; - - await tx.insert(refunds).values({ - orderId: order.id, - refundStatus, - }); - - return { orderId: order.id, userId: order.userId }; - }); - - // Publish to Redis for Telegram notification - await publishCancellation(result.orderId, 'admin', reason); - - return { success: true, message: "Order cancelled successfully" }; - }), -}); - -// {"id": "order_Rhh00qJNdjUp8o", "notes": {"retry": "true", "customerOrderId": "14"}, "amount": 21000, "entity": "order", "status": "created", "receipt": "order_14_retry", "attempts": 0, "currency": "INR", "offer_id": null, "signature": "6df20655021f1d6841340f2a2ef2ef9378cb3d43495ab09e85f08aea1a851583", "amount_due": 21000, "created_at": 1763575791, "payment_id": "pay_Rhh15cLL28YM7j", "amount_paid": 0} - -type RefundStatus = "success" | "pending" | "failed" | "none" | "na"; - -export async function deleteOrderById(orderId: number): Promise { - await db.transaction(async (tx) => { - await tx.delete(orderItems).where(eq(orderItems.orderId, orderId)); - await tx.delete(orderStatus).where(eq(orderStatus.orderId, orderId)); - await tx.delete(payments).where(eq(payments.orderId, orderId)); - await tx.delete(refunds).where(eq(refunds.orderId, orderId)); - await tx.delete(couponUsage).where(eq(couponUsage.orderId, orderId)); - await tx.delete(complaints).where(eq(complaints.orderId, orderId)); - await tx.delete(orders).where(eq(orders.id, orderId)); - }); -} - diff --git a/verifier/admin-apis/apis/payments.ts b/verifier/admin-apis/apis/payments.ts deleted file mode 100644 index 51de5fb..0000000 --- a/verifier/admin-apis/apis/payments.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { router, protectedProcedure } from "@/src/trpc/trpc-index" -import { z } from "zod"; -import { db } from "@/src/db/db_index" -import { - orders, - orderStatus, - payments, - refunds, -} from "@/src/db/schema"; -import { and, eq } from "drizzle-orm"; -import { ApiError } from "@/src/lib/api-error" -import { RazorpayPaymentService } from "@/src/lib/payments-utils" - -const initiateRefundSchema = z - .object({ - orderId: z.number(), - refundPercent: z.number().min(0).max(100).optional(), - refundAmount: z.number().min(0).optional(), - }) - .refine( - (data) => { - const hasPercent = data.refundPercent !== undefined; - const hasAmount = data.refundAmount !== undefined; - return (hasPercent && !hasAmount) || (!hasPercent && hasAmount); - }, - { - message: - "Provide either refundPercent or refundAmount, not both or neither", - } - ); - -export const adminPaymentsRouter = router({ - initiateRefund: protectedProcedure - .input(initiateRefundSchema) - .mutation(async ({ input }) => { - try { - const { orderId, refundPercent, refundAmount } = input; - - // Validate order exists - const order = await db.query.orders.findFirst({ - where: eq(orders.id, orderId), - }); - - if (!order) { - throw new ApiError("Order not found", 404); - } - - // Check if order is paid - const orderStatusRecord = await db.query.orderStatus.findFirst({ - where: eq(orderStatus.orderId, orderId), - }); - - if(order.isCod) { - throw new ApiError("Order is a Cash On Delivery. Not eligible for refund") - } - - if ( - !orderStatusRecord || - (orderStatusRecord.paymentStatus !== "success" && - !(order.isCod && orderStatusRecord.isDelivered)) - ) { - throw new ApiError("Order payment not verified or not eligible for refund", 400); - } - - // Calculate refund amount - let calculatedRefundAmount: number; - if (refundPercent !== undefined) { - calculatedRefundAmount = - (parseFloat(order.totalAmount) * refundPercent) / 100; - } else if (refundAmount !== undefined) { - calculatedRefundAmount = refundAmount; - if (calculatedRefundAmount > parseFloat(order.totalAmount)) { - throw new ApiError("Refund amount cannot exceed order total", 400); - } - } else { - throw new ApiError("Invalid refund parameters", 400); - } - - let razorpayRefund = null; - let merchantRefundId = null; - - // Get payment record for online payments - const payment = await db.query.payments.findFirst({ - where: and( - eq(payments.orderId, orderId), - eq(payments.status, "success") - ), - }); - - if (!payment || payment.status !== "success") { - throw new ApiError("Payment not found or not successful", 404); - } - - const payload = payment.payload as any; - // Initiate Razorpay refund - razorpayRefund = await RazorpayPaymentService.initiateRefund( - payload.payment_id, - Math.round(calculatedRefundAmount * 100) // Convert to paisa - ); - merchantRefundId = razorpayRefund.id; - - - - // Check if refund already exists for this order - const existingRefund = await db.query.refunds.findFirst({ - where: eq(refunds.orderId, orderId), - }); - - const refundStatus = "initiated"; - - if (existingRefund) { - // Update existing refund - await db - .update(refunds) - .set({ - refundAmount: calculatedRefundAmount.toString(), - refundStatus, - merchantRefundId, - refundProcessedAt: order.isCod ? new Date() : null, - }) - .where(eq(refunds.id, existingRefund.id)); - } else { - // Insert new refund - await db - .insert(refunds) - .values({ - orderId, - refundAmount: calculatedRefundAmount.toString(), - refundStatus, - merchantRefundId, - }); - } - - return { - refundId: merchantRefundId || `cod_${orderId}`, - amount: calculatedRefundAmount, - status: refundStatus, - message: order.isCod ? "COD refund processed successfully" : "Refund initiated successfully", - }; - } - catch(e) { - console.log(e); - throw new ApiError("Failed to initiate refund") - } - }), -}); diff --git a/verifier/admin-apis/apis/product-availability-schedules.ts b/verifier/admin-apis/apis/product-availability-schedules.ts deleted file mode 100644 index 47cdc43..0000000 --- a/verifier/admin-apis/apis/product-availability-schedules.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { router, protectedProcedure } from '@/src/trpc/trpc-index' -import { z } from 'zod'; -import { db } from '@/src/db/db_index' -import { productAvailabilitySchedules } from '@/src/db/schema' -import { eq } from 'drizzle-orm'; -import { refreshScheduleJobs } from '@/src/lib/automatedJobs'; - -const createScheduleSchema = z.object({ - scheduleName: z.string().min(1, "Schedule name is required"), - time: z.string().min(1, "Time is required").regex(/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/, "Invalid time format. Use HH:MM"), - action: z.enum(['in', 'out']), - productIds: z.array(z.number().int().positive()).min(1, "At least one product is required"), - groupIds: z.array(z.number().int().positive()).default([]), -}); - -const updateScheduleSchema = z.object({ - id: z.number().int().positive(), - updates: createScheduleSchema.partial().extend({ - scheduleName: z.string().min(1).optional(), - productIds: z.array(z.number().int().positive()).optional(), - groupIds: z.array(z.number().int().positive()).optional(), - }), -}); - -export const productAvailabilitySchedulesRouter = router({ - create: protectedProcedure - .input(createScheduleSchema) - .mutation(async ({ input, ctx }) => { - const { scheduleName, time, action, productIds, groupIds } = input; - - // Get staff user ID from auth middleware - const staffUserId = ctx.staffUser?.id; - if (!staffUserId) { - throw new Error("Unauthorized"); - } - - // Check if schedule name already exists - const existingSchedule = await db.query.productAvailabilitySchedules.findFirst({ - where: eq(productAvailabilitySchedules.scheduleName, scheduleName), - }); - if (existingSchedule) { - throw new Error("Schedule name already exists"); - } - - // Create schedule with arrays - const scheduleResult = await db.insert(productAvailabilitySchedules).values({ - scheduleName, - time, - action, - productIds, - groupIds, - }).returning(); - - // Refresh cron jobs to include new schedule - await refreshScheduleJobs(); - - return scheduleResult[0]; - }), - - getAll: protectedProcedure - .query(async () => { - const schedules = await db.query.productAvailabilitySchedules.findMany({ - orderBy: (productAvailabilitySchedules, { desc }) => [desc(productAvailabilitySchedules.createdAt)], - }); - - return schedules.map(schedule => ({ - ...schedule, - productCount: schedule.productIds.length, - groupCount: schedule.groupIds.length, - })); - }), - - getById: protectedProcedure - .input(z.object({ id: z.number().int().positive() })) - .query(async ({ input }) => { - const { id } = input; - - const schedule = await db.query.productAvailabilitySchedules.findFirst({ - where: eq(productAvailabilitySchedules.id, id), - }); - - if (!schedule) { - throw new Error("Schedule not found"); - } - - return schedule; - }), - - update: protectedProcedure - .input(updateScheduleSchema) - .mutation(async ({ input }) => { - const { id, updates } = input; - - // Check if schedule exists - const existingSchedule = await db.query.productAvailabilitySchedules.findFirst({ - where: eq(productAvailabilitySchedules.id, id), - }); - if (!existingSchedule) { - throw new Error("Schedule not found"); - } - - // Check schedule name uniqueness if being updated - if (updates.scheduleName && updates.scheduleName !== existingSchedule.scheduleName) { - const duplicateSchedule = await db.query.productAvailabilitySchedules.findFirst({ - where: eq(productAvailabilitySchedules.scheduleName, updates.scheduleName), - }); - if (duplicateSchedule) { - throw new Error("Schedule name already exists"); - } - } - - // Update schedule - const updateData: any = {}; - if (updates.scheduleName !== undefined) updateData.scheduleName = updates.scheduleName; - if (updates.time !== undefined) updateData.time = updates.time; - if (updates.action !== undefined) updateData.action = updates.action; - if (updates.productIds !== undefined) updateData.productIds = updates.productIds; - if (updates.groupIds !== undefined) updateData.groupIds = updates.groupIds; - updateData.lastUpdated = new Date(); - - const result = await db.update(productAvailabilitySchedules) - .set(updateData) - .where(eq(productAvailabilitySchedules.id, id)) - .returning(); - - if (result.length === 0) { - throw new Error("Failed to update schedule"); - } - - // Refresh cron jobs to reflect changes - await refreshScheduleJobs(); - - return result[0]; - }), - - delete: protectedProcedure - .input(z.object({ id: z.number().int().positive() })) - .mutation(async ({ input }) => { - const { id } = input; - - const result = await db.delete(productAvailabilitySchedules) - .where(eq(productAvailabilitySchedules.id, id)) - .returning(); - - if (result.length === 0) { - throw new Error("Schedule not found"); - } - - // Refresh cron jobs to remove deleted schedule - await refreshScheduleJobs(); - - return { message: "Schedule deleted successfully" }; - }), -}); diff --git a/verifier/admin-apis/apis/product.ts b/verifier/admin-apis/apis/product.ts deleted file mode 100644 index 4677c40..0000000 --- a/verifier/admin-apis/apis/product.ts +++ /dev/null @@ -1,758 +0,0 @@ -import { router, protectedProcedure } from '@/src/trpc/trpc-index' -import { z } from 'zod'; -import { db } from '@/src/db/db_index' -import { productInfo, units, specialDeals, productSlots, productTags, productReviews, users, productGroupInfo, productGroupMembership } from '@/src/db/schema' -import { eq, and, inArray, desc, sql } from 'drizzle-orm'; -import { ApiError } from '@/src/lib/api-error' -import { imageUploadS3, scaffoldAssetUrl, getOriginalUrlFromSignedUrl, claimUploadUrl } from '@/src/lib/s3-client' -import { deleteS3Image } from '@/src/lib/delete-image' -import type { SpecialDeal } from '@/src/db/types' -import { scheduleStoreInitialization } from '@/src/stores/store-initializer' - - -type CreateDeal = { - quantity: number; - price: number; - validTill: string; -}; - -export const productRouter = router({ - getProducts: protectedProcedure - .query(async ({ ctx }) => { - const products = await db.query.productInfo.findMany({ - orderBy: productInfo.name, - with: { - unit: true, - store: true, - }, - }); - - // Generate signed URLs for all product images - const productsWithSignedUrls = await Promise.all( - products.map(async (product) => ({ - ...product, - images: scaffoldAssetUrl((product.images as string[]) || []), - })) - ); - - return { - products: productsWithSignedUrls, - count: productsWithSignedUrls.length, - }; - }), - - getProductById: protectedProcedure - .input(z.object({ - id: z.number(), - })) - .query(async ({ input, ctx }) => { - const { id } = input; - - const product = await db.query.productInfo.findFirst({ - where: eq(productInfo.id, id), - with: { - unit: true, - }, - }); - - if (!product) { - throw new ApiError("Product not found", 404); - } - - // Fetch special deals for this product - const deals = await db.query.specialDeals.findMany({ - where: eq(specialDeals.productId, id), - orderBy: specialDeals.quantity, - }); - - // Fetch associated tags for this product - const productTagsData = await db.query.productTags.findMany({ - where: eq(productTags.productId, id), - with: { - tag: true, - }, - }); - - // Generate signed URLs for product images - const productWithSignedUrls = { - ...product, - images: scaffoldAssetUrl((product.images as string[]) || []), - deals, - tags: productTagsData.map(pt => pt.tag), - }; - - return { - product: productWithSignedUrls, - }; - }), - - deleteProduct: protectedProcedure - .input(z.object({ - id: z.number(), - })) - .mutation(async ({ input, ctx }) => { - const { id } = input; - - const [deletedProduct] = await db - .delete(productInfo) - .where(eq(productInfo.id, id)) - .returning(); - - if (!deletedProduct) { - throw new ApiError("Product not found", 404); - } - - // Reinitialize stores to reflect changes - scheduleStoreInitialization() - - return { - message: "Product deleted successfully", - }; - }), - - createProduct: protectedProcedure - .input(z.object({ - name: z.string().min(1), - shortDescription: z.string().optional(), - longDescription: z.string().optional(), - unitId: z.number(), - storeId: z.number(), - price: z.number(), - marketPrice: z.number().optional(), - incrementStep: z.number().default(1), - productQuantity: z.number().default(1), - isSuspended: z.boolean().default(false), - isFlashAvailable: z.boolean().default(false), - flashPrice: z.number().optional(), - deals: z.array(z.object({ - quantity: z.number(), - price: z.number(), - validTill: z.string(), - })).optional(), - tagIds: z.array(z.number()).optional(), - imageKeys: z.array(z.string()).optional(), - })) - .mutation(async ({ input }) => { - const { - name, shortDescription, longDescription, unitId, storeId, - price, marketPrice, incrementStep, productQuantity, - isSuspended, isFlashAvailable, flashPrice, - deals, tagIds, imageKeys - } = input; - - // Validation - if (!name || !unitId || !storeId || !price) { - throw new ApiError("Name, unitId, storeId, and price are required", 400); - } - - // Check for duplicate name - const existingProduct = await db.query.productInfo.findFirst({ - where: eq(productInfo.name, name.trim()), - }); - if (existingProduct) { - throw new ApiError("A product with this name already exists", 400); - } - - // Check if unit exists - const unit = await db.query.units.findFirst({ - where: eq(units.id, unitId), - }); - if (!unit) { - throw new ApiError("Invalid unit ID", 400); - } - - console.log(imageKeys) - const [newProduct] = await db - .insert(productInfo) - .values({ - name: name.trim(), - shortDescription, - longDescription, - unitId, - storeId, - price: price.toString(), - marketPrice: marketPrice?.toString(), - incrementStep, - productQuantity, - isSuspended, - isFlashAvailable, - flashPrice: flashPrice?.toString(), - images: imageKeys || [], - }) - .returning(); - - // Handle deals - if (deals && deals.length > 0) { - const dealInserts = deals.map(deal => ({ - productId: newProduct.id, - quantity: deal.quantity.toString(), - price: deal.price.toString(), - validTill: new Date(deal.validTill), - })); - await db.insert(specialDeals).values(dealInserts); - } - - // Handle tags - if (tagIds && tagIds.length > 0) { - const tagAssociations = tagIds.map(tagId => ({ - productId: newProduct.id, - tagId, - })); - await db.insert(productTags).values(tagAssociations); - } - - // Claim upload URLs - if (imageKeys && imageKeys.length > 0) { - for (const key of imageKeys) { - try { - await claimUploadUrl(key); - } catch (e) { - console.warn(`Failed to claim upload URL for key: ${key}`, e); - } - } - } - - scheduleStoreInitialization(); - - return { - product: newProduct, - message: "Product created successfully", - }; - }), - - updateProduct: protectedProcedure - .input(z.object({ - id: z.number(), - name: z.string().min(1).optional(), - shortDescription: z.string().optional(), - longDescription: z.string().optional(), - unitId: z.number().optional(), - storeId: z.number().optional(), - price: z.number().optional(), - marketPrice: z.number().optional(), - incrementStep: z.number().optional(), - productQuantity: z.number().optional(), - isSuspended: z.boolean().optional(), - isFlashAvailable: z.boolean().optional(), - flashPrice: z.number().optional(), - deals: z.array(z.object({ - quantity: z.number(), - price: z.number(), - validTill: z.string(), - })).optional(), - tagIds: z.array(z.number()).optional(), - newImageKeys: z.array(z.string()).optional(), - imagesToDelete: z.array(z.string()).optional(), - })) - .mutation(async ({ input }) => { - const { id, newImageKeys, imagesToDelete, deals, tagIds, ...updateData } = input; - - // Get current product - const currentProduct = await db.query.productInfo.findFirst({ - where: eq(productInfo.id, id), - }); - if (!currentProduct) { - throw new ApiError("Product not found", 404); - } - - // Handle image deletions - let currentImages = (currentProduct.images as string[]) || []; - if (imagesToDelete && imagesToDelete.length > 0) { - for (const imageUrl of imagesToDelete) { - try { - await deleteS3Image(imageUrl); - } catch (e) { - console.error(`Failed to delete image: ${imageUrl}`, e); - } - } - currentImages = currentImages.filter(img => { - //!imagesToDelete.includes(img) - const isRemoved = imagesToDelete.some(item => item.includes(img)); - return !isRemoved; - }); - } - - // Add new images - if (newImageKeys && newImageKeys.length > 0) { - currentImages = [...currentImages, ...newImageKeys]; - - for (const key of newImageKeys) { - try { - await claimUploadUrl(key); - } catch (e) { - console.warn(`Failed to claim upload URL for key: ${key}`, e); - } - } - } - - // Update product - convert numeric fields to strings for PostgreSQL numeric type - const { price, marketPrice, flashPrice, ...otherData } = updateData; - const [updatedProduct] = await db - .update(productInfo) - .set({ - ...otherData, - ...(price !== undefined && { price: price.toString() }), - ...(marketPrice !== undefined && { marketPrice: marketPrice.toString() }), - ...(flashPrice !== undefined && { flashPrice: flashPrice.toString() }), - images: currentImages, - }) - .where(eq(productInfo.id, id)) - .returning(); - - // Handle deals update - if (deals !== undefined) { - await db.delete(specialDeals).where(eq(specialDeals.productId, id)); - if (deals.length > 0) { - const dealInserts = deals.map(deal => ({ - productId: id, - quantity: deal.quantity.toString(), - price: deal.price.toString(), - validTill: new Date(deal.validTill), - })); - await db.insert(specialDeals).values(dealInserts); - } - } - - // Handle tags update - if (tagIds !== undefined) { - await db.delete(productTags).where(eq(productTags.productId, id)); - if (tagIds.length > 0) { - const tagAssociations = tagIds.map(tagId => ({ - productId: id, - tagId, - })); - await db.insert(productTags).values(tagAssociations); - } - } - - scheduleStoreInitialization(); - - return { - product: updatedProduct, - message: "Product updated successfully", - }; - }), - - toggleOutOfStock: protectedProcedure - .input(z.object({ - id: z.number(), - })) - .mutation(async ({ input, ctx }) => { - const { id } = input; - - const product = await db.query.productInfo.findFirst({ - where: eq(productInfo.id, id), - }); - - if (!product) { - throw new ApiError("Product not found", 404); - } - - const [updatedProduct] = await db - .update(productInfo) - .set({ - isOutOfStock: !product.isOutOfStock, - }) - .where(eq(productInfo.id, id)) - .returning(); - - // Reinitialize stores to reflect changes - scheduleStoreInitialization() - - return { - product: updatedProduct, - message: `Product marked as ${updatedProduct.isOutOfStock ? 'out of stock' : 'in stock'}`, - }; - }), - - updateSlotProducts: protectedProcedure - .input(z.object({ - slotId: z.string(), - productIds: z.array(z.string()), - })) - .mutation(async ({ input, ctx }) => { - const { slotId, productIds } = input; - - if (!Array.isArray(productIds)) { - throw new ApiError("productIds must be an array", 400); - } - - // Get current associations - const currentAssociations = await db.query.productSlots.findMany({ - where: eq(productSlots.slotId, parseInt(slotId)), - columns: { - productId: true, - }, - }); - - const currentProductIds = currentAssociations.map(assoc => assoc.productId); - const newProductIds = productIds.map((id: string) => parseInt(id)); - - // Find products to add and remove - const productsToAdd = newProductIds.filter(id => !currentProductIds.includes(id)); - const productsToRemove = currentProductIds.filter(id => !newProductIds.includes(id)); - - // Remove associations for products that are no longer selected - if (productsToRemove.length > 0) { - await db.delete(productSlots).where( - and( - eq(productSlots.slotId, parseInt(slotId)), - inArray(productSlots.productId, productsToRemove) - ) - ); - } - - // Add associations for newly selected products - if (productsToAdd.length > 0) { - const newAssociations = productsToAdd.map(productId => ({ - productId, - slotId: parseInt(slotId), - })); - - await db.insert(productSlots).values(newAssociations); - } - - // Reinitialize stores to reflect changes - scheduleStoreInitialization() - - return { - message: "Slot products updated successfully", - added: productsToAdd.length, - removed: productsToRemove.length, - }; - }), - - getSlotProductIds: protectedProcedure - .input(z.object({ - slotId: z.string(), - })) - .query(async ({ input, ctx }) => { - const { slotId } = input; - - const associations = await db.query.productSlots.findMany({ - where: eq(productSlots.slotId, parseInt(slotId)), - columns: { - productId: true, - }, - }); - - const productIds = associations.map(assoc => assoc.productId); - - return { - productIds, - }; - }), - - getSlotsProductIds: protectedProcedure - .input(z.object({ - slotIds: z.array(z.number()), - })) - .query(async ({ input, ctx }) => { - const { slotIds } = input; - - if (!Array.isArray(slotIds)) { - throw new ApiError("slotIds must be an array", 400); - } - - if (slotIds.length === 0) { - return {}; - } - - // Fetch all associations for the requested slots - const associations = await db.query.productSlots.findMany({ - where: inArray(productSlots.slotId, slotIds), - columns: { - slotId: true, - productId: true, - }, - }); - - // Group by slotId - const result = associations.reduce((acc, assoc) => { - if (!acc[assoc.slotId]) { - acc[assoc.slotId] = []; - } - acc[assoc.slotId].push(assoc.productId); - return acc; - }, {} as Record); - - // Ensure all requested slots have entries (even if empty) - slotIds.forEach(slotId => { - if (!result[slotId]) { - result[slotId] = []; - } - }); - - return result; - }), - - getProductReviews: protectedProcedure - .input(z.object({ - productId: z.number().int().positive(), - limit: z.number().int().min(1).max(50).optional().default(10), - offset: z.number().int().min(0).optional().default(0), - })) - .query(async ({ input }) => { - const { productId, limit, offset } = input; - - const reviews = await db - .select({ - id: productReviews.id, - reviewBody: productReviews.reviewBody, - ratings: productReviews.ratings, - imageUrls: productReviews.imageUrls, - reviewTime: productReviews.reviewTime, - adminResponse: productReviews.adminResponse, - adminResponseImages: productReviews.adminResponseImages, - 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); - - // Generate signed URLs for images - const reviewsWithSignedUrls = await Promise.all( - reviews.map(async (review) => ({ - ...review, - signedImageUrls: scaffoldAssetUrl((review.imageUrls as string[]) || []), - signedAdminImageUrls: scaffoldAssetUrl((review.adminResponseImages as string[]) || []), - })) - ); - - // Check if more reviews exist - const totalCountResult = await db - .select({ count: sql`count(*)` }) - .from(productReviews) - .where(eq(productReviews.productId, productId)); - - const totalCount = Number(totalCountResult[0].count); - const hasMore = offset + limit < totalCount; - - return { reviews: reviewsWithSignedUrls, hasMore }; - }), - - respondToReview: protectedProcedure - .input(z.object({ - reviewId: z.number().int().positive(), - adminResponse: z.string().optional(), - adminResponseImages: z.array(z.string()).optional().default([]), - uploadUrls: z.array(z.string()).optional().default([]), - })) - .mutation(async ({ input }) => { - const { reviewId, adminResponse, adminResponseImages, uploadUrls } = input; - - const [updatedReview] = await db - .update(productReviews) - .set({ - adminResponse, - adminResponseImages, - }) - .where(eq(productReviews.id, reviewId)) - .returning(); - - if (!updatedReview) { - throw new ApiError('Review not found', 404); - } - - // Claim upload URLs - if (uploadUrls && uploadUrls.length > 0) { - // const { claimUploadUrl } = await import('@/src/lib/s3-client'); - await Promise.all(uploadUrls.map(url => claimUploadUrl(url))); - } - - return { success: true, review: updatedReview }; - }), - - getGroups: protectedProcedure - .query(async ({ ctx }) => { - const groups = await db.query.productGroupInfo.findMany({ - with: { - memberships: { - with: { - product: true, - }, - }, - }, - orderBy: desc(productGroupInfo.createdAt), - }); - - return { - groups: groups.map(group => ({ - ...group, - products: group.memberships.map(m => m.product), - productCount: group.memberships.length, - })), - }; - }), - - createGroup: protectedProcedure - .input(z.object({ - group_name: z.string().min(1), - description: z.string().optional(), - product_ids: z.array(z.number()).default([]), - })) - .mutation(async ({ input, ctx }) => { - const { group_name, description, product_ids } = input; - - const [newGroup] = await db - .insert(productGroupInfo) - .values({ - groupName: group_name, - description, - }) - .returning(); - - if (product_ids.length > 0) { - const memberships = product_ids.map(productId => ({ - productId, - groupId: newGroup.id, - })); - - await db.insert(productGroupMembership).values(memberships); - } - - // Reinitialize stores to reflect changes - scheduleStoreInitialization() - - return { - group: newGroup, - message: 'Group created successfully', - }; - }), - - updateGroup: protectedProcedure - .input(z.object({ - id: z.number(), - group_name: z.string().optional(), - description: z.string().optional(), - product_ids: z.array(z.number()).optional(), - })) - .mutation(async ({ input, ctx }) => { - const { id, group_name, description, product_ids } = input; - - const updateData: any = {}; - if (group_name !== undefined) updateData.groupName = group_name; - if (description !== undefined) updateData.description = description; - - const [updatedGroup] = await db - .update(productGroupInfo) - .set(updateData) - .where(eq(productGroupInfo.id, id)) - .returning(); - - if (!updatedGroup) { - throw new ApiError('Group not found', 404); - } - - if (product_ids !== undefined) { - // Delete existing memberships - await db.delete(productGroupMembership).where(eq(productGroupMembership.groupId, id)); - - // Insert new memberships - if (product_ids.length > 0) { - const memberships = product_ids.map(productId => ({ - productId, - groupId: id, - })); - - await db.insert(productGroupMembership).values(memberships); - } - } - - // Reinitialize stores to reflect changes - scheduleStoreInitialization() - - return { - group: updatedGroup, - message: 'Group updated successfully', - }; - }), - - deleteGroup: protectedProcedure - .input(z.object({ - id: z.number(), - })) - .mutation(async ({ input, ctx }) => { - const { id } = input; - - // Delete memberships first - await db.delete(productGroupMembership).where(eq(productGroupMembership.groupId, id)); - - // Delete group - const [deletedGroup] = await db - .delete(productGroupInfo) - .where(eq(productGroupInfo.id, id)) - .returning(); - - if (!deletedGroup) { - throw new ApiError('Group not found', 404); - } - - // Reinitialize stores to reflect changes - scheduleStoreInitialization() - - return { - message: 'Group deleted successfully', - }; - }), - - updateProductPrices: protectedProcedure - .input(z.object({ - updates: z.array(z.object({ - productId: z.number(), - price: z.number().optional(), - marketPrice: z.number().nullable().optional(), - flashPrice: z.number().nullable().optional(), - isFlashAvailable: z.boolean().optional(), - })), - })) - .mutation(async ({ input, ctx }) => { - const { updates } = input; - - if (updates.length === 0) { - throw new ApiError('No updates provided', 400); - } - - // Validate that all productIds exist - const productIds = updates.map(u => u.productId); - const existingProducts = await db.query.productInfo.findMany({ - where: inArray(productInfo.id, productIds), - columns: { id: true }, - }); - - const existingIds = new Set(existingProducts.map(p => p.id)); - const invalidIds = productIds.filter(id => !existingIds.has(id)); - - if (invalidIds.length > 0) { - throw new ApiError(`Invalid product IDs: ${invalidIds.join(', ')}`, 400); - } - - // Perform batch update - const updatePromises = updates.map(async (update) => { - const { productId, price, marketPrice, flashPrice, isFlashAvailable } = update; - const updateData: any = {}; - if (price !== undefined) updateData.price = price; - if (marketPrice !== undefined) updateData.marketPrice = marketPrice; - if (flashPrice !== undefined) updateData.flashPrice = flashPrice; - if (isFlashAvailable !== undefined) updateData.isFlashAvailable = isFlashAvailable; - - return db - .update(productInfo) - .set(updateData) - .where(eq(productInfo.id, productId)); - }); - - await Promise.all(updatePromises); - - // Reinitialize stores to reflect changes - scheduleStoreInitialization() - - return { - message: `Updated prices for ${updates.length} product(s)`, - updatedCount: updates.length, - }; - }), - }); diff --git a/verifier/admin-apis/apis/slots.ts b/verifier/admin-apis/apis/slots.ts deleted file mode 100644 index 1cc40e8..0000000 --- a/verifier/admin-apis/apis/slots.ts +++ /dev/null @@ -1,610 +0,0 @@ -import { router, protectedProcedure } from "@/src/trpc/trpc-index" -import { TRPCError } from "@trpc/server"; -import { z } from "zod"; -import { db } from "@/src/db/db_index" -import { deliverySlotInfo, productSlots, productInfo, vendorSnippets, productGroupInfo } from "@/src/db/schema" -import { eq, inArray, and, desc } from "drizzle-orm"; -import { ApiError } from "@/src/lib/api-error" -import { appUrl } from "@/src/lib/env-exporter" -import redisClient from "@/src/lib/redis-client" -import { getSlotSequenceKey } from "@/src/lib/redisKeyGetters" -import { scheduleStoreInitialization } from '@/src/stores/store-initializer' - - -interface CachedDeliverySequence { - [userId: string]: number[]; -} - -const cachedSequenceSchema = z.record(z.string(), z.array(z.number())); - -const createSlotSchema = z.object({ - deliveryTime: z.string(), - freezeTime: z.string(), - isActive: z.boolean().optional(), - productIds: z.array(z.number()).optional(), - vendorSnippets: z.array(z.object({ - name: z.string().min(1), - productIds: z.array(z.number().int().positive()).min(1), - validTill: z.string().optional(), - })).optional(), - groupIds: z.array(z.number()).optional(), -}); - -const getSlotByIdSchema = z.object({ - id: z.number(), -}); - -const updateSlotSchema = z.object({ - id: z.number(), - deliveryTime: z.string(), - freezeTime: z.string(), - isActive: z.boolean().optional(), - productIds: z.array(z.number()).optional(), - vendorSnippets: z.array(z.object({ - name: z.string().min(1), - productIds: z.array(z.number().int().positive()).min(1), - validTill: z.string().optional(), - })).optional(), - groupIds: z.array(z.number()).optional(), -}); - -const deleteSlotSchema = z.object({ - id: z.number(), -}); - -const getDeliverySequenceSchema = z.object({ - id: z.string(), -}); - -const updateDeliverySequenceSchema = z.object({ - id: z.number(), - // deliverySequence: z.array(z.number()), - deliverySequence: z.any(), -}); - -export const slotsRouter = router({ - // Exact replica of GET /av/slots - getAll: protectedProcedure.query(async ({ ctx }) => { - if (!ctx.staffUser?.id) { - throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); - } - - const slots = await db.query.deliverySlotInfo - .findMany({ - where: eq(deliverySlotInfo.isActive, true), - orderBy: desc(deliverySlotInfo.deliveryTime), - with: { - productSlots: { - with: { - product: { - columns: { - id: true, - name: true, - images: true, - }, - }, - }, - }, - }, - }) - .then((slots) => - slots.map((slot) => ({ - ...slot, - deliverySequence: slot.deliverySequence as number[], - products: slot.productSlots.map((ps) => ps.product), - })) - ); - - return { - slots, - count: slots.length, - }; - }), - - // Exact replica of POST /av/products/slots/product-ids - getSlotsProductIds: protectedProcedure - .input(z.object({ slotIds: z.array(z.number()) })) - .query(async ({ input, ctx }) => { - if (!ctx.staffUser?.id) { - throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); - } - - const { slotIds } = input; - - if (!Array.isArray(slotIds)) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "slotIds must be an array", - }); - } - - if (slotIds.length === 0) { - return {}; - } - - // Fetch all associations for the requested slots - const associations = await db.query.productSlots.findMany({ - where: inArray(productSlots.slotId, slotIds), - columns: { - slotId: true, - productId: true, - }, - }); - - // Group by slotId - const result = associations.reduce((acc, assoc) => { - if (!acc[assoc.slotId]) { - acc[assoc.slotId] = []; - } - acc[assoc.slotId].push(assoc.productId); - return acc; - }, {} as Record); - - // Ensure all requested slots have entries (even if empty) - slotIds.forEach((slotId) => { - if (!result[slotId]) { - result[slotId] = []; - } - }); - - return result; - }), - - // Exact replica of PUT /av/products/slots/:slotId/products - updateSlotProducts: protectedProcedure - .input( - z.object({ - slotId: z.number(), - productIds: z.array(z.number()), - }) - ) - .mutation(async ({ input, ctx }) => { - if (!ctx.staffUser?.id) { - throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); - } - - const { slotId, productIds } = input; - - if (!Array.isArray(productIds)) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "productIds must be an array", - }); - } - - // Get current associations - const currentAssociations = await db.query.productSlots.findMany({ - where: eq(productSlots.slotId, slotId), - columns: { - productId: true, - }, - }); - - const currentProductIds = currentAssociations.map( - (assoc) => assoc.productId - ); - const newProductIds = productIds; - - // Find products to add and remove - const productsToAdd = newProductIds.filter( - (id) => !currentProductIds.includes(id) - ); - const productsToRemove = currentProductIds.filter( - (id) => !newProductIds.includes(id) - ); - - // Remove associations for products that are no longer selected - if (productsToRemove.length > 0) { - await db - .delete(productSlots) - .where( - and( - eq(productSlots.slotId, slotId), - inArray(productSlots.productId, productsToRemove) - ) - ); - } - - // Add associations for newly selected products - if (productsToAdd.length > 0) { - const newAssociations = productsToAdd.map((productId) => ({ - productId, - slotId, - })); - - await db.insert(productSlots).values(newAssociations); - } - - // Reinitialize stores to reflect changes - scheduleStoreInitialization() - - return { - message: "Slot products updated successfully", - added: productsToAdd.length, - removed: productsToRemove.length, - }; - }), - - createSlot: protectedProcedure - .input(createSlotSchema) - .mutation(async ({ input, ctx }) => { - if (!ctx.staffUser?.id) { - throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); - } - - const { deliveryTime, freezeTime, isActive, productIds, vendorSnippets: snippets, groupIds } = input; - - // Validate required fields - if (!deliveryTime || !freezeTime) { - throw new ApiError("Delivery time and orders close time are required", 400); - } - - const result = await db.transaction(async (tx) => { - // Create slot - const [newSlot] = await tx - .insert(deliverySlotInfo) - .values({ - deliveryTime: new Date(deliveryTime), - freezeTime: new Date(freezeTime), - isActive: isActive !== undefined ? isActive : true, - groupIds: groupIds !== undefined ? groupIds : [], - }) - .returning(); - - // Insert product associations if provided - if (productIds && productIds.length > 0) { - const associations = productIds.map((productId) => ({ - productId, - slotId: newSlot.id, - })); - await tx.insert(productSlots).values(associations); - } - - // Create vendor snippets if provided - let createdSnippets: any[] = []; - if (snippets && snippets.length > 0) { - for (const snippet of snippets) { - // Validate products exist - const products = await tx.query.productInfo.findMany({ - where: inArray(productInfo.id, snippet.productIds), - }); - if (products.length !== snippet.productIds.length) { - throw new ApiError(`One or more invalid product IDs in snippet "${snippet.name}"`, 400); - } - - // Check if snippet name already exists - const existingSnippet = await tx.query.vendorSnippets.findFirst({ - where: eq(vendorSnippets.snippetCode, snippet.name), - }); - if (existingSnippet) { - throw new ApiError(`Snippet name "${snippet.name}" already exists`, 400); - } - - const [createdSnippet] = await tx.insert(vendorSnippets).values({ - snippetCode: snippet.name, - slotId: newSlot.id, - productIds: snippet.productIds, - validTill: snippet.validTill ? new Date(snippet.validTill) : undefined, - }).returning(); - - createdSnippets.push(createdSnippet); - } - } - - return { - slot: newSlot, - createdSnippets, - message: "Slot created successfully", - }; - }); - - // Reinitialize stores to reflect changes (outside transaction) - scheduleStoreInitialization() - - return result; - }), - - getSlots: protectedProcedure.query(async ({ ctx }) => { - if (!ctx.staffUser?.id) { - throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); - } - - const slots = await db.query.deliverySlotInfo.findMany({ - where: eq(deliverySlotInfo.isActive, true), - }); - - return { - slots, - count: slots.length, - }; - }), - - getSlotById: protectedProcedure - .input(getSlotByIdSchema) - .query(async ({ input, ctx }) => { - if (!ctx.staffUser?.id) { - throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); - } - - const { id } = input; - - const slot = await db.query.deliverySlotInfo.findFirst({ - where: eq(deliverySlotInfo.id, id), - with: { - productSlots: { - with: { - product: { - columns: { - id: true, - name: true, - images: true, - }, - }, - }, - }, - vendorSnippets: true, - }, - }); - - if (!slot) { - throw new ApiError("Slot not found", 404); - } - - return { - slot: { - ...slot, - deliverySequence: slot.deliverySequence as number[], - groupIds: slot.groupIds as number[], - products: slot.productSlots.map((ps) => ps.product), - vendorSnippets: slot.vendorSnippets?.map(snippet => ({ - ...snippet, - accessUrl: `${appUrl}/vendor-order-list?id=${snippet.snippetCode}` - })), - }, - }; - }), - - updateSlot: protectedProcedure - .input(updateSlotSchema) - .mutation(async ({ input, ctx }) => { - if (!ctx.staffUser?.id) { - throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); - } - try{ - const { id, deliveryTime, freezeTime, isActive, productIds, vendorSnippets: snippets, groupIds } = input; - - if (!deliveryTime || !freezeTime) { - throw new ApiError("Delivery time and orders close time are required", 400); - } - - // Filter groupIds to only include valid (existing) groups - let validGroupIds = groupIds; - if (groupIds && groupIds.length > 0) { - const existingGroups = await db.query.productGroupInfo.findMany({ - where: inArray(productGroupInfo.id, groupIds), - columns: { id: true }, - }); - validGroupIds = existingGroups.map(g => g.id); - } - - const result = await db.transaction(async (tx) => { - const [updatedSlot] = await tx - .update(deliverySlotInfo) - .set({ - deliveryTime: new Date(deliveryTime), - freezeTime: new Date(freezeTime), - isActive: isActive !== undefined ? isActive : true, - groupIds: validGroupIds !== undefined ? validGroupIds : [], - }) - .where(eq(deliverySlotInfo.id, id)) - .returning(); - - if (!updatedSlot) { - throw new ApiError("Slot not found", 404); - } - - // Update product associations - if (productIds !== undefined) { - // Delete existing associations - await tx.delete(productSlots).where(eq(productSlots.slotId, id)); - - // Insert new associations - if (productIds.length > 0) { - const associations = productIds.map((productId) => ({ - productId, - slotId: id, - })); - await tx.insert(productSlots).values(associations); - } - } - - // Create vendor snippets if provided - let createdSnippets: any[] = []; - if (snippets && snippets.length > 0) { - for (const snippet of snippets) { - // Validate products exist - const products = await tx.query.productInfo.findMany({ - where: inArray(productInfo.id, snippet.productIds), - }); - if (products.length !== snippet.productIds.length) { - throw new ApiError(`One or more invalid product IDs in snippet "${snippet.name}"`, 400); - } - - // Check if snippet name already exists - const existingSnippet = await tx.query.vendorSnippets.findFirst({ - where: eq(vendorSnippets.snippetCode, snippet.name), - }); - if (existingSnippet) { - throw new ApiError(`Snippet name "${snippet.name}" already exists`, 400); - } - - const [createdSnippet] = await tx.insert(vendorSnippets).values({ - snippetCode: snippet.name, - slotId: id, - productIds: snippet.productIds, - validTill: snippet.validTill ? new Date(snippet.validTill) : undefined, - - }).returning(); - - createdSnippets.push(createdSnippet); - } - } - - return { - slot: updatedSlot, - createdSnippets, - message: "Slot updated successfully", - }; - }); - - // Reinitialize stores to reflect changes (outside transaction) - scheduleStoreInitialization() - - return result; - } - catch(e) { - console.log(e) - throw new ApiError("Unable to Update Slot"); - } - }), - - deleteSlot: protectedProcedure - .input(deleteSlotSchema) - .mutation(async ({ input, ctx }) => { - if (!ctx.staffUser?.id) { - throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); - } - - const { id } = input; - - const [deletedSlot] = await db - .update(deliverySlotInfo) - .set({ isActive: false }) - .where(eq(deliverySlotInfo.id, id)) - .returning(); - - if (!deletedSlot) { - throw new ApiError("Slot not found", 404); - } - - // Reinitialize stores to reflect changes - scheduleStoreInitialization() - - return { - message: "Slot deleted successfully", - }; - }), - - getDeliverySequence: protectedProcedure - .input(getDeliverySequenceSchema) - .query(async ({ input, ctx }) => { - - const { id } = input; - const slotId = parseInt(id); - const cacheKey = getSlotSequenceKey(slotId); - - try { - const cached = await redisClient.get(cacheKey); - if (cached) { - const parsed = JSON.parse(cached); - const validated = cachedSequenceSchema.parse(parsed) as CachedDeliverySequence; - console.log('sending cached response') - - return { deliverySequence: validated }; - } - } catch (error) { - console.warn('Redis cache read/validation failed, falling back to DB:', error); - // Continue to DB fallback - } - - // Fallback to DB - const slot = await db.query.deliverySlotInfo.findFirst({ - where: eq(deliverySlotInfo.id, slotId), - }); - - if (!slot) { - throw new ApiError("Slot not found", 404); - } - - const sequence = (slot.deliverySequence || {}) as CachedDeliverySequence; - - // Cache the validated result - try { - const validated = cachedSequenceSchema.parse(sequence); - await redisClient.set(cacheKey, JSON.stringify(validated), 3600); - } catch (cacheError) { - console.warn('Redis cache write failed:', cacheError); - } - - return { deliverySequence: sequence }; - }), - - updateDeliverySequence: protectedProcedure - .input(updateDeliverySequenceSchema) - .mutation(async ({ input, ctx }) => { - if (!ctx.staffUser?.id) { - throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); - } - - const { id, deliverySequence } = input; - - const [updatedSlot] = await db - .update(deliverySlotInfo) - .set({ deliverySequence }) - .where(eq(deliverySlotInfo.id, id)) - .returning({ - id: deliverySlotInfo.id, - deliverySequence: deliverySlotInfo.deliverySequence, - }); - - if (!updatedSlot) { - throw new ApiError("Slot not found", 404); - } - - // Cache the updated sequence - const cacheKey = getSlotSequenceKey(id); - try { - const validated = cachedSequenceSchema.parse(deliverySequence); - await redisClient.set(cacheKey, JSON.stringify(validated), 3600); - } catch (cacheError) { - console.warn('Redis cache write failed:', cacheError); - } - - return { - slot: updatedSlot, - message: "Delivery sequence updated successfully", - }; - }), - - updateSlotCapacity: protectedProcedure - .input(z.object({ - slotId: z.number(), - isCapacityFull: z.boolean(), - })) - .mutation(async ({ input, ctx }) => { - if (!ctx.staffUser?.id) { - throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); - } - - const { slotId, isCapacityFull } = input; - - const [updatedSlot] = await db - .update(deliverySlotInfo) - .set({ isCapacityFull }) - .where(eq(deliverySlotInfo.id, slotId)) - .returning(); - - if (!updatedSlot) { - throw new ApiError("Slot not found", 404); - } - - // Reinitialize stores to reflect changes - scheduleStoreInitialization() - - return { - success: true, - slot: updatedSlot, - message: `Slot ${isCapacityFull ? 'marked as full capacity' : 'capacity reset'}`, - }; - }), -}); diff --git a/verifier/admin-apis/apis/staff-user.ts b/verifier/admin-apis/apis/staff-user.ts deleted file mode 100644 index 428f36e..0000000 --- a/verifier/admin-apis/apis/staff-user.ts +++ /dev/null @@ -1,242 +0,0 @@ -import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index' -import { z } from 'zod'; -import { db } from '@/src/db/db_index' -import { staffUsers, staffRoles, users, userDetails, orders } from '@/src/db/schema' -import { eq, or, ilike, and, lt, desc } from 'drizzle-orm'; -import bcrypt from 'bcryptjs'; -import { ApiError } from '@/src/lib/api-error' -import { signToken } from '@/src/lib/jwt-utils' - -export const staffUserRouter = router({ - login: publicProcedure - .input(z.object({ - name: z.string(), - password: z.string(), - })) - .mutation(async ({ input }) => { - const { name, password } = input; - - if (!name || !password) { - throw new ApiError('Name and password are required', 400); - } - - const staff = await db.query.staffUsers.findFirst({ - where: eq(staffUsers.name, name), - }); - - if (!staff) { - throw new ApiError('Invalid credentials', 401); - } - - const isPasswordValid = await bcrypt.compare(password, staff.password); - if (!isPasswordValid) { - throw new ApiError('Invalid credentials', 401); - } - - const token = await signToken( - { staffId: staff.id, name: staff.name }, - '30d' - ); - - return { - message: 'Login successful', - token, - staff: { id: staff.id, name: staff.name }, - }; - }), - - getStaff: protectedProcedure - .query(async ({ ctx }) => { - const staff = await db.query.staffUsers.findMany({ - columns: { - id: true, - name: true, - }, - with: { - role: { - with: { - rolePermissions: { - with: { - permission: true, - }, - }, - }, - }, - }, - }); - - // Transform the data to include role and permissions in a cleaner format - const transformedStaff = staff.map((user) => ({ - id: user.id, - name: user.name, - role: user.role ? { - id: user.role.id, - name: user.role.roleName, - } : null, - permissions: user.role?.rolePermissions.map((rp) => ({ - id: rp.permission.id, - name: rp.permission.permissionName, - })) || [], - })); - - return { - staff: transformedStaff, - }; - }), - - getUsers: protectedProcedure - .input(z.object({ - cursor: z.number().optional(), - limit: z.number().default(20), - search: z.string().optional(), - })) - .query(async ({ input }) => { - const { cursor, limit, search } = input; - - let whereCondition = undefined; - - if (search) { - whereCondition = or( - ilike(users.name, `%${search}%`), - ilike(users.email, `%${search}%`), - ilike(users.mobile, `%${search}%`) - ); - } - - if (cursor) { - const cursorCondition = lt(users.id, cursor); - whereCondition = whereCondition ? and(whereCondition, cursorCondition) : cursorCondition; - } - - const allUsers = await db.query.users.findMany({ - where: whereCondition, - with: { - userDetails: true, - }, - orderBy: desc(users.id), - limit: limit + 1, // fetch one extra to check if there's more - }); - - const hasMore = allUsers.length > limit; - const usersToReturn = hasMore ? allUsers.slice(0, limit) : allUsers; - - const formattedUsers = usersToReturn.map(user => ({ - id: user.id, - name: user.name, - email: user.email, - mobile: user.mobile, - image: user.userDetails?.profileImage || null, - })); - - return { - users: formattedUsers, - nextCursor: hasMore ? usersToReturn[usersToReturn.length - 1].id : undefined, - }; - }), - - getUserDetails: protectedProcedure - .input(z.object({ userId: z.number() })) - .query(async ({ input }) => { - const { userId } = input; - - const user = await db.query.users.findFirst({ - where: eq(users.id, userId), - with: { - userDetails: true, - orders: { - orderBy: desc(orders.createdAt), - limit: 1, - }, - }, - }); - - if (!user) { - throw new ApiError("User not found", 404); - } - - const lastOrder = user.orders[0]; - - return { - id: user.id, - name: user.name, - email: user.email, - mobile: user.mobile, - addedOn: user.createdAt, - lastOrdered: lastOrder?.createdAt || null, - isSuspended: user.userDetails?.isSuspended || false, - }; - }), - - updateUserSuspension: protectedProcedure - .input(z.object({ userId: z.number(), isSuspended: z.boolean() })) - .mutation(async ({ input }) => { - const { userId, isSuspended } = input; - - await db - .insert(userDetails) - .values({ userId, isSuspended }) - .onConflictDoUpdate({ - target: userDetails.userId, - set: { isSuspended }, - }); - - return { success: true }; - }), - - createStaffUser: protectedProcedure - .input(z.object({ - name: z.string().min(1, 'Name is required'), - password: z.string().min(6, 'Password must be at least 6 characters'), - roleId: z.number().int().positive('Role is required'), - })) - .mutation(async ({ input, ctx }) => { - const { name, password, roleId } = input; - - // Check if staff user already exists - const existingUser = await db.query.staffUsers.findFirst({ - where: eq(staffUsers.name, name), - }); - - if (existingUser) { - throw new ApiError('Staff user with this name already exists', 409); - } - - // Check if role exists - const role = await db.query.staffRoles.findFirst({ - where: eq(staffRoles.id, roleId), - }); - - if (!role) { - throw new ApiError('Invalid role selected', 400); - } - - // Hash password - const hashedPassword = await bcrypt.hash(password, 12); - - // Create staff user - const [newUser] = await db.insert(staffUsers).values({ - name: name.trim(), - password: hashedPassword, - staffRoleId: roleId, - }).returning(); - - return { success: true, user: { id: newUser.id, name: newUser.name } }; - }), - - getRoles: protectedProcedure - .query(async ({ ctx }) => { - const roles = await db.query.staffRoles.findMany({ - columns: { - id: true, - roleName: true, - }, - }); - - return { - roles: roles.map(role => ({ - id: role.id, - name: role.roleName, - })), - }; - }), -}); \ No newline at end of file diff --git a/verifier/admin-apis/apis/store.ts b/verifier/admin-apis/apis/store.ts deleted file mode 100644 index 5925843..0000000 --- a/verifier/admin-apis/apis/store.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { router, protectedProcedure } from '@/src/trpc/trpc-index' -import { z } from 'zod'; -import { db } from '@/src/db/db_index' -import { storeInfo, productInfo } from '@/src/db/schema' -import { eq, inArray } from 'drizzle-orm'; -import { ApiError } from '@/src/lib/api-error' - import { extractKeyFromPresignedUrl, deleteImageUtil, scaffoldAssetUrl } from '@/src/lib/s3-client' -import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; -import { scheduleStoreInitialization } from '@/src/stores/store-initializer' - - -export const storeRouter = router({ - getStores: protectedProcedure - .query(async ({ ctx }) => { - const stores = await db.query.storeInfo.findMany({ - with: { - owner: true, - }, - }); - - Promise.all(stores.map(async store => { - if(store.imageUrl) - store.imageUrl = scaffoldAssetUrl(store.imageUrl) - })).catch((e) => { - throw new ApiError("Unable to find store image urls") - } - ) - return { - stores, - count: stores.length, - }; - }), - - getStoreById: protectedProcedure - .input(z.object({ - id: z.number(), - })) - .query(async ({ input, ctx }) => { - const { id } = input; - - const store = await db.query.storeInfo.findFirst({ - where: eq(storeInfo.id, id), - with: { - owner: true, - }, - }); - - if (!store) { - throw new ApiError("Store not found", 404); - } - store.imageUrl = scaffoldAssetUrl(store.imageUrl); - return { - store, - }; - }), - - createStore: protectedProcedure - .input(z.object({ - name: z.string().min(1, "Name is required"), - description: z.string().optional(), - imageUrl: z.string().optional(), - owner: z.number().min(1, "Owner is required"), - products: z.array(z.number()).optional(), - })) - .mutation(async ({ input, ctx }) => { - const { name, description, imageUrl, owner, products } = input; - - // const imageKey = imageUrl ? extractKeyFromPresignedUrl(imageUrl) : undefined; - const imageKey = imageUrl - - const [newStore] = await db - .insert(storeInfo) - .values({ - name, - description, - imageUrl: imageKey, - owner, - }) - .returning(); - - // Assign selected products to this store - if (products && products.length > 0) { - await db - .update(productInfo) - .set({ storeId: newStore.id }) - .where(inArray(productInfo.id, products)); - } - - // Reinitialize stores to reflect changes - scheduleStoreInitialization() - - return { - store: newStore, - message: "Store created successfully", - }; - }), - - updateStore: protectedProcedure - .input(z.object({ - id: z.number(), - name: z.string().min(1, "Name is required"), - description: z.string().optional(), - imageUrl: z.string().optional(), - owner: z.number().min(1, "Owner is required"), - products: z.array(z.number()).optional(), - })) - .mutation(async ({ input, ctx }) => { - const { id, name, description, imageUrl, owner, products } = input; - - const existingStore = await db.query.storeInfo.findFirst({ - where: eq(storeInfo.id, id), - }); - - if (!existingStore) { - throw new ApiError("Store not found", 404); - } - - const oldImageKey = existingStore.imageUrl; - const newImageKey = imageUrl ? extractKeyFromPresignedUrl(imageUrl) : oldImageKey; - - // Delete old image only if: - // 1. New image provided and keys are different, OR - // 2. No new image but old exists (clearing the image) - if (oldImageKey && ( - (newImageKey && newImageKey !== oldImageKey) || - (!newImageKey) - )) { - try { - await deleteImageUtil({keys: [oldImageKey]}); - } catch (error) { - console.error('Failed to delete old image:', error); - // Continue with update even if deletion fails - } - } - - const [updatedStore] = await db - .update(storeInfo) - .set({ - name, - description, - imageUrl: newImageKey, - owner, - }) - .where(eq(storeInfo.id, id)) - .returning(); - - if (!updatedStore) { - throw new ApiError("Store not found", 404); - } - - // Update products if provided - if (products) { - // First, set storeId to null for products not in the list but currently assigned to this store - await db - .update(productInfo) - .set({ storeId: null }) - .where(eq(productInfo.storeId, id)); - - // Then, assign the selected products to this store - if (products.length > 0) { - await db - .update(productInfo) - .set({ storeId: id }) - .where(inArray(productInfo.id, products)); - } - } - - // Reinitialize stores to reflect changes - scheduleStoreInitialization() - - return { - store: updatedStore, - message: "Store updated successfully", - }; - }), - - deleteStore: protectedProcedure - .input(z.object({ - storeId: z.number(), - })) - .mutation(async ({ input, ctx }) => { - const { storeId } = input; - - const result = await db.transaction(async (tx) => { - // First, update all products of this store to set storeId to null - await tx - .update(productInfo) - .set({ storeId: null }) - .where(eq(productInfo.storeId, storeId)); - - // Then delete the store - const [deletedStore] = await tx - .delete(storeInfo) - .where(eq(storeInfo.id, storeId)) - .returning(); - - if (!deletedStore) { - throw new ApiError("Store not found", 404); - } - - return { - message: "Store deleted successfully", - }; - }); - - // Reinitialize stores to reflect changes (outside transaction) - scheduleStoreInitialization() - - return result; - }), - }); diff --git a/verifier/admin-apis/apis/tag.ts b/verifier/admin-apis/apis/tag.ts deleted file mode 100644 index 66dac83..0000000 --- a/verifier/admin-apis/apis/tag.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { router, protectedProcedure } from '@/src/trpc/trpc-index' -import { z } from 'zod'; -import { db } from '@/src/db/db_index' -import { productTagInfo } from '@/src/db/schema' -import { eq } from 'drizzle-orm'; -import { ApiError } from '@/src/lib/api-error' -import { scaffoldAssetUrl, claimUploadUrl } from '@/src/lib/s3-client' -import { deleteS3Image } from '@/src/lib/delete-image' -import { scheduleStoreInitialization } from '@/src/stores/store-initializer' - -export const tagRouter = router({ - getTags: protectedProcedure - .query(async () => { - const tags = await db - .select() - .from(productTagInfo) - .orderBy(productTagInfo.tagName); - - // Generate asset URLs for tag images - const tagsWithUrls = tags.map(tag => ({ - ...tag, - imageUrl: tag.imageUrl ? scaffoldAssetUrl(tag.imageUrl) : null, - })); - - return { - tags: tagsWithUrls, - message: "Tags retrieved successfully", - }; - }), - - getTagById: protectedProcedure - .input(z.object({ - id: z.number(), - })) - .query(async ({ input }) => { - const tag = await db.query.productTagInfo.findFirst({ - where: eq(productTagInfo.id, input.id), - }); - - if (!tag) { - throw new ApiError("Tag not found", 404); - } - - // Generate asset URL for tag image - const tagWithUrl = { - ...tag, - imageUrl: tag.imageUrl ? scaffoldAssetUrl(tag.imageUrl) : null, - }; - - return { - tag: tagWithUrl, - message: "Tag retrieved successfully", - }; - }), - - createTag: protectedProcedure - .input(z.object({ - tagName: z.string().min(1), - tagDescription: z.string().optional(), - isDashboardTag: z.boolean().default(false), - relatedStores: z.array(z.number()).default([]), - imageKey: z.string().optional(), - })) - .mutation(async ({ input }) => { - const { tagName, tagDescription, isDashboardTag, relatedStores, imageKey } = input; - - // Check for duplicate tag name - const existingTag = await db.query.productTagInfo.findFirst({ - where: eq(productTagInfo.tagName, tagName.trim()), - }); - - if (existingTag) { - throw new ApiError("A tag with this name already exists", 400); - } - - const [newTag] = await db - .insert(productTagInfo) - .values({ - tagName: tagName.trim(), - tagDescription, - imageUrl: imageKey || null, - isDashboardTag, - relatedStores, - }) - .returning(); - - // Claim upload URL if image was provided - if (imageKey) { - try { - await claimUploadUrl(imageKey); - } catch (e) { - console.warn(`Failed to claim upload URL for key: ${imageKey}`, e); - } - } - - scheduleStoreInitialization(); - - return { - tag: newTag, - message: "Tag created successfully", - }; - }), - - updateTag: protectedProcedure - .input(z.object({ - id: z.number(), - tagName: z.string().min(1), - tagDescription: z.string().optional(), - isDashboardTag: z.boolean(), - relatedStores: z.array(z.number()), - imageKey: z.string().optional(), - deleteExistingImage: z.boolean().optional(), - })) - .mutation(async ({ input }) => { - const { id, imageKey, deleteExistingImage, ...updateData } = input; - - // Get current tag - const currentTag = await db.query.productTagInfo.findFirst({ - where: eq(productTagInfo.id, id), - }); - - if (!currentTag) { - throw new ApiError("Tag not found", 404); - } - - let newImageUrl = currentTag.imageUrl; - - // Handle image deletion - if (deleteExistingImage && currentTag.imageUrl) { - try { - await deleteS3Image(currentTag.imageUrl); - } catch (e) { - console.error(`Failed to delete old image: ${currentTag.imageUrl}`, e); - } - newImageUrl = null; - } - - // Handle new image upload (only if different from existing) - if (imageKey && imageKey !== currentTag.imageUrl) { - // Delete old image if exists and not already deleted - if (currentTag.imageUrl && !deleteExistingImage) { - try { - await deleteS3Image(currentTag.imageUrl); - } catch (e) { - console.error(`Failed to delete old image: ${currentTag.imageUrl}`, e); - } - } - newImageUrl = imageKey; - - // Claim upload URL - try { - await claimUploadUrl(imageKey); - } catch (e) { - console.warn(`Failed to claim upload URL for key: ${imageKey}`, e); - } - } - - const [updatedTag] = await db - .update(productTagInfo) - .set({ - tagName: updateData.tagName.trim(), - tagDescription: updateData.tagDescription, - isDashboardTag: updateData.isDashboardTag, - relatedStores: updateData.relatedStores, - imageUrl: newImageUrl, - }) - .where(eq(productTagInfo.id, id)) - .returning(); - - scheduleStoreInitialization(); - - return { - tag: updatedTag, - message: "Tag updated successfully", - }; - }), - - deleteTag: protectedProcedure - .input(z.object({ - id: z.number(), - })) - .mutation(async ({ input }) => { - const { id } = input; - - // Get tag to check for image - const tag = await db.query.productTagInfo.findFirst({ - where: eq(productTagInfo.id, id), - }); - - if (!tag) { - throw new ApiError("Tag not found", 404); - } - - // Delete image from S3 if exists - if (tag.imageUrl) { - try { - await deleteS3Image(tag.imageUrl); - } catch (e) { - console.error(`Failed to delete image: ${tag.imageUrl}`, e); - } - } - - // Delete tag (will fail if tag is assigned to products due to FK constraint) - await db.delete(productTagInfo).where(eq(productTagInfo.id, id)); - - scheduleStoreInitialization(); - - return { - message: "Tag deleted successfully", - }; - }), -}); - -export type TagRouter = typeof tagRouter; diff --git a/verifier/admin-apis/apis/user.ts b/verifier/admin-apis/apis/user.ts deleted file mode 100644 index 2b176b2..0000000 --- a/verifier/admin-apis/apis/user.ts +++ /dev/null @@ -1,489 +0,0 @@ -import { protectedProcedure } from '@/src/trpc/trpc-index'; -import { z } from 'zod'; -import { db } from '@/src/db/db_index'; -import { users, complaints, orders, orderItems, notifCreds, unloggedUserTokens, userDetails, userIncidents } from '@/src/db/schema'; -import { eq, sql, desc, asc, count, max, inArray } from 'drizzle-orm'; -import { ApiError } from '@/src/lib/api-error'; -import { notificationQueue } from '@/src/lib/notif-job'; -import { recomputeUserNegativityScore } from '@/src/stores/user-negativity-store'; - -async function createUserByMobile(mobile: string): Promise { - // Clean mobile number (remove non-digits) - const cleanMobile = mobile.replace(/\D/g, ''); - - // Validate: exactly 10 digits - if (cleanMobile.length !== 10) { - throw new ApiError('Mobile number must be exactly 10 digits', 400); - } - - // Check if user already exists - const [existingUser] = await db - .select() - .from(users) - .where(eq(users.mobile, cleanMobile)) - .limit(1); - - if (existingUser) { - throw new ApiError('User with this mobile number already exists', 409); - } - - // Create user - const [newUser] = await db - .insert(users) - .values({ - name: null, - email: null, - mobile: cleanMobile, - }) - .returning(); - - return newUser; -} - -export const userRouter = { - createUserByMobile: protectedProcedure - .input(z.object({ - mobile: z.string().min(1, 'Mobile number is required'), - })) - .mutation(async ({ input }) => { - const newUser = await createUserByMobile(input.mobile); - - return { - success: true, - data: newUser, - }; - }), - - getEssentials: protectedProcedure - .query(async () => { - const count = await db.$count(complaints, eq(complaints.isResolved, false)); - - return { - unresolvedComplaints: count || 0, - }; - }), - - getAllUsers: protectedProcedure - .input(z.object({ - limit: z.number().min(1).max(100).default(50), - cursor: z.number().optional(), - search: z.string().optional(), - })) - .query(async ({ input }) => { - const { limit, cursor, search } = input; - - // Build where conditions - const whereConditions = []; - - if (search && search.trim()) { - whereConditions.push(sql`${users.mobile} ILIKE ${`%${search.trim()}%`}`); - } - - if (cursor) { - whereConditions.push(sql`${users.id} > ${cursor}`); - } - - // Get users with filters applied - const usersList = await db - .select({ - id: users.id, - name: users.name, - mobile: users.mobile, - createdAt: users.createdAt, - }) - .from(users) - .where(whereConditions.length > 0 ? sql.join(whereConditions, sql` AND `) : undefined) - .orderBy(asc(users.id)) - .limit(limit + 1); // Get one extra to determine if there's more - - // Check if there are more results - const hasMore = usersList.length > limit; - const usersToReturn = hasMore ? usersList.slice(0, limit) : usersList; - - // Get order stats for each user - const userIds = usersToReturn.map(u => u.id); - - let orderCounts: { userId: number; totalOrders: number }[] = []; - let lastOrders: { userId: number; lastOrderDate: Date | null }[] = []; - let suspensionStatuses: { userId: number; isSuspended: boolean }[] = []; - - if (userIds.length > 0) { - // Get total orders per user - orderCounts = await db - .select({ - userId: orders.userId, - totalOrders: count(orders.id), - }) - .from(orders) - .where(sql`${orders.userId} IN (${sql.join(userIds, sql`, `)})`) - .groupBy(orders.userId); - - // Get last order date per user - lastOrders = await db - .select({ - userId: orders.userId, - lastOrderDate: max(orders.createdAt), - }) - .from(orders) - .where(sql`${orders.userId} IN (${sql.join(userIds, sql`, `)})`) - .groupBy(orders.userId); - - // Get suspension status for each user - suspensionStatuses = await db - .select({ - userId: userDetails.userId, - isSuspended: userDetails.isSuspended, - }) - .from(userDetails) - .where(sql`${userDetails.userId} IN (${sql.join(userIds, sql`, `)})`); - } - - // Create lookup maps - const orderCountMap = new Map(orderCounts.map(o => [o.userId, o.totalOrders])); - const lastOrderMap = new Map(lastOrders.map(o => [o.userId, o.lastOrderDate])); - const suspensionMap = new Map(suspensionStatuses.map(s => [s.userId, s.isSuspended])); - - // Combine data - const usersWithStats = usersToReturn.map(user => ({ - ...user, - totalOrders: orderCountMap.get(user.id) || 0, - lastOrderDate: lastOrderMap.get(user.id) || null, - isSuspended: suspensionMap.get(user.id) ?? false, - })); - - // Get next cursor - const nextCursor = hasMore ? usersToReturn[usersToReturn.length - 1].id : undefined; - - return { - users: usersWithStats, - nextCursor, - hasMore, - }; - }), - - getUserDetails: protectedProcedure - .input(z.object({ - userId: z.number(), - })) - .query(async ({ input }) => { - const { userId } = input; - - // Get user info - const user = await db - .select({ - id: users.id, - name: users.name, - mobile: users.mobile, - createdAt: users.createdAt, - }) - .from(users) - .where(eq(users.id, userId)) - .limit(1); - - if (!user || user.length === 0) { - throw new ApiError('User not found', 404); - } - - // Get user suspension status - const userDetail = await db - .select({ - isSuspended: userDetails.isSuspended, - }) - .from(userDetails) - .where(eq(userDetails.userId, userId)) - .limit(1); - - // Get all orders for this user with order items count - const userOrders = await db - .select({ - id: orders.id, - readableId: orders.readableId, - totalAmount: orders.totalAmount, - createdAt: orders.createdAt, - isFlashDelivery: orders.isFlashDelivery, - }) - .from(orders) - .where(eq(orders.userId, userId)) - .orderBy(desc(orders.createdAt)); - - // Get order status for each order - const orderIds = userOrders.map(o => o.id); - - let orderStatuses: { orderId: number; isDelivered: boolean; isCancelled: boolean }[] = []; - - if (orderIds.length > 0) { - const { orderStatus } = await import('@/src/db/schema'); - orderStatuses = await db - .select({ - orderId: orderStatus.orderId, - isDelivered: orderStatus.isDelivered, - isCancelled: orderStatus.isCancelled, - }) - .from(orderStatus) - .where(sql`${orderStatus.orderId} IN (${sql.join(orderIds, sql`, `)})`); - } - - // Get item counts for each order - const itemCounts = await db - .select({ - orderId: orderItems.orderId, - itemCount: count(orderItems.id), - }) - .from(orderItems) - .where(sql`${orderItems.orderId} IN (${sql.join(orderIds, sql`, `)})`) - .groupBy(orderItems.orderId); - - // Create lookup maps - const statusMap = new Map(orderStatuses.map(s => [s.orderId, s])); - const itemCountMap = new Map(itemCounts.map(c => [c.orderId, c.itemCount])); - - // Determine status string - const getStatus = (status: { isDelivered: boolean; isCancelled: boolean } | undefined) => { - if (!status) return 'pending'; - if (status.isCancelled) return 'cancelled'; - if (status.isDelivered) return 'delivered'; - return 'pending'; - }; - - // Combine data - const ordersWithDetails = userOrders.map(order => { - const status = statusMap.get(order.id); - return { - id: order.id, - readableId: order.readableId, - totalAmount: order.totalAmount, - createdAt: order.createdAt, - isFlashDelivery: order.isFlashDelivery, - status: getStatus(status), - itemCount: itemCountMap.get(order.id) || 0, - }; - }); - - return { - user: { - ...user[0], - isSuspended: userDetail[0]?.isSuspended ?? false, - }, - orders: ordersWithDetails, - }; - }), - - updateUserSuspension: protectedProcedure - .input(z.object({ - userId: z.number(), - isSuspended: z.boolean(), - })) - .mutation(async ({ input }) => { - const { userId, isSuspended } = input; - - // Check if user exists - const user = await db - .select({ id: users.id }) - .from(users) - .where(eq(users.id, userId)) - .limit(1); - - if (!user || user.length === 0) { - throw new ApiError('User not found', 404); - } - - // Check if user_details record exists - const existingDetail = await db - .select({ id: userDetails.id }) - .from(userDetails) - .where(eq(userDetails.userId, userId)) - .limit(1); - - if (existingDetail.length > 0) { - // Update existing record - await db - .update(userDetails) - .set({ isSuspended }) - .where(eq(userDetails.userId, userId)); - } else { - // Insert new record - await db - .insert(userDetails) - .values({ - userId, - isSuspended, - }); - } - - return { - success: true, - message: `User ${isSuspended ? 'suspended' : 'unsuspended'} successfully`, - }; - }), - - getUsersForNotification: protectedProcedure - .input(z.object({ - search: z.string().optional(), - })) - .query(async ({ input }) => { - const { search } = input; - - // Get all users - let usersList; - if (search && search.trim()) { - usersList = await db - .select({ - id: users.id, - name: users.name, - mobile: users.mobile, - }) - .from(users) - .where(sql`${users.mobile} ILIKE ${`%${search.trim()}%`} OR ${users.name} ILIKE ${`%${search.trim()}%`}`); - } else { - usersList = await db - .select({ - id: users.id, - name: users.name, - mobile: users.mobile, - }) - .from(users); - } - - // Get eligible users (have notif_creds entry) - const eligibleUsers = await db - .select({ userId: notifCreds.userId }) - .from(notifCreds); - - const eligibleSet = new Set(eligibleUsers.map(u => u.userId)); - - return { - users: usersList.map(user => ({ - id: user.id, - name: user.name, - mobile: user.mobile, - isEligibleForNotif: eligibleSet.has(user.id), - })), - }; - }), - - sendNotification: protectedProcedure - .input(z.object({ - userIds: z.array(z.number()).default([]), - title: z.string().min(1, 'Title is required'), - text: z.string().min(1, 'Message is required'), - imageUrl: z.string().optional(), - })) - .mutation(async ({ input }) => { - const { userIds, title, text, imageUrl } = input; - - let tokens: string[] = []; - - if (userIds.length === 0) { - // Send to all users - get tokens from both logged-in and unlogged users - const loggedInTokens = await db.select({ token: notifCreds.token }).from(notifCreds); - const unloggedTokens = await db.select({ token: unloggedUserTokens.token }).from(unloggedUserTokens); - - tokens = [ - ...loggedInTokens.map(t => t.token), - ...unloggedTokens.map(t => t.token) - ]; - } else { - // Send to specific users - get their tokens - const userTokens = await db - .select({ token: notifCreds.token }) - .from(notifCreds) - .where(inArray(notifCreds.userId, userIds)); - - tokens = userTokens.map(t => t.token); - } - - // Queue one job per token - let queuedCount = 0; - for (const token of tokens) { - try { - await notificationQueue.add('send-admin-notification', { - token, - title, - body: text, - imageUrl: imageUrl || null, - }, { - attempts: 3, - backoff: { - type: 'exponential', - delay: 2000, - }, - }); - queuedCount++; - } catch (error) { - console.error(`Failed to queue notification for token:`, error); - } - } - - return { - success: true, - message: `Notification queued for ${queuedCount} users`, - }; - }), - - getUserIncidents: protectedProcedure - .input(z.object({ - userId: z.number(), - })) - .query(async ({ input }) => { - const { userId } = input; - - const incidents = await db.query.userIncidents.findMany({ - where: eq(userIncidents.userId, userId), - with: { - order: { - with: { - orderStatus: true, - }, - }, - addedBy: true, - }, - orderBy: desc(userIncidents.dateAdded), - }); - - return { - incidents: incidents.map(incident => ({ - id: incident.id, - userId: incident.userId, - orderId: incident.orderId, - dateAdded: incident.dateAdded, - adminComment: incident.adminComment, - addedBy: incident.addedBy?.name || 'Unknown', - negativityScore: incident.negativityScore, - orderStatus: incident.order?.orderStatus?.[0]?.isCancelled ? 'cancelled' : 'active', - })), - }; - }), - - addUserIncident: protectedProcedure - .input(z.object({ - userId: z.number(), - orderId: z.number().optional(), - adminComment: z.string().optional(), - negativityScore: z.number().optional(), - })) - .mutation(async ({ input, ctx }) => { - const { userId, orderId, adminComment, negativityScore } = input; - - const adminUserId = ctx.staffUser?.id; - - if (!adminUserId) { - throw new ApiError('Admin user not authenticated', 401); - } - - - const incidentObj = { userId, orderId, adminComment, addedBy: adminUserId, negativityScore }; - - const [incident] = await db.insert(userIncidents) - .values({ - ...incidentObj, - }) - .returning(); - - recomputeUserNegativityScore(userId); - - return { - success: true, - data: incident, - }; - }), -}; diff --git a/verifier/admin-apis/apis/vendor-snippets.ts b/verifier/admin-apis/apis/vendor-snippets.ts deleted file mode 100644 index 4a307d3..0000000 --- a/verifier/admin-apis/apis/vendor-snippets.ts +++ /dev/null @@ -1,531 +0,0 @@ -import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index' -import { z } from 'zod'; -import dayjs from 'dayjs'; -import { db } from '@/src/db/db_index' -import { vendorSnippets, deliverySlotInfo, productInfo, orders, orderItems, users, orderStatus } from '@/src/db/schema' -import { eq, and, inArray, isNotNull, gt, sql, asc, ne } from 'drizzle-orm'; -import { appUrl } from '@/src/lib/env-exporter' - -const createSnippetSchema = z.object({ - snippetCode: z.string().min(1, "Snippet code is required"), - slotId: z.number().optional(), - productIds: z.array(z.number().int().positive()).min(1, "At least one product is required"), - validTill: z.string().optional(), - isPermanent: z.boolean().default(false) -}); - -const updateSnippetSchema = z.object({ - id: z.number().int().positive(), - updates: createSnippetSchema.partial().extend({ - snippetCode: z.string().min(1).optional(), - productIds: z.array(z.number().int().positive()).optional(), - isPermanent: z.boolean().default(false) - }), -}); - -export const vendorSnippetsRouter = router({ - create: protectedProcedure - .input(createSnippetSchema) - .mutation(async ({ input, ctx }) => { - const { snippetCode, slotId, productIds, validTill, isPermanent } = input; - - // Get staff user ID from auth middleware - const staffUserId = ctx.staffUser?.id; - if (!staffUserId) { - throw new Error("Unauthorized"); - } - - // Validate slot exists - if(slotId) { - const slot = await db.query.deliverySlotInfo.findFirst({ - where: eq(deliverySlotInfo.id, slotId), - }); - if (!slot) { - throw new Error("Invalid slot ID"); - } - } - - // Validate products exist - const products = await db.query.productInfo.findMany({ - where: inArray(productInfo.id, productIds), - }); - if (products.length !== productIds.length) { - throw new Error("One or more invalid product IDs"); - } - - // Check if snippet code already exists - const existingSnippet = await db.query.vendorSnippets.findFirst({ - where: eq(vendorSnippets.snippetCode, snippetCode), - }); - if (existingSnippet) { - throw new Error("Snippet code already exists"); - } - - const result = await db.insert(vendorSnippets).values({ - snippetCode, - slotId, - productIds, - isPermanent, - validTill: validTill ? new Date(validTill) : undefined, - }).returning(); - - return result[0]; - }), - - getAll: protectedProcedure - .query(async () => { - console.log('from the vendor snipptes methods') - - try { - const result = await db.query.vendorSnippets.findMany({ - with: { - slot: true, - }, - orderBy: (vendorSnippets, { desc }) => [desc(vendorSnippets.createdAt)], - }); - - const snippetsWithProducts = await Promise.all( - result.map(async (snippet) => { - const products = await db.query.productInfo.findMany({ - where: inArray(productInfo.id, snippet.productIds), - columns: { id: true, name: true }, - }); - - return { - ...snippet, - accessUrl: `${appUrl}/vendor-order-list?id=${snippet.snippetCode}`, - products: products.map(p => ({ id: p.id, name: p.name })), - }; - }) - ); - - return snippetsWithProducts; - } - catch(e) { - console.log(e) - } - return []; - }), - - getById: protectedProcedure - .input(z.object({ id: z.number().int().positive() })) - .query(async ({ input }) => { - const { id } = input; - - const result = await db.query.vendorSnippets.findFirst({ - where: eq(vendorSnippets.id, id), - with: { - slot: true, - }, - }); - - if (!result) { - throw new Error("Vendor snippet not found"); - } - - return result; - }), - - update: protectedProcedure - .input(updateSnippetSchema) - .mutation(async ({ input }) => { - const { id, updates } = input; - - // Check if snippet exists - const existingSnippet = await db.query.vendorSnippets.findFirst({ - where: eq(vendorSnippets.id, id), - }); - if (!existingSnippet) { - throw new Error("Vendor snippet not found"); - } - - // Validate slot if being updated - if (updates.slotId) { - const slot = await db.query.deliverySlotInfo.findFirst({ - where: eq(deliverySlotInfo.id, updates.slotId), - }); - if (!slot) { - throw new Error("Invalid slot ID"); - } - } - - // Validate products if being updated - if (updates.productIds) { - const products = await db.query.productInfo.findMany({ - where: inArray(productInfo.id, updates.productIds), - }); - if (products.length !== updates.productIds.length) { - throw new Error("One or more invalid product IDs"); - } - } - - // Check snippet code uniqueness if being updated - if (updates.snippetCode && updates.snippetCode !== existingSnippet.snippetCode) { - const duplicateSnippet = await db.query.vendorSnippets.findFirst({ - where: eq(vendorSnippets.snippetCode, updates.snippetCode), - }); - if (duplicateSnippet) { - throw new Error("Snippet code already exists"); - } - } - - const updateData: any = { ...updates }; - if (updates.validTill !== undefined) { - updateData.validTill = updates.validTill ? new Date(updates.validTill) : null; - } - - const result = await db.update(vendorSnippets) - .set(updateData) - .where(eq(vendorSnippets.id, id)) - .returning(); - - if (result.length === 0) { - throw new Error("Failed to update vendor snippet"); - } - - return result[0]; - }), - - delete: protectedProcedure - .input(z.object({ id: z.number().int().positive() })) - .mutation(async ({ input }) => { - const { id } = input; - - const result = await db.delete(vendorSnippets) - .where(eq(vendorSnippets.id, id)) - .returning(); - - if (result.length === 0) { - throw new Error("Vendor snippet not found"); - } - - return { message: "Vendor snippet deleted successfully" }; - }), - - getOrdersBySnippet: publicProcedure - .input(z.object({ - snippetCode: z.string().min(1, "Snippet code is required") - })) - .query(async ({ input }) => { - const { snippetCode } = input; - - // Find the snippet - const snippet = await db.query.vendorSnippets.findFirst({ - where: eq(vendorSnippets.snippetCode, snippetCode), - }); - - if (!snippet) { - throw new Error("Vendor snippet not found"); - } - - // Check if snippet is still valid - if (snippet.validTill && new Date(snippet.validTill) < new Date()) { - throw new Error("Vendor snippet has expired"); - } - - // Query orders that match the snippet criteria - const matchingOrders = await db.query.orders.findMany({ - where: eq(orders.slotId, snippet.slotId!), - with: { - orderItems: { - with: { - product: { - with: { - unit: true, - }, - }, - }, - }, - orderStatus: true, - user: true, - slot: true, - }, - orderBy: (orders, { desc }) => [desc(orders.createdAt)], - }); - - // Filter orders that contain at least one of the snippet's products - const filteredOrders = matchingOrders.filter(order => { - const status = order.orderStatus; - if (status[0].isCancelled) return false; - const orderProductIds = order.orderItems.map(item => item.productId); - return snippet.productIds.some(productId => orderProductIds.includes(productId)); - }); - - // Format the response - const formattedOrders = filteredOrders.map(order => { - // Filter orderItems to only include products attached to the snippet - const attachedOrderItems = order.orderItems.filter(item => - snippet.productIds.includes(item.productId) - ); - - const products = attachedOrderItems.map(item => ({ - orderItemId: item.id, - productId: item.productId, - productName: item.product.name, - quantity: parseFloat(item.quantity), - productSize: item.product.productQuantity, - price: parseFloat(item.price.toString()), - unit: item.product.unit?.shortNotation || 'unit', - subtotal: parseFloat(item.price.toString()) * parseFloat(item.quantity), - is_packaged: item.is_packaged, - is_package_verified: item.is_package_verified, - })); - - const orderTotal = products.reduce((sum, p) => sum + p.subtotal, 0); - - return { - orderId: `ORD${order.id}`, - orderDate: order.createdAt.toISOString(), - customerName: order.user.name, - totalAmount: orderTotal, - slotInfo: order.slot ? { - time: order.slot.deliveryTime.toISOString(), - sequence: order.slot.deliverySequence, - } : null, - products, - matchedProducts: snippet.productIds, // All snippet products are considered matched - snippetCode: snippet.snippetCode, - }; - }); - - return { - success: true, - data: formattedOrders, - snippet: { - id: snippet.id, - snippetCode: snippet.snippetCode, - slotId: snippet.slotId, - productIds: snippet.productIds, - validTill: snippet.validTill?.toISOString(), - createdAt: snippet.createdAt.toISOString(), - isPermanent: snippet.isPermanent, - }, - }; - }), - - getVendorOrders: protectedProcedure - .query(async () => { - const vendorOrders = await db.query.orders.findMany({ - with: { - user: true, - orderItems: { - with: { - product: { - with: { - unit: true, - }, - }, - }, - }, - }, - orderBy: (orders, { desc }) => [desc(orders.createdAt)], - }); - - return vendorOrders.map(order => ({ - id: order.id, - status: 'pending', // Default status since orders table may not have status field - orderDate: order.createdAt.toISOString(), - totalQuantity: order.orderItems.reduce((sum, item) => sum + parseFloat(item.quantity || '0'), 0), - products: order.orderItems.map(item => ({ - name: item.product.name, - quantity: parseFloat(item.quantity || '0'), - unit: item.product.unit?.shortNotation || 'unit', - })), - })); - }), - - getUpcomingSlots: publicProcedure - .query(async () => { - const threeHoursAgo = dayjs().subtract(3, 'hour').toDate(); - const slots = await db.query.deliverySlotInfo.findMany({ - where: and( - eq(deliverySlotInfo.isActive, true), - gt(deliverySlotInfo.deliveryTime, threeHoursAgo) - ), - orderBy: asc(deliverySlotInfo.deliveryTime), - }); - - return { - success: true, - data: slots.map(slot => ({ - id: slot.id, - deliveryTime: slot.deliveryTime.toISOString(), - freezeTime: slot.freezeTime.toISOString(), - deliverySequence: slot.deliverySequence, - })), - }; - }), - - getOrdersBySnippetAndSlot: publicProcedure - .input(z.object({ - snippetCode: z.string().min(1, "Snippet code is required"), - slotId: z.number().int().positive("Valid slot ID is required"), - })) - .query(async ({ input }) => { - const { snippetCode, slotId } = input; - - // Find the snippet - const snippet = await db.query.vendorSnippets.findFirst({ - where: eq(vendorSnippets.snippetCode, snippetCode), - }); - - if (!snippet) { - throw new Error("Vendor snippet not found"); - } - - // Find the slot - const slot = await db.query.deliverySlotInfo.findFirst({ - where: eq(deliverySlotInfo.id, slotId), - }); - - if (!slot) { - throw new Error("Slot not found"); - } - - // Query orders that match the slot and snippet criteria - const matchingOrders = await db.query.orders.findMany({ - where: eq(orders.slotId, slotId), - with: { - orderItems: { - with: { - product: { - with: { - unit: true, - }, - }, - }, - }, - orderStatus: true, - user: true, - slot: true, - }, - orderBy: (orders, { desc }) => [desc(orders.createdAt)], - }); - - // Filter orders that contain at least one of the snippet's products - const filteredOrders = matchingOrders.filter(order => { - const status = order.orderStatus; - if (status[0]?.isCancelled) return false; - const orderProductIds = order.orderItems.map(item => item.productId); - return snippet.productIds.some(productId => orderProductIds.includes(productId)); - }); - - // Format the response - const formattedOrders = filteredOrders.map(order => { - // Filter orderItems to only include products attached to the snippet - const attachedOrderItems = order.orderItems.filter(item => - snippet.productIds.includes(item.productId) - ); - - const products = attachedOrderItems.map(item => ({ - orderItemId: item.id, - productId: item.productId, - productName: item.product.name, - quantity: parseFloat(item.quantity), - price: parseFloat(item.price.toString()), - unit: item.product.unit?.shortNotation || 'unit', - subtotal: parseFloat(item.price.toString()) * parseFloat(item.quantity), - productSize: item.product.productQuantity, - is_packaged: item.is_packaged, - is_package_verified: item.is_package_verified, - })); - - const orderTotal = products.reduce((sum, p) => sum + p.subtotal, 0); - - return { - orderId: `ORD${order.id}`, - orderDate: order.createdAt.toISOString(), - customerName: order.user.name, - totalAmount: orderTotal, - slotInfo: order.slot ? { - time: order.slot.deliveryTime.toISOString(), - sequence: order.slot.deliverySequence, - } : null, - products, - matchedProducts: snippet.productIds, - snippetCode: snippet.snippetCode, - }; - }); - - return { - success: true, - data: formattedOrders, - snippet: { - id: snippet.id, - snippetCode: snippet.snippetCode, - slotId: snippet.slotId, - productIds: snippet.productIds, - validTill: snippet.validTill?.toISOString(), - createdAt: snippet.createdAt.toISOString(), - isPermanent: snippet.isPermanent, - }, - selectedSlot: { - id: slot.id, - deliveryTime: slot.deliveryTime.toISOString(), - freezeTime: slot.freezeTime.toISOString(), - deliverySequence: slot.deliverySequence, - }, - }; - }), - - updateOrderItemPackaging: publicProcedure - .input(z.object({ - orderItemId: z.number().int().positive("Valid order item ID required"), - is_packaged: z.boolean() - })) - .mutation(async ({ input, ctx }) => { - const { orderItemId, is_packaged } = input; - - // Get staff user ID from auth middleware - // const staffUserId = ctx.staffUser?.id; - // if (!staffUserId) { - // throw new Error("Unauthorized"); - // } - - // Check if order item exists and get related data - const orderItem = await db.query.orderItems.findFirst({ - where: eq(orderItems.id, orderItemId), - with: { - order: { - with: { - slot: true - } - } - } - }); - - if (!orderItem) { - throw new Error("Order item not found"); - } - - // Check if this order item belongs to a slot that has vendor snippets - // This ensures only order items from vendor-accessible orders can be updated - if (!orderItem.order.slotId) { - throw new Error("Order item not associated with a vendor slot"); - } - - const snippetExists = await db.query.vendorSnippets.findFirst({ - where: eq(vendorSnippets.slotId, orderItem.order.slotId), - }); - - if (!snippetExists) { - throw new Error("No vendor snippet found for this order's slot"); - } - - // Update the is_packaged field - const result = await db.update(orderItems) - .set({ is_packaged }) - .where(eq(orderItems.id, orderItemId)) - .returning(); - - if (result.length === 0) { - throw new Error("Failed to update packaging status"); - } - - return { - success: true, - orderItemId, - is_packaged - }; - }), -}); \ No newline at end of file diff --git a/verifier/admin-apis/dataAccessors/demo.txt b/verifier/admin-apis/dataAccessors/demo.txt deleted file mode 100644 index e69de29..0000000 diff --git a/verifier/apis/address.ts b/verifier/apis/address.ts deleted file mode 100644 index 019b895..0000000 --- a/verifier/apis/address.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { z } from 'zod'; -import { addressZones, addressAreas } from '@/src/db/schema' -import { eq, desc } from 'drizzle-orm'; -import { db } from '@/src/db/db_index' -import { router,protectedProcedure } from '@/src/trpc/trpc-index' - -const addressRouter = router({ - getZones: protectedProcedure.query(async () => { - const zones = await db.select().from(addressZones).orderBy(desc(addressZones.addedAt)); - return zones - }), - - getAreas: protectedProcedure.query(async () => { - const areas = await db.select().from(addressAreas).orderBy(desc(addressAreas.createdAt)); - return areas - }), - - createZone: protectedProcedure.input(z.object({ zoneName: z.string().min(1) })).mutation(async ({ input }) => { - - const zone = await db.insert(addressZones).values({ zoneName: input.zoneName }).returning(); - return {zone: zone}; - }), - - createArea: protectedProcedure.input(z.object({ placeName: z.string().min(1), zoneId: z.number().nullable() })).mutation(async ({ input }) => { - const area = await db.insert(addressAreas).values({ placeName: input.placeName, zoneId: input.zoneId }).returning(); - return {area}; - }), - - // TODO: Add update and delete mutations if needed -}); - -export default addressRouter; \ No newline at end of file diff --git a/verifier/apis/admin-trpc-index.ts b/verifier/apis/admin-trpc-index.ts deleted file mode 100644 index 4e23b84..0000000 --- a/verifier/apis/admin-trpc-index.ts +++ /dev/null @@ -1,39 +0,0 @@ -// import { router } from '@/src/trpc/trpc-index'; -import { router } from '@/src/trpc/trpc-index' -import { complaintRouter } from '@/src/trpc/apis/admin-apis/apis/complaint' -import { couponRouter } from '@/src/trpc/apis/admin-apis/apis/coupon' -import { cancelledOrdersRouter } from '@/src/trpc/apis/admin-apis/apis/cancelled-orders' -import { orderRouter } from '@/src/trpc/apis/admin-apis/apis/order' -import { vendorSnippetsRouter } from '@/src/trpc/apis/admin-apis/apis/vendor-snippets' -import { slotsRouter } from '@/src/trpc/apis/admin-apis/apis/slots' -import { productRouter } from '@/src/trpc/apis/admin-apis/apis/product' -import { staffUserRouter } from '@/src/trpc/apis/admin-apis/apis/staff-user' -import { storeRouter } from '@/src/trpc/apis/admin-apis/apis/store' -import { adminPaymentsRouter } from '@/src/trpc/apis/admin-apis/apis/payments' -import addressRouter from '@/src/trpc/apis/admin-apis/apis/address' -import { bannerRouter } from '@/src/trpc/apis/admin-apis/apis/banner' -import { userRouter } from '@/src/trpc/apis/admin-apis/apis/user' -import { constRouter } from '@/src/trpc/apis/admin-apis/apis/const' -import { productAvailabilitySchedulesRouter } from '@/src/trpc/apis/admin-apis/apis/product-availability-schedules' -import { tagRouter } from '@/src/trpc/apis/admin-apis/apis/tag' - -export const adminRouter = router({ - complaint: complaintRouter, - coupon: couponRouter, - cancelledOrders: cancelledOrdersRouter, - order: orderRouter, - vendorSnippets: vendorSnippetsRouter, - slots: slotsRouter, - product: productRouter, - staffUser: staffUserRouter, - store: storeRouter, - payments: adminPaymentsRouter, - address: addressRouter, - banner: bannerRouter, - user: userRouter, - const: constRouter, - productAvailabilitySchedules: productAvailabilitySchedulesRouter, - tag: tagRouter, -}); - -export type AdminRouter = typeof adminRouter; diff --git a/verifier/apis/banner.ts b/verifier/apis/banner.ts deleted file mode 100644 index 5794143..0000000 --- a/verifier/apis/banner.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { z } from 'zod'; -import { db } from '@/src/db/db_index' -import { homeBanners } from '@/src/db/schema' -import { eq, and, desc, sql } from 'drizzle-orm'; -import { protectedProcedure, router } from '@/src/trpc/trpc-index' -import { extractKeyFromPresignedUrl, scaffoldAssetUrl } from '@/src/lib/s3-client' -import { ApiError } from '@/src/lib/api-error'; -import { scheduleStoreInitialization } from '@/src/stores/store-initializer' - - -export const bannerRouter = router({ - // Get all banners - getBanners: protectedProcedure - .query(async () => { - try { - - const banners = await db.query.homeBanners.findMany({ - orderBy: desc(homeBanners.createdAt), // Order by creation date instead - // Removed product relationship since we now use productIds array - }); - - // Convert S3 keys to signed URLs for client - const bannersWithSignedUrls = await Promise.all( - banners.map(async (banner) => { - try { - return { - ...banner, - imageUrl: banner.imageUrl ? scaffoldAssetUrl(banner.imageUrl) : banner.imageUrl, - // Ensure productIds is always an array - productIds: banner.productIds || [], - }; - } catch (error) { - console.error(`Failed to generate signed URL for banner ${banner.id}:`, error); - return { - ...banner, - imageUrl: banner.imageUrl, // Keep original on error - // Ensure productIds is always an array - productIds: banner.productIds || [], - }; - } - }) - ); - - return { - banners: bannersWithSignedUrls, - }; - } - catch(e:any) { - console.log(e) - - throw new ApiError(e.message); - } - }), - - // Get single banner by ID - getBanner: protectedProcedure - .input(z.object({ id: z.number() })) - .query(async ({ input }) => { - const banner = await db.query.homeBanners.findFirst({ - where: eq(homeBanners.id, input.id), - // Removed product relationship since we now use productIds array - }); - - if (banner) { - try { - // Convert S3 key to signed URL for client - if (banner.imageUrl) { - banner.imageUrl = scaffoldAssetUrl(banner.imageUrl); - } - } catch (error) { - console.error(`Failed to generate signed URL for banner ${banner.id}:`, error); - // Keep original imageUrl on error - } - - // Ensure productIds is always an array (handle migration compatibility) - if (!banner.productIds) { - banner.productIds = []; - } - } - - return banner; - }), - - // Create new banner - createBanner: protectedProcedure - .input(z.object({ - name: z.string().min(1), - imageUrl: z.string(), - description: z.string().optional(), - productIds: z.array(z.number()).optional(), - redirectUrl: z.string().url().optional(), - // serialNum removed completely - })) - .mutation(async ({ input }) => { - try { - const imageUrl = extractKeyFromPresignedUrl(input.imageUrl) - // const imageUrl = input.imageUrl - const [banner] = await db.insert(homeBanners).values({ - name: input.name, - imageUrl: imageUrl, - description: input.description, - productIds: input.productIds || [], - redirectUrl: input.redirectUrl, - serialNum: 999, // Default value, not used - isActive: false, // Default to inactive - }).returning(); - - // Reinitialize stores to reflect changes - scheduleStoreInitialization() - - return banner; - } catch (error) { - console.error('Error creating banner:', error); - throw error; // Re-throw to maintain tRPC error handling - } - }), - - // Update banner - updateBanner: protectedProcedure - .input(z.object({ - id: z.number(), - name: z.string().min(1).optional(), - imageUrl: z.string().url().optional(), - description: z.string().optional(), - productIds: z.array(z.number()).optional(), - redirectUrl: z.string().url().optional(), - serialNum: z.number().nullable().optional(), - isActive: z.boolean().optional(), - })) - .mutation(async ({ input }) => { - try { - - const { id, ...updateData } = input; - const incomingProductIds = input.productIds; - // Extract S3 key from presigned URL if imageUrl is provided - const processedData = { - ...updateData, - ...(updateData.imageUrl && { - imageUrl: extractKeyFromPresignedUrl(updateData.imageUrl) - }), - }; - - // Handle serialNum null case - const finalData: any = { ...processedData }; - if ('serialNum' in finalData && finalData.serialNum === null) { - // Set to null explicitly - finalData.serialNum = null; - } - - const [banner] = await db.update(homeBanners) - .set({ ...finalData, lastUpdated: new Date(), }) - .where(eq(homeBanners.id, id)) - .returning(); - - // Reinitialize stores to reflect changes - scheduleStoreInitialization() - - return banner; - } catch (error) { - console.error('Error updating banner:', error); - throw error; - } - }), - - // Delete banner - deleteBanner: protectedProcedure - .input(z.object({ id: z.number() })) - .mutation(async ({ input }) => { - await db.delete(homeBanners).where(eq(homeBanners.id, input.id)); - - // Reinitialize stores to reflect changes - scheduleStoreInitialization() - - return { success: true }; - }), -}); diff --git a/verifier/apis/cancelled-orders.ts b/verifier/apis/cancelled-orders.ts deleted file mode 100644 index 14c0b26..0000000 --- a/verifier/apis/cancelled-orders.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { router, protectedProcedure } from '@/src/trpc/trpc-index' -import { z } from 'zod'; -import { db } from '@/src/db/db_index' -import { orders, orderStatus, users, addresses, orderItems, productInfo, units, refunds } from '@/src/db/schema' -import { eq, desc } from 'drizzle-orm'; - -const updateCancellationReviewSchema = z.object({ - orderId: z.number(), - cancellationReviewed: z.boolean(), - adminNotes: z.string().optional(), -}); - -const updateRefundSchema = z.object({ - orderId: z.number(), - isRefundDone: z.boolean(), -}); - -export const cancelledOrdersRouter = router({ - getAll: protectedProcedure - .query(async () => { - // First get cancelled order statuses with order details - const cancelledOrderStatuses = await db.query.orderStatus.findMany({ - where: eq(orderStatus.isCancelled, true), - with: { - order: { - with: { - user: true, - address: true, - orderItems: { - with: { - product: { - with: { - unit: true, - }, - }, - }, - }, - refunds: true, - }, - }, - }, - orderBy: [desc(orderStatus.orderTime)], - }); - - const filteredStatuses = cancelledOrderStatuses.filter(status => { - return status.order.isCod || status.paymentStatus === 'success'; - }); - - return filteredStatuses.map(status => { - const refund = status.order.refunds[0]; - return { - id: status.order.id, - readableId: status.order.id, - customerName: `${status.order.user.name}`, - address: `${status.order.address.addressLine1}, ${status.order.address.city}`, - totalAmount: status.order.totalAmount, - cancellationReviewed: status.cancellationReviewed || false, - isRefundDone: refund?.refundStatus === 'processed' || false, - adminNotes: status.order.adminNotes, - cancelReason: status.cancelReason, - paymentMode: status.order.isCod ? 'COD' : 'Online', - paymentStatus: status.paymentStatus || 'pending', - items: status.order.orderItems.map(item => ({ - name: item.product.name, - quantity: item.quantity, - price: item.price, - unit: item.product.unit?.shortNotation, - amount: parseFloat(item.price.toString()) * parseFloat(item.quantity || '0'), - })), - createdAt: status.order.createdAt, - }; - }); - }), - - updateReview: protectedProcedure - .input(updateCancellationReviewSchema) - .mutation(async ({ input }) => { - const { orderId, cancellationReviewed, adminNotes } = input; - - const result = await db.update(orderStatus) - .set({ - cancellationReviewed, - cancellationAdminNotes: adminNotes || null, - cancellationReviewedAt: new Date(), - }) - .where(eq(orderStatus.orderId, orderId)) - .returning(); - - if (result.length === 0) { - throw new Error("Cancellation record not found"); - } - - return result[0]; - }), - - getById: protectedProcedure - .input(z.object({ id: z.number() })) - .query(async ({ input }) => { - const { id } = input; - - // Get cancelled order with full details - const cancelledOrderStatus = await db.query.orderStatus.findFirst({ - where: eq(orderStatus.id, id), - with: { - order: { - with: { - user: true, - address: true, - orderItems: { - with: { - product: { - with: { - unit: true, - }, - }, - }, - }, - }, - }, - }, - }); - - if (!cancelledOrderStatus || !cancelledOrderStatus.isCancelled) { - throw new Error("Cancelled order not found"); - } - - // Get refund details separately - const refund = await db.query.refunds.findFirst({ - where: eq(refunds.orderId, cancelledOrderStatus.orderId), - }); - - const order = cancelledOrderStatus.order; - - // Format the response similar to the getAll method - const formattedOrder = { - id: order.id, - readableId: order.id, - customerName: order.user.name, - address: `${order.address.addressLine1}${order.address.addressLine2 ? ', ' + order.address.addressLine2 : ''}, ${order.address.city}, ${order.address.state} ${order.address.pincode}`, - totalAmount: order.totalAmount, - cancellationReviewed: cancelledOrderStatus.cancellationReviewed || false, - isRefundDone: refund?.refundStatus === 'processed' || false, - adminNotes: cancelledOrderStatus.cancellationAdminNotes || null, - cancelReason: cancelledOrderStatus.cancelReason || null, - items: order.orderItems.map((item: any) => ({ - name: item.product.name, - quantity: item.quantity, - price: parseFloat(item.price.toString()), - unit: item.product.unit?.shortNotation || 'unit', - amount: parseFloat(item.price.toString()) * parseFloat(item.quantity), - image: item.product.images?.[0] || null, - })), - createdAt: order.createdAt.toISOString(), - }; - - return { order: formattedOrder }; - }), - - updateRefund: protectedProcedure - .input(updateRefundSchema) - .mutation(async ({ input }) => { - const { orderId, isRefundDone } = input; - - const refundStatus = isRefundDone ? 'processed' : 'none'; - const result = await db.update(refunds) - .set({ - refundStatus, - refundProcessedAt: isRefundDone ? new Date() : null, - }) - .where(eq(refunds.orderId, orderId)) - .returning(); - - if (result.length === 0) { - throw new Error("Cancellation record not found"); - } - - return result[0]; - }), -}); \ No newline at end of file diff --git a/verifier/apis/complaint.ts b/verifier/apis/complaint.ts deleted file mode 100644 index b1791a6..0000000 --- a/verifier/apis/complaint.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { router, protectedProcedure } from '@/src/trpc/trpc-index' -import { z } from 'zod'; -import { db } from '@/src/db/db_index' -import { complaints, users } from '@/src/db/schema' -import { eq, desc, lt, and } from 'drizzle-orm'; -import { scaffoldAssetUrl } from '@/src/lib/s3-client' - -export const complaintRouter = router({ - getAll: protectedProcedure - .input(z.object({ - cursor: z.number().optional(), - limit: z.number().default(20), - })) - .query(async ({ input }) => { - const { cursor, limit } = input; - - let whereCondition = cursor - ? lt(complaints.id, cursor) - : undefined; - - const complaintsData = await db - .select({ - id: complaints.id, - complaintBody: complaints.complaintBody, - userId: complaints.userId, - orderId: complaints.orderId, - isResolved: complaints.isResolved, - createdAt: complaints.createdAt, - userName: users.name, - userMobile: users.mobile, - images: complaints.images, - }) - .from(complaints) - .leftJoin(users, eq(complaints.userId, users.id)) - .where(whereCondition) - .orderBy(desc(complaints.id)) - .limit(limit + 1); - - const hasMore = complaintsData.length > limit; - const complaintsToReturn = hasMore ? complaintsData.slice(0, limit) : complaintsData; - - const complaintsWithSignedImages = await Promise.all( - complaintsToReturn.map(async (c) => { - const signedImages = c.images - ? scaffoldAssetUrl(c.images as string[]) - : []; - - return { - id: c.id, - text: c.complaintBody, - userId: c.userId, - userName: c.userName, - userMobile: c.userMobile, - orderId: c.orderId, - status: c.isResolved ? 'resolved' : 'pending', - createdAt: c.createdAt, - images: signedImages, - }; - }) - ); - - return { - complaints: complaintsWithSignedImages, - nextCursor: hasMore - ? complaintsToReturn[complaintsToReturn.length - 1].id - : undefined, - }; - }), - - resolve: protectedProcedure - .input(z.object({ id: z.string(), response: z.string().optional() })) - .mutation(async ({ input }) => { - await db - .update(complaints) - .set({ isResolved: true, response: input.response }) - .where(eq(complaints.id, parseInt(input.id))); - - return { message: 'Complaint resolved successfully' }; - }), -}); \ No newline at end of file diff --git a/verifier/apis/const.ts b/verifier/apis/const.ts deleted file mode 100644 index a426087..0000000 --- a/verifier/apis/const.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { router, protectedProcedure } from '@/src/trpc/trpc-index' -import { z } from 'zod'; -import { db } from '@/src/db/db_index' -import { keyValStore } from '@/src/db/schema' -import { computeConstants } from '@/src/lib/const-store' -import { CONST_KEYS } from '@/src/lib/const-keys' - -export const constRouter = router({ - getConstants: protectedProcedure - .query(async () => { - - const constants = await db.select().from(keyValStore); - - const resp = constants.map(c => ({ - key: c.key, - value: c.value, - })); - - return resp; - }), - - updateConstants: protectedProcedure - .input(z.object({ - constants: z.array(z.object({ - key: z.string(), - value: z.any(), - })), - })) - .mutation(async ({ input }) => { - const { constants } = input; - - const validKeys = Object.values(CONST_KEYS) as string[]; - const invalidKeys = constants - .filter(c => !validKeys.includes(c.key)) - .map(c => c.key); - - if (invalidKeys.length > 0) { - throw new Error(`Invalid constant keys: ${invalidKeys.join(', ')}`); - } - - await db.transaction(async (tx) => { - for (const { key, value } of constants) { - await tx.insert(keyValStore) - .values({ key, value }) - .onConflictDoUpdate({ - target: keyValStore.key, - set: { value }, - }); - } - }); - - // Refresh all constants in Redis after database update - await computeConstants(); - - return { - success: true, - updatedCount: constants.length, - keys: constants.map(c => c.key), - }; - }), -}); \ No newline at end of file diff --git a/verifier/apis/coupon.ts b/verifier/apis/coupon.ts deleted file mode 100644 index 4eb3017..0000000 --- a/verifier/apis/coupon.ts +++ /dev/null @@ -1,711 +0,0 @@ -import { router, protectedProcedure } from '@/src/trpc/trpc-index' -import { z } from 'zod'; -import { db } from '@/src/db/db_index' -import { coupons, users, staffUsers, orders, couponApplicableUsers, couponApplicableProducts, orderStatus, reservedCoupons } from '@/src/db/schema' -import { eq, and, like, or, inArray, lt } from 'drizzle-orm'; -import dayjs from 'dayjs'; - -const createCouponBodySchema = z.object({ - couponCode: z.string().optional(), - isUserBased: z.boolean().optional(), - discountPercent: z.number().optional(), - flatDiscount: z.number().optional(), - minOrder: z.number().optional(), - targetUser: z.number().optional(), - productIds: z.array(z.number()).optional().nullable(), - applicableUsers: z.array(z.number()).optional(), - applicableProducts: z.array(z.number()).optional(), - maxValue: z.number().optional(), - isApplyForAll: z.boolean().optional(), - validTill: z.string().optional(), - maxLimitForUser: z.number().optional(), - exclusiveApply: z.boolean().optional(), -}); - -const validateCouponBodySchema = z.object({ - code: z.string(), - userId: z.number(), - orderAmount: z.number(), -}); - -export const couponRouter = router({ - create: protectedProcedure - .input(createCouponBodySchema) - .mutation(async ({ input, ctx }) => { - const { couponCode, isUserBased, discountPercent, flatDiscount, minOrder, productIds, applicableUsers, applicableProducts, maxValue, isApplyForAll, validTill, maxLimitForUser, exclusiveApply } = input; - - // Validation: ensure at least one discount type is provided - if ((!discountPercent && !flatDiscount) || (discountPercent && flatDiscount)) { - throw new Error("Either discountPercent or flatDiscount must be provided (but not both)"); - } - - // If user-based, applicableUsers is required (unless it's apply for all) - if (isUserBased && (!applicableUsers || applicableUsers.length === 0) && !isApplyForAll) { - throw new Error("applicableUsers is required for user-based coupons (or set isApplyForAll to true)"); - } - - // Cannot be both user-based and apply for all - if (isUserBased && isApplyForAll) { - throw new Error("Cannot be both user-based and apply for all users"); - } - - // If applicableUsers is provided, verify users exist - if (applicableUsers && applicableUsers.length > 0) { - const existingUsers = await db.query.users.findMany({ - where: inArray(users.id, applicableUsers), - columns: { id: true }, - }); - if (existingUsers.length !== applicableUsers.length) { - throw new Error("Some applicable users not found"); - } - } - - // Get staff user ID from auth middleware - const staffUserId = ctx.staffUser?.id; - if (!staffUserId) { - throw new Error("Unauthorized"); - } - - // Generate coupon code if not provided - let finalCouponCode = couponCode; - if (!finalCouponCode) { - // Generate a unique coupon code - const timestamp = Date.now().toString().slice(-6); - const random = Math.random().toString(36).substring(2, 8).toUpperCase(); - finalCouponCode = `MF${timestamp}${random}`; - } - - // Check if coupon code already exists - const existingCoupon = await db.query.coupons.findFirst({ - where: eq(coupons.couponCode, finalCouponCode), - }); - - if (existingCoupon) { - throw new Error("Coupon code already exists"); - } - - const result = await db.insert(coupons).values({ - couponCode: finalCouponCode, - isUserBased: isUserBased || false, - discountPercent: discountPercent?.toString(), - flatDiscount: flatDiscount?.toString(), - minOrder: minOrder?.toString(), - productIds: productIds || null, - createdBy: staffUserId, - maxValue: maxValue?.toString(), - isApplyForAll: isApplyForAll || false, - validTill: validTill ? dayjs(validTill).toDate() : undefined, - maxLimitForUser: maxLimitForUser, - exclusiveApply: exclusiveApply || false, - }).returning(); - - const coupon = result[0]; - - // Insert applicable users - if (applicableUsers && applicableUsers.length > 0) { - await db.insert(couponApplicableUsers).values( - applicableUsers.map(userId => ({ - couponId: coupon.id, - userId, - })) - ); - } - - // Insert applicable products - if (applicableProducts && applicableProducts.length > 0) { - await db.insert(couponApplicableProducts).values( - applicableProducts.map(productId => ({ - couponId: coupon.id, - productId, - })) - ); - } - - return coupon; - }), - - getAll: protectedProcedure - .input(z.object({ - cursor: z.number().optional(), - limit: z.number().default(50), - search: z.string().optional(), - })) - .query(async ({ input }) => { - const { cursor, limit, search } = input; - - let whereCondition = undefined; - const conditions = []; - - if (cursor) { - conditions.push(lt(coupons.id, cursor)); - } - - if (search && search.trim()) { - conditions.push(like(coupons.couponCode, `%${search}%`)); - } - - if (conditions.length > 0) { - whereCondition = and(...conditions); - } - - const result = await db.query.coupons.findMany({ - where: whereCondition, - with: { - creator: true, - applicableUsers: { - with: { - user: true, - }, - }, - applicableProducts: { - with: { - product: true, - }, - }, - }, - orderBy: (coupons, { desc }) => [desc(coupons.createdAt)], - limit: limit + 1, - }); - - const hasMore = result.length > limit; - const couponsList = hasMore ? result.slice(0, limit) : result; - const nextCursor = hasMore ? result[result.length - 1].id : undefined; - - return { coupons: couponsList, nextCursor }; - }), - - getById: protectedProcedure - .input(z.object({ id: z.number() })) - .query(async ({ input }) => { - const couponId = input.id; - - const result = await db.query.coupons.findFirst({ - where: eq(coupons.id, couponId), - with: { - creator: true, - applicableUsers: { - with: { - user: true, - }, - }, - applicableProducts: { - with: { - product: true, - }, - }, - }, - }); - - if (!result) { - throw new Error("Coupon not found"); - } - - return { - ...result, - productIds: (result.productIds as number[]) || undefined, - applicableUsers: result.applicableUsers.map(au => au.user), - applicableProducts: result.applicableProducts.map(ap => ap.product), - }; - }), - - update: protectedProcedure - .input(z.object({ - id: z.number(), - updates: createCouponBodySchema.extend({ - isInvalidated: z.boolean().optional(), - }), - })) - .mutation(async ({ input }) => { - const { id, updates } = input; - - // Validation: ensure discount types are valid - if (updates.discountPercent !== undefined && updates.flatDiscount !== undefined) { - if (updates.discountPercent && updates.flatDiscount) { - throw new Error("Cannot have both discountPercent and flatDiscount"); - } - } - - // If updating to user-based, applicableUsers is required - if (updates.isUserBased && (!updates.applicableUsers || updates.applicableUsers.length === 0)) { - const existingCount = await db.$count(couponApplicableUsers, eq(couponApplicableUsers.couponId, id)); - if (existingCount === 0) { - throw new Error("applicableUsers is required for user-based coupons"); - } - } - - // If applicableUsers is provided, verify users exist - if (updates.applicableUsers && updates.applicableUsers.length > 0) { - const existingUsers = await db.query.users.findMany({ - where: inArray(users.id, updates.applicableUsers), - columns: { id: true }, - }); - if (existingUsers.length !== updates.applicableUsers.length) { - throw new Error("Some applicable users not found"); - } - } - - const updateData: any = { ...updates }; - delete updateData.applicableUsers; // Remove since we use couponApplicableUsers table - if (updates.discountPercent !== undefined) { - updateData.discountPercent = updates.discountPercent?.toString(); - } - if (updates.flatDiscount !== undefined) { - updateData.flatDiscount = updates.flatDiscount?.toString(); - } - if (updates.minOrder !== undefined) { - updateData.minOrder = updates.minOrder?.toString(); - } - if (updates.maxValue !== undefined) { - updateData.maxValue = updates.maxValue?.toString(); - } - if (updates.validTill !== undefined) { - updateData.validTill = updates.validTill ? dayjs(updates.validTill).toDate() : null; - } - - const result = await db.update(coupons) - .set(updateData) - .where(eq(coupons.id, id)) - .returning(); - - if (result.length === 0) { - throw new Error("Coupon not found"); - } - - console.log('updated coupon successfully') - - // Update applicable users: delete existing and insert new - if (updates.applicableUsers !== undefined) { - await db.delete(couponApplicableUsers).where(eq(couponApplicableUsers.couponId, id)); - if (updates.applicableUsers.length > 0) { - await db.insert(couponApplicableUsers).values( - updates.applicableUsers.map(userId => ({ - couponId: id, - userId, - })) - ); - } - } - - // Update applicable products: delete existing and insert new - if (updates.applicableProducts !== undefined) { - await db.delete(couponApplicableProducts).where(eq(couponApplicableProducts.couponId, id)); - if (updates.applicableProducts.length > 0) { - await db.insert(couponApplicableProducts).values( - updates.applicableProducts.map(productId => ({ - couponId: id, - productId, - })) - ); - } - } - - return result[0]; - }), - - delete: protectedProcedure - .input(z.object({ id: z.number() })) - .mutation(async ({ input }) => { - const { id } = input; - - const result = await db.update(coupons) - .set({ isInvalidated: true }) - .where(eq(coupons.id, id)) - .returning(); - - if (result.length === 0) { - throw new Error("Coupon not found"); - } - - return { message: "Coupon invalidated successfully" }; - }), - - validate: protectedProcedure - .input(validateCouponBodySchema) - .query(async ({ input }) => { - const { code, userId, orderAmount } = input; - - if (!code || typeof code !== 'string') { - return { valid: false, message: "Invalid coupon code" }; - } - - const coupon = await db.query.coupons.findFirst({ - where: and( - eq(coupons.couponCode, code.toUpperCase()), - eq(coupons.isInvalidated, false) - ), - }); - - if (!coupon) { - return { valid: false, message: "Coupon not found or invalidated" }; - } - - // Check expiry date - if (coupon.validTill && new Date(coupon.validTill) < new Date()) { - return { valid: false, message: "Coupon has expired" }; - } - - // Check if coupon applies to all users or specific user - if (!coupon.isApplyForAll && !coupon.isUserBased) { - return { valid: false, message: "Coupon is not available for use" }; - } - - // Check minimum order amount - const minOrderValue = coupon.minOrder ? parseFloat(coupon.minOrder) : 0; - if (minOrderValue > 0 && orderAmount < minOrderValue) { - return { valid: false, message: `Minimum order amount is ${minOrderValue}` }; - } - - // Calculate discount - let discountAmount = 0; - if (coupon.discountPercent) { - const percent = parseFloat(coupon.discountPercent); - discountAmount = (orderAmount * percent) / 100; - } else if (coupon.flatDiscount) { - discountAmount = parseFloat(coupon.flatDiscount); - } - - // Apply max value limit - const maxValueLimit = coupon.maxValue ? parseFloat(coupon.maxValue) : 0; - if (maxValueLimit > 0 && discountAmount > maxValueLimit) { - discountAmount = maxValueLimit; - } - - return { - valid: true, - discountAmount, - coupon: { - id: coupon.id, - discountPercent: coupon.discountPercent, - flatDiscount: coupon.flatDiscount, - maxValue: coupon.maxValue, - } - }; - }), - - generateCancellationCoupon: protectedProcedure - .input( - z.object({ - orderId: z.number(), - }) - ) - .mutation(async ({ input, ctx }) => { - const { orderId } = input; - - // Get staff user ID from auth middleware - const staffUserId = ctx.staffUser?.id; - if (!staffUserId) { - throw new Error("Unauthorized"); - } - - // Find the order with user and order status information - const order = await db.query.orders.findFirst({ - where: eq(orders.id, orderId), - with: { - user: true, - orderStatus: true, - }, - }); - - if (!order) { - throw new Error("Order not found"); - } - - // Check if order is cancelled (check if any status entry has isCancelled: true) - // const isOrderCancelled = order.orderStatus?.some(status => status.isCancelled) || false; - // if (!isOrderCancelled) { - // throw new Error("Order is not cancelled"); - // } - - // // Check if payment method is COD - // if (order.isCod) { - // throw new Error("Can't generate refund coupon for CoD Order"); - // } - - // Verify user exists - if (!order.user) { - throw new Error("User not found for this order"); - } - - // Generate coupon code: first 3 letters of user name or mobile + orderId - const userNamePrefix = (order.user.name || order.user.mobile || 'USR').substring(0, 3).toUpperCase(); - const couponCode = `${userNamePrefix}${orderId}`; - - // Check if coupon code already exists - const existingCoupon = await db.query.coupons.findFirst({ - where: eq(coupons.couponCode, couponCode), - }); - - if (existingCoupon) { - throw new Error("Coupon code already exists"); - } - - // Get order total amount - const orderAmount = parseFloat(order.totalAmount); - - // Calculate expiry date (30 days from now) - const expiryDate = new Date(); - expiryDate.setDate(expiryDate.getDate() + 30); - - // Create the coupon and update order status in a transaction - const coupon = await db.transaction(async (tx) => { - // Create the coupon - const result = await tx.insert(coupons).values({ - couponCode, - isUserBased: true, - flatDiscount: orderAmount.toString(), - minOrder: orderAmount.toString(), - maxValue: orderAmount.toString(), - validTill: expiryDate, - maxLimitForUser: 1, - createdBy: staffUserId, - isApplyForAll: false, - }).returning(); - - const coupon = result[0]; - - // Insert applicable users - await tx.insert(couponApplicableUsers).values({ - couponId: coupon.id, - userId: order.userId, - }); - - // Update order_status with refund coupon ID - await tx.update(orderStatus) - .set({ refundCouponId: coupon.id }) - .where(eq(orderStatus.orderId, orderId)); - - return coupon; - }); - - return coupon; - }), - - getReservedCoupons: protectedProcedure - .input(z.object({ - cursor: z.number().optional(), - limit: z.number().default(50), - search: z.string().optional(), - })) - .query(async ({ input }) => { - const { cursor, limit, search } = input; - - let whereCondition = undefined; - const conditions = []; - - if (cursor) { - conditions.push(lt(reservedCoupons.id, cursor)); - } - - if (search && search.trim()) { - conditions.push(or( - like(reservedCoupons.secretCode, `%${search}%`), - like(reservedCoupons.couponCode, `%${search}%`) - )); - } - - if (conditions.length > 0) { - whereCondition = and(...conditions); - } - - const result = await db.query.reservedCoupons.findMany({ - where: whereCondition, - with: { - redeemedUser: true, - creator: true, - }, - orderBy: (reservedCoupons, { desc }) => [desc(reservedCoupons.createdAt)], - limit: limit + 1, // Fetch one extra to check if there's more - }); - - const hasMore = result.length > limit; - const coupons = hasMore ? result.slice(0, limit) : result; - const nextCursor = hasMore ? result[result.length - 1].id : undefined; - - return { - coupons, - nextCursor, - }; - }), - - createReservedCoupon: protectedProcedure - .input(createCouponBodySchema) - .mutation(async ({ input, ctx }) => { - const { couponCode, isUserBased, discountPercent, flatDiscount, minOrder, productIds, applicableUsers, applicableProducts, maxValue, isApplyForAll, validTill, maxLimitForUser, exclusiveApply } = input; - - // Validation: ensure at least one discount type is provided - if ((!discountPercent && !flatDiscount) || (discountPercent && flatDiscount)) { - throw new Error("Either discountPercent or flatDiscount must be provided (but not both)"); - } - - // For reserved coupons, applicableUsers is not used, as it's redeemed by one user - - // Get staff user ID from auth middleware - const staffUserId = ctx.staffUser?.id; - if (!staffUserId) { - throw new Error("Unauthorized"); - } - - // Generate secret code if not provided (use couponCode as base) - let secretCode = couponCode || `SECRET${Date.now().toString().slice(-6)}${Math.random().toString(36).substring(2, 8).toUpperCase()}`; - - // Check if secret code already exists - const existing = await db.query.reservedCoupons.findFirst({ - where: eq(reservedCoupons.secretCode, secretCode), - }); - - if (existing) { - throw new Error("Secret code already exists"); - } - - const result = await db.insert(reservedCoupons).values({ - secretCode, - couponCode: couponCode || `RESERVED${Date.now().toString().slice(-6)}`, - discountPercent: discountPercent?.toString(), - flatDiscount: flatDiscount?.toString(), - minOrder: minOrder?.toString(), - productIds, - maxValue: maxValue?.toString(), - validTill: validTill ? dayjs(validTill).toDate() : undefined, - maxLimitForUser, - exclusiveApply: exclusiveApply || false, - createdBy: staffUserId, - }).returning(); - - const coupon = result[0]; - - // Insert applicable products if provided - if (applicableProducts && applicableProducts.length > 0) { - await db.insert(couponApplicableProducts).values( - applicableProducts.map(productId => ({ - couponId: coupon.id, - productId, - })) - ); - } - - return coupon; - }), - - getUsersMiniInfo: protectedProcedure - .input(z.object({ - search: z.string().optional(), - limit: z.number().min(1).max(50).default(20), - offset: z.number().min(0).default(0), - })) - .query(async ({ input }) => { - const { search, limit } = input; - - let whereCondition = undefined; - if (search && search.trim()) { - whereCondition = or( - like(users.name, `%${search}%`), - like(users.mobile, `%${search}%`) - ); - } - - const userList = await db.query.users.findMany({ - where: whereCondition, - columns: { - id: true, - name: true, - mobile: true, - }, - limit: limit, - offset: input.offset, - orderBy: (users, { asc }) => [asc(users.name)], - }); - - return { - users: userList.map(user => ({ - id: user.id, - name: user.name || 'Unknown', - mobile: user.mobile, - })) - }; - }), - - createCoupon: protectedProcedure - .input(z.object({ - mobile: z.string().min(1, 'Mobile number is required'), - })) - .mutation(async ({ input, ctx }) => { - const { mobile } = input; - - // Get staff user ID from auth middleware - const staffUserId = ctx.staffUser?.id; - if (!staffUserId) { - throw new Error("Unauthorized"); - } - - // Clean mobile number (remove non-digits) - const cleanMobile = mobile.replace(/\D/g, ''); - - // Validate: exactly 10 digits - if (cleanMobile.length !== 10) { - throw new Error("Mobile number must be exactly 10 digits"); - } - - // Check if user exists, create if not - let user = await db.query.users.findFirst({ - where: eq(users.mobile, cleanMobile), - }); - - if (!user) { - // Create new user - const [newUser] = await db.insert(users).values({ - name: null, - email: null, - mobile: cleanMobile, - }).returning(); - user = newUser; - } - - // Generate unique coupon code - const timestamp = Date.now().toString().slice(-6); - const random = Math.random().toString(36).substring(2, 6).toUpperCase(); - const couponCode = `MF${cleanMobile.slice(-4)}${timestamp}${random}`; - - // Check if coupon code already exists (very unlikely but safe) - const existingCode = await db.query.coupons.findFirst({ - where: eq(coupons.couponCode, couponCode), - }); - - if (existingCode) { - throw new Error("Generated coupon code already exists - please try again"); - } - - // Create the coupon - const [coupon] = await db.insert(coupons).values({ - couponCode, - isUserBased: true, - discountPercent: "20", // 20% discount - minOrder: "1000", // ₹1000 minimum order - maxValue: "500", // ₹500 maximum discount - maxLimitForUser: 1, // One-time use - isApplyForAll: false, - exclusiveApply: false, - createdBy: staffUserId, - validTill: dayjs().add(90, 'days').toDate(), // 90 days from now - }).returning(); - - // Associate coupon with user - await db.insert(couponApplicableUsers).values({ - couponId: coupon.id, - userId: user.id, - }); - - return { - success: true, - coupon: { - id: coupon.id, - couponCode: coupon.couponCode, - userId: user.id, - userMobile: user.mobile, - discountPercent: 20, - minOrder: 1000, - maxValue: 500, - maxLimitForUser: 1, - }, - }; - }), -}); diff --git a/verifier/apis/order.ts b/verifier/apis/order.ts deleted file mode 100644 index 707f642..0000000 --- a/verifier/apis/order.ts +++ /dev/null @@ -1,1018 +0,0 @@ -import { router, protectedProcedure } from "@/src/trpc/trpc-index" -import { z } from "zod"; -import { db } from "@/src/db/db_index" -import { - orders, - orderItems, - orderStatus, - users, - addresses, - refunds, - coupons, - couponUsage, - complaints, - payments, -} from "@/src/db/schema"; -import { eq, and, gte, lt, desc, SQL, inArray } from "drizzle-orm"; -import dayjs from "dayjs"; -import utc from "dayjs/plugin/utc"; -import { ApiError } from "@/src/lib/api-error" -import { - sendOrderPackagedNotification, - sendOrderDeliveredNotification, -} from "@/src/lib/notif-job"; -import { publishCancellation } from "@/src/lib/post-order-handler" -import { getMultipleUserNegativityScores } from "@/src/stores/user-negativity-store" - -const updateOrderNotesSchema = z.object({ - orderId: z.number(), - adminNotes: z.string(), -}); - -const getFullOrderSchema = z.object({ - orderId: z.number(), -}); - -const getOrderDetailsSchema = z.object({ - orderId: z.number(), -}); - -const updatePackagedSchema = z.object({ - orderId: z.string(), - isPackaged: z.boolean(), -}); - -const updateDeliveredSchema = z.object({ - orderId: z.string(), - isDelivered: z.boolean(), -}); - -const updateOrderItemPackagingSchema = z.object({ - orderItemId: z.number(), - isPackaged: z.boolean().optional(), - isPackageVerified: z.boolean().optional(), -}); - -const getSlotOrdersSchema = z.object({ - slotId: z.string(), -}); - -const getTodaysOrdersSchema = z.object({ - slotId: z.string().optional(), -}); - -const getAllOrdersSchema = z.object({ - cursor: z.number().optional(), - limit: z.number().default(20), - slotId: z.number().optional().nullable(), - packagedFilter: z - .enum(["all", "packaged", "not_packaged"]) - .optional() - .default("all"), - deliveredFilter: z - .enum(["all", "delivered", "not_delivered"]) - .optional() - .default("all"), - cancellationFilter: z - .enum(["all", "cancelled", "not_cancelled"]) - .optional() - .default("all"), - flashDeliveryFilter: z - .enum(["all", "flash", "regular"]) - .optional() - .default("all"), -}); - -export const orderRouter = router({ - updateNotes: protectedProcedure - .input(updateOrderNotesSchema) - .mutation(async ({ input }) => { - const { orderId, adminNotes } = input; - - const result = await db - .update(orders) - .set({ - adminNotes: adminNotes || null, - }) - .where(eq(orders.id, orderId)) - .returning(); - - if (result.length === 0) { - throw new Error("Order not found"); - } - - return result[0]; - }), - - getFullOrder: protectedProcedure - .input(getFullOrderSchema) - .query(async ({ input }) => { - const { orderId } = input; - - const orderData = await db.query.orders.findFirst({ - where: eq(orders.id, orderId), - with: { - user: true, - address: true, - slot: true, - orderItems: { - with: { - product: { - with: { - unit: true, - }, - }, - }, - }, - payment: true, - paymentInfo: true, - }, - }); - - if (!orderData) { - throw new Error("Order not found"); - } - - // Get order status separately - const statusRecord = await db.query.orderStatus.findFirst({ - where: eq(orderStatus.orderId, orderId), - }); - - let status: "pending" | "delivered" | "cancelled" = "pending"; - if (statusRecord?.isCancelled) { - status = "cancelled"; - } else if (statusRecord?.isDelivered) { - status = "delivered"; - } - - // Get refund details if order is cancelled - let refund = null; - if (status === "cancelled") { - refund = await db.query.refunds.findFirst({ - where: eq(refunds.orderId, orderId), - }); - } - - return { - id: orderData.id, - readableId: orderData.id, - customerName: `${orderData.user.name}`, - customerEmail: orderData.user.email, - customerMobile: orderData.user.mobile, - address: { - line1: orderData.address.addressLine1, - line2: orderData.address.addressLine2, - city: orderData.address.city, - state: orderData.address.state, - pincode: orderData.address.pincode, - phone: orderData.address.phone, - }, - slotInfo: orderData.slot - ? { - time: orderData.slot.deliveryTime.toISOString(), - sequence: orderData.slot.deliverySequence, - } - : null, - isCod: orderData.isCod, - isOnlinePayment: orderData.isOnlinePayment, - totalAmount: orderData.totalAmount, - adminNotes: orderData.adminNotes, - userNotes: orderData.userNotes, - createdAt: orderData.createdAt, - status, - isPackaged: - orderData.orderItems.every((item) => item.is_packaged) || false, - isDelivered: statusRecord?.isDelivered || false, - items: orderData.orderItems.map((item) => ({ - id: item.id, - name: item.product.name, - quantity: item.quantity, - price: item.price, - unit: item.product.unit?.shortNotation, - amount: - parseFloat(item.price.toString()) * - parseFloat(item.quantity || "0"), - })), - payment: orderData.payment - ? { - status: orderData.payment.status, - gateway: orderData.payment.gateway, - merchantOrderId: orderData.payment.merchantOrderId, - } - : null, - paymentInfo: orderData.paymentInfo - ? { - status: orderData.paymentInfo.status, - gateway: orderData.paymentInfo.gateway, - merchantOrderId: orderData.paymentInfo.merchantOrderId, - } - : null, - // Cancellation details (only present for cancelled orders) - cancelReason: statusRecord?.cancelReason || null, - cancellationReviewed: statusRecord?.cancellationReviewed || false, - isRefundDone: refund?.refundStatus === "processed" || false, - }; - }), - - getOrderDetails: protectedProcedure - .input(getOrderDetailsSchema) - .query(async ({ input }) => { - const { orderId } = input; - - // Single optimized query with all relations - const orderData = await db.query.orders.findFirst({ - where: eq(orders.id, orderId), - with: { - user: true, - address: true, - slot: true, - orderItems: { - with: { - product: { - with: { - unit: true, - }, - }, - }, - }, - payment: true, - paymentInfo: true, - orderStatus: true, // Include in main query - refunds: true, // Include in main query - }, - }); - - if (!orderData) { - throw new Error("Order not found"); - } - - // Get coupon usage for this specific order using new orderId field - const couponUsageData = await db.query.couponUsage.findMany({ - where: eq(couponUsage.orderId, orderData.id), // Use new orderId field - with: { - coupon: true, - }, - }); - - let couponData = null; - if (couponUsageData.length > 0) { - // Calculate total discount from multiple coupons - let totalDiscountAmount = 0; - const orderTotal = parseFloat(orderData.totalAmount.toString()); - - for (const usage of couponUsageData) { - let discountAmount = 0; - - if (usage.coupon.discountPercent) { - discountAmount = - (orderTotal * - parseFloat(usage.coupon.discountPercent.toString())) / - 100; - } else if (usage.coupon.flatDiscount) { - discountAmount = parseFloat(usage.coupon.flatDiscount.toString()); - } - - // Apply max value limit if set - if ( - usage.coupon.maxValue && - discountAmount > parseFloat(usage.coupon.maxValue.toString()) - ) { - discountAmount = parseFloat(usage.coupon.maxValue.toString()); - } - - totalDiscountAmount += discountAmount; - } - - couponData = { - couponCode: couponUsageData - .map((u) => u.coupon.couponCode) - .join(", "), - couponDescription: `${couponUsageData.length} coupons applied`, - discountAmount: totalDiscountAmount, - }; - } - - // Status determination from included relation - const statusRecord = orderData.orderStatus?.[0]; - let status: "pending" | "delivered" | "cancelled" = "pending"; - if (statusRecord?.isCancelled) { - status = "cancelled"; - } else if (statusRecord?.isDelivered) { - status = "delivered"; - } - - // Always include refund data (will be null/undefined if not cancelled) - const refund = orderData.refunds?.[0]; - - return { - id: orderData.id, - readableId: orderData.id, - userId: orderData.user.id, - customerName: `${orderData.user.name}`, - customerEmail: orderData.user.email, - customerMobile: orderData.user.mobile, - address: { - name: orderData.address.name, - line1: orderData.address.addressLine1, - line2: orderData.address.addressLine2, - city: orderData.address.city, - state: orderData.address.state, - pincode: orderData.address.pincode, - phone: orderData.address.phone, - }, - slotInfo: orderData.slot - ? { - time: orderData.slot.deliveryTime.toISOString(), - sequence: orderData.slot.deliverySequence, - } - : null, - isCod: orderData.isCod, - isOnlinePayment: orderData.isOnlinePayment, - totalAmount: parseFloat(orderData.totalAmount?.toString() || '0') - parseFloat(orderData.deliveryCharge?.toString() || '0'), - deliveryCharge: parseFloat(orderData.deliveryCharge?.toString() || '0'), - adminNotes: orderData.adminNotes, - userNotes: orderData.userNotes, - createdAt: orderData.createdAt, - status, - isPackaged: statusRecord?.isPackaged || false, - isDelivered: statusRecord?.isDelivered || false, - items: orderData.orderItems.map((item) => ({ - id: item.id, - name: item.product.name, - quantity: item.quantity, - productSize: item.product.productQuantity, - price: item.price, - unit: item.product.unit?.shortNotation, - amount: - parseFloat(item.price.toString()) * - parseFloat(item.quantity || "0"), - isPackaged: item.is_packaged, - isPackageVerified: item.is_package_verified, - })), - payment: orderData.payment - ? { - status: orderData.payment.status, - gateway: orderData.payment.gateway, - merchantOrderId: orderData.payment.merchantOrderId, - } - : null, - paymentInfo: orderData.paymentInfo - ? { - status: orderData.paymentInfo.status, - gateway: orderData.paymentInfo.gateway, - merchantOrderId: orderData.paymentInfo.merchantOrderId, - } - : null, - // Cancellation details (always included, null if not cancelled) - cancelReason: statusRecord?.cancelReason || null, - cancellationReviewed: statusRecord?.cancellationReviewed || false, - isRefundDone: refund?.refundStatus === "processed" || false, - refundStatus: refund?.refundStatus as RefundStatus, - refundAmount: refund?.refundAmount - ? parseFloat(refund.refundAmount.toString()) - : null, - // Coupon information - couponData: couponData, - couponCode: couponData?.couponCode || null, - couponDescription: couponData?.couponDescription || null, - discountAmount: couponData?.discountAmount || null, - orderStatus: statusRecord, - refundRecord: refund, - isFlashDelivery: orderData.isFlashDelivery, - }; - }), - - updatePackaged: protectedProcedure - .input(updatePackagedSchema) - .mutation(async ({ input }) => { - const { orderId, isPackaged } = input; - - // Update all order items to the specified packaged state - await db - .update(orderItems) - .set({ is_packaged: isPackaged }) - .where(eq(orderItems.orderId, parseInt(orderId))); - - // Also update the order status table for backward compatibility - if (!isPackaged) { - await db - .update(orderStatus) - .set({ isPackaged, isDelivered: false }) - .where(eq(orderStatus.orderId, parseInt(orderId))); - } else { - await db - .update(orderStatus) - .set({ isPackaged }) - .where(eq(orderStatus.orderId, parseInt(orderId))); - } - - const order = await db.query.orders.findFirst({ - where: eq(orders.id, parseInt(orderId)), - }); - if (order) await sendOrderPackagedNotification(order.userId, orderId); - - return { success: true }; - }), - - updateDelivered: protectedProcedure - .input(updateDeliveredSchema) - .mutation(async ({ input }) => { - const { orderId, isDelivered } = input; - - await db - .update(orderStatus) - .set({ isDelivered }) - .where(eq(orderStatus.orderId, parseInt(orderId))); - - const order = await db.query.orders.findFirst({ - where: eq(orders.id, parseInt(orderId)), - }); - if (order) await sendOrderDeliveredNotification(order.userId, orderId); - - return { success: true }; - }), - - updateOrderItemPackaging: protectedProcedure - .input(updateOrderItemPackagingSchema) - .mutation(async ({ input }) => { - const { orderItemId, isPackaged, isPackageVerified } = input; - - // Validate that orderItem exists - const orderItem = await db.query.orderItems.findFirst({ - where: eq(orderItems.id, orderItemId), - }); - - if (!orderItem) { - throw new ApiError("Order item not found", 404); - } - - // Build update object with only provided fields - const updateData: any = {}; - if (isPackaged !== undefined) { - updateData.is_packaged = isPackaged; - } - if (isPackageVerified !== undefined) { - updateData.is_package_verified = isPackageVerified; - } - - // Update the order item - await db - .update(orderItems) - .set(updateData) - .where(eq(orderItems.id, orderItemId)); - - return { success: true }; - }), - - removeDeliveryCharge: protectedProcedure - .input(z.object({ orderId: z.number() })) - .mutation(async ({ input }) => { - const { orderId } = input; - - const order = await db.query.orders.findFirst({ - where: eq(orders.id, orderId), - }); - - if (!order) { - throw new Error('Order not found'); - } - - const currentDeliveryCharge = parseFloat(order.deliveryCharge?.toString() || '0'); - const currentTotalAmount = parseFloat(order.totalAmount?.toString() || '0'); - const newTotalAmount = currentTotalAmount - currentDeliveryCharge; - - await db - .update(orders) - .set({ - deliveryCharge: '0', - totalAmount: newTotalAmount.toString() - }) - .where(eq(orders.id, orderId)); - - return { success: true, message: 'Delivery charge removed' }; - }), - - getSlotOrders: protectedProcedure - .input(getSlotOrdersSchema) - .query(async ({ input }) => { - const { slotId } = input; - - const slotOrders = await db.query.orders.findMany({ - where: eq(orders.slotId, parseInt(slotId)), - with: { - user: true, - address: true, - slot: true, - orderItems: { - with: { - product: { - with: { - unit: true, - }, - }, - }, - }, - orderStatus: true, - }, - }); - - const filteredOrders = slotOrders.filter((order) => { - const statusRecord = order.orderStatus[0]; - return ( - order.isCod || - (statusRecord && statusRecord.paymentStatus === "success") - ); - }); - - const formattedOrders = filteredOrders.map((order) => { - const statusRecord = order.orderStatus[0]; // assuming one status per order - let status: "pending" | "delivered" | "cancelled" = "pending"; - if (statusRecord?.isCancelled) { - status = "cancelled"; - } else if (statusRecord?.isDelivered) { - status = "delivered"; - } - - const items = order.orderItems.map((item) => ({ - id: item.id, - name: item.product.name, - quantity: parseFloat(item.quantity), - price: parseFloat(item.price.toString()), - amount: parseFloat(item.quantity) * parseFloat(item.price.toString()), - unit: item.product.unit?.shortNotation || "", - isPackaged: item.is_packaged, - isPackageVerified: item.is_package_verified, - })); - - return { - id: order.id, - readableId: order.id, - customerName: order.user.name, - address: `${order.address.addressLine1}${ - order.address.addressLine2 ? `, ${order.address.addressLine2}` : "" - }, ${order.address.city}, ${order.address.state} - ${ - order.address.pincode - }, Phone: ${order.address.phone}`, - addressId: order.addressId, - latitude: order.address.adminLatitude ?? order.address.latitude, - longitude: order.address.adminLongitude ?? order.address.longitude, - totalAmount: parseFloat(order.totalAmount), - items, - deliveryTime: order.slot?.deliveryTime.toISOString() || null, - status, - isPackaged: - order.orderItems.every((item) => item.is_packaged) || false, - isDelivered: statusRecord?.isDelivered || false, - isCod: order.isCod, - paymentMode: order.isCod ? "COD" : "Online", - paymentStatus: statusRecord?.paymentStatus || "pending", - slotId: order.slotId, - adminNotes: order.adminNotes, - userNotes: order.userNotes, - }; - }); - - return { success: true, data: formattedOrders }; - }), - - getTodaysOrders: protectedProcedure - .input(getTodaysOrdersSchema) - .query(async ({ input }) => { - const { slotId } = input; - const start = dayjs().startOf("day").toDate(); - const end = dayjs().endOf("day").toDate(); - - let whereCondition = and( - gte(orders.createdAt, start), - lt(orders.createdAt, end) - ); - - if (slotId) { - whereCondition = and( - whereCondition, - eq(orders.slotId, parseInt(slotId)) - ); - } - - const todaysOrders = await db.query.orders.findMany({ - where: whereCondition, - with: { - user: true, - address: true, - slot: true, - orderItems: { - with: { - product: { - with: { - unit: true, - }, - }, - }, - }, - orderStatus: true, - }, - }); - - const filteredOrders = todaysOrders.filter((order) => { - const statusRecord = order.orderStatus[0]; - return ( - order.isCod || - (statusRecord && statusRecord.paymentStatus === "success") - ); - }); - - const formattedOrders = filteredOrders.map((order) => { - const statusRecord = order.orderStatus[0]; // assuming one status per order - let status: "pending" | "delivered" | "cancelled" = "pending"; - if (statusRecord?.isCancelled) { - status = "cancelled"; - } else if (statusRecord?.isDelivered) { - status = "delivered"; - } - - const items = order.orderItems.map((item) => ({ - name: item.product.name, - quantity: parseFloat(item.quantity), - price: parseFloat(item.price.toString()), - amount: parseFloat(item.quantity) * parseFloat(item.price.toString()), - unit: item.product.unit?.shortNotation || "", - })); - - return { - orderId: order.id.toString(), - readableId: order.id, - customerName: order.user.name, - address: `${order.address.addressLine1}${ - order.address.addressLine2 ? `, ${order.address.addressLine2}` : "" - }, ${order.address.city}, ${order.address.state} - ${ - order.address.pincode - }`, - totalAmount: parseFloat(order.totalAmount), - items, - deliveryTime: order.slot?.deliveryTime.toISOString() || null, - status, - isPackaged: - order.orderItems.every((item) => item.is_packaged) || false, - isDelivered: statusRecord?.isDelivered || false, - isCod: order.isCod, - paymentMode: order.isCod ? "COD" : "Online", - paymentStatus: statusRecord?.paymentStatus || "pending", - slotId: order.slotId, - adminNotes: order.adminNotes, - userNotes: order.userNotes, - }; - }); - - return { success: true, data: formattedOrders }; - }), - - updateAddressCoords: protectedProcedure - .input( - z.object({ - addressId: z.number(), - latitude: z.number(), - longitude: z.number(), - }) - ) - .mutation(async ({ input }) => { - const { addressId, latitude, longitude } = input; - - const result = await db - .update(addresses) - .set({ - adminLatitude: latitude, - adminLongitude: longitude, - }) - .where(eq(addresses.id, addressId)) - .returning(); - - if (result.length === 0) { - throw new ApiError("Address not found", 404); - } - - return { success: true }; - }), - - getAll: protectedProcedure - .input(getAllOrdersSchema) - .query(async ({ input }) => { - try { - const { - cursor, - limit, - slotId, - packagedFilter, - deliveredFilter, - cancellationFilter, - flashDeliveryFilter, - } = input; - - let whereCondition: SQL | undefined = eq(orders.id, orders.id); // always true - if (cursor) { - whereCondition = and(whereCondition, lt(orders.id, cursor)); - } - if (slotId) { - whereCondition = and(whereCondition, eq(orders.slotId, slotId)); - } - if (packagedFilter === "packaged") { - whereCondition = and( - whereCondition, - eq(orderStatus.isPackaged, true) - ); - } else if (packagedFilter === "not_packaged") { - whereCondition = and( - whereCondition, - eq(orderStatus.isPackaged, false) - ); - } - if (deliveredFilter === "delivered") { - whereCondition = and( - whereCondition, - eq(orderStatus.isDelivered, true) - ); - } else if (deliveredFilter === "not_delivered") { - whereCondition = and( - whereCondition, - eq(orderStatus.isDelivered, false) - ); - } - if (cancellationFilter === "cancelled") { - whereCondition = and( - whereCondition, - eq(orderStatus.isCancelled, true) - ); - } else if (cancellationFilter === "not_cancelled") { - whereCondition = and( - whereCondition, - eq(orderStatus.isCancelled, false) - ); - } - if (flashDeliveryFilter === "flash") { - whereCondition = and( - whereCondition, - eq(orders.isFlashDelivery, true) - ); - } else if (flashDeliveryFilter === "regular") { - whereCondition = and( - whereCondition, - eq(orders.isFlashDelivery, false) - ); - } - - const allOrders = await db.query.orders.findMany({ - where: whereCondition, - orderBy: desc(orders.createdAt), - limit: limit + 1, // fetch one extra to check if there's more - with: { - user: true, - address: true, - slot: true, - orderItems: { - with: { - product: { - with: { - unit: true, - }, - }, - }, - }, - orderStatus: true, - }, - }); - - const hasMore = allOrders.length > limit; - const ordersToReturn = hasMore ? allOrders.slice(0, limit) : allOrders; - - const userIds = [...new Set(ordersToReturn.map(o => o.userId))]; - const negativityScores = await getMultipleUserNegativityScores(userIds); - - const filteredOrders = ordersToReturn.filter((order) => { - const statusRecord = order.orderStatus[0]; - return ( - order.isCod || - (statusRecord && statusRecord.paymentStatus === "success") - ); - }); - - const formattedOrders = filteredOrders.map((order) => { - const statusRecord = order.orderStatus[0]; - let status: "pending" | "delivered" | "cancelled" = "pending"; - if (statusRecord?.isCancelled) { - status = "cancelled"; - } else if (statusRecord?.isDelivered) { - status = "delivered"; - } - - const items = order.orderItems - .map((item) => ({ - id: item.id, - name: item.product.name, - quantity: parseFloat(item.quantity), - price: parseFloat(item.price.toString()), - amount: - parseFloat(item.quantity) * parseFloat(item.price.toString()), - unit: item.product.unit?.shortNotation || "", - productSize: item.product.productQuantity, - isPackaged: item.is_packaged, - isPackageVerified: item.is_package_verified, - })) - .sort((first, second) => first.id - second.id); - dayjs.extend(utc); - return { - id: order.id, - orderId: order.id.toString(), - readableId: order.id, - customerName: order.user.name, - customerMobile: order.user.mobile, - address: `${order.address.addressLine1}${ - order.address.addressLine2 - ? `, ${order.address.addressLine2}` - : "" - }, ${order.address.city}, ${order.address.state} - ${ - order.address.pincode - }, Phone: ${order.address.phone}`, - addressId: order.addressId, - latitude: order.address.adminLatitude ?? order.address.latitude, - longitude: order.address.adminLongitude ?? order.address.longitude, - totalAmount: parseFloat(order.totalAmount), - deliveryCharge: parseFloat(order.deliveryCharge || "0"), - items, - createdAt: order.createdAt, - // deliveryTime: order.slot ? dayjs.utc(order.slot.deliveryTime).format('ddd, MMM D • h:mm A') : 'Not scheduled', - deliveryTime: order.slot?.deliveryTime.toISOString() || null, - status, - isPackaged: - order.orderItems.every((item) => item.is_packaged) || false, - isDelivered: statusRecord?.isDelivered || false, - isCod: order.isCod, - isFlashDelivery: order.isFlashDelivery, - userNotes: order.userNotes, - adminNotes: order.adminNotes, - userNegativityScore: negativityScores[order.userId] || 0, - }; - }); - - return { - orders: formattedOrders, - nextCursor: hasMore - ? ordersToReturn[ordersToReturn.length - 1].id - : undefined, - }; - } catch (e) { - console.log({ e }); - } - }), - - rebalanceSlots: protectedProcedure - .input(z.object({ slotIds: z.array(z.number()).min(1).max(50) })) - .mutation(async ({ input }) => { - const slotIds = input.slotIds; - - const ordersList = await db.query.orders.findMany({ - where: inArray(orders.slotId, slotIds), - with: { - orderItems: { - with: { - product: true - } - }, - couponUsages: { - with: { - coupon: true - } - }, - } - }); - - const processedOrdersData = ordersList.map((order) => { - - let newTotal = order.orderItems.reduce((acc,item) => { - const latestPrice = +item.product.price; - const amount = (latestPrice * Number(item.quantity)); - return acc+amount; - },0) - - order.orderItems.forEach(item => { - item.price = item.product.price; - item.discountedPrice = item.product.price - }) - - const coupon = order.couponUsages[0]?.coupon; - - let discount = 0; - if(coupon && !coupon.isInvalidated && (!coupon.validTill || new Date(coupon.validTill) > new Date())) { - const proportion = Number(order.orderGroupProportion || 1); - if(coupon.discountPercent) { - const maxDiscount = Number(coupon.maxValue || Infinity) * proportion; - discount = Math.min((newTotal * parseFloat(coupon.discountPercent)) / 100, maxDiscount); - } - else { - discount = Number(coupon.flatDiscount) * proportion; - } - } - newTotal -= discount - - const { couponUsages, orderItems: orderItemsRaw, ...rest} = order; - const updatedOrderItems = orderItemsRaw.map(item => { - const { product, ...rawOrderItem } = item; - return rawOrderItem; - }) - return {order: rest, updatedOrderItems, newTotal } - }) - - const updatedOrderIds: number[] = []; - await db.transaction(async (tx) => { - for (const { order, updatedOrderItems, newTotal } of processedOrdersData) { - await tx.update(orders).set({ totalAmount: newTotal.toString() }).where(eq(orders.id, order.id)); - updatedOrderIds.push(order.id); - - for (const item of updatedOrderItems) { - await tx.update(orderItems).set({ - price: item.price, - discountedPrice: item.discountedPrice - }).where(eq(orderItems.id, item.id)); - } - } - }); - - return { success: true, updatedOrders: updatedOrderIds, message: `Rebalanced ${updatedOrderIds.length} orders.` }; - }), - - cancelOrder: protectedProcedure - .input(z.object({ - orderId: z.number(), - reason: z.string().min(1, "Cancellation reason is required"), - })) - .mutation(async ({ input }) => { - const { orderId, reason } = input; - - const order = await db.query.orders.findFirst({ - where: eq(orders.id, orderId), - with: { - orderStatus: true, - }, - }); - - if (!order) { - throw new ApiError("Order not found", 404); - } - - const status = order.orderStatus[0]; - if (!status) { - throw new ApiError("Order status not found", 400); - } - - if (status.isCancelled) { - throw new ApiError("Order is already cancelled", 400); - } - - if (status.isDelivered) { - throw new ApiError("Cannot cancel delivered order", 400); - } - - const result = await db.transaction(async (tx) => { - await tx - .update(orderStatus) - .set({ - isCancelled: true, - isCancelledByAdmin: true, - cancelReason: reason, - cancellationAdminNotes: reason, - cancellationReviewed: true, - cancellationReviewedAt: new Date(), - }) - .where(eq(orderStatus.id, status.id)); - - const refundStatus = order.isCod ? "na" : "pending"; - - await tx.insert(refunds).values({ - orderId: order.id, - refundStatus, - }); - - return { orderId: order.id, userId: order.userId }; - }); - - // Publish to Redis for Telegram notification - await publishCancellation(result.orderId, 'admin', reason); - - return { success: true, message: "Order cancelled successfully" }; - }), -}); - -// {"id": "order_Rhh00qJNdjUp8o", "notes": {"retry": "true", "customerOrderId": "14"}, "amount": 21000, "entity": "order", "status": "created", "receipt": "order_14_retry", "attempts": 0, "currency": "INR", "offer_id": null, "signature": "6df20655021f1d6841340f2a2ef2ef9378cb3d43495ab09e85f08aea1a851583", "amount_due": 21000, "created_at": 1763575791, "payment_id": "pay_Rhh15cLL28YM7j", "amount_paid": 0} - -type RefundStatus = "success" | "pending" | "failed" | "none" | "na"; - -export async function deleteOrderById(orderId: number): Promise { - await db.transaction(async (tx) => { - await tx.delete(orderItems).where(eq(orderItems.orderId, orderId)); - await tx.delete(orderStatus).where(eq(orderStatus.orderId, orderId)); - await tx.delete(payments).where(eq(payments.orderId, orderId)); - await tx.delete(refunds).where(eq(refunds.orderId, orderId)); - await tx.delete(couponUsage).where(eq(couponUsage.orderId, orderId)); - await tx.delete(complaints).where(eq(complaints.orderId, orderId)); - await tx.delete(orders).where(eq(orders.id, orderId)); - }); -} - diff --git a/verifier/apis/payments.ts b/verifier/apis/payments.ts deleted file mode 100644 index 51de5fb..0000000 --- a/verifier/apis/payments.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { router, protectedProcedure } from "@/src/trpc/trpc-index" -import { z } from "zod"; -import { db } from "@/src/db/db_index" -import { - orders, - orderStatus, - payments, - refunds, -} from "@/src/db/schema"; -import { and, eq } from "drizzle-orm"; -import { ApiError } from "@/src/lib/api-error" -import { RazorpayPaymentService } from "@/src/lib/payments-utils" - -const initiateRefundSchema = z - .object({ - orderId: z.number(), - refundPercent: z.number().min(0).max(100).optional(), - refundAmount: z.number().min(0).optional(), - }) - .refine( - (data) => { - const hasPercent = data.refundPercent !== undefined; - const hasAmount = data.refundAmount !== undefined; - return (hasPercent && !hasAmount) || (!hasPercent && hasAmount); - }, - { - message: - "Provide either refundPercent or refundAmount, not both or neither", - } - ); - -export const adminPaymentsRouter = router({ - initiateRefund: protectedProcedure - .input(initiateRefundSchema) - .mutation(async ({ input }) => { - try { - const { orderId, refundPercent, refundAmount } = input; - - // Validate order exists - const order = await db.query.orders.findFirst({ - where: eq(orders.id, orderId), - }); - - if (!order) { - throw new ApiError("Order not found", 404); - } - - // Check if order is paid - const orderStatusRecord = await db.query.orderStatus.findFirst({ - where: eq(orderStatus.orderId, orderId), - }); - - if(order.isCod) { - throw new ApiError("Order is a Cash On Delivery. Not eligible for refund") - } - - if ( - !orderStatusRecord || - (orderStatusRecord.paymentStatus !== "success" && - !(order.isCod && orderStatusRecord.isDelivered)) - ) { - throw new ApiError("Order payment not verified or not eligible for refund", 400); - } - - // Calculate refund amount - let calculatedRefundAmount: number; - if (refundPercent !== undefined) { - calculatedRefundAmount = - (parseFloat(order.totalAmount) * refundPercent) / 100; - } else if (refundAmount !== undefined) { - calculatedRefundAmount = refundAmount; - if (calculatedRefundAmount > parseFloat(order.totalAmount)) { - throw new ApiError("Refund amount cannot exceed order total", 400); - } - } else { - throw new ApiError("Invalid refund parameters", 400); - } - - let razorpayRefund = null; - let merchantRefundId = null; - - // Get payment record for online payments - const payment = await db.query.payments.findFirst({ - where: and( - eq(payments.orderId, orderId), - eq(payments.status, "success") - ), - }); - - if (!payment || payment.status !== "success") { - throw new ApiError("Payment not found or not successful", 404); - } - - const payload = payment.payload as any; - // Initiate Razorpay refund - razorpayRefund = await RazorpayPaymentService.initiateRefund( - payload.payment_id, - Math.round(calculatedRefundAmount * 100) // Convert to paisa - ); - merchantRefundId = razorpayRefund.id; - - - - // Check if refund already exists for this order - const existingRefund = await db.query.refunds.findFirst({ - where: eq(refunds.orderId, orderId), - }); - - const refundStatus = "initiated"; - - if (existingRefund) { - // Update existing refund - await db - .update(refunds) - .set({ - refundAmount: calculatedRefundAmount.toString(), - refundStatus, - merchantRefundId, - refundProcessedAt: order.isCod ? new Date() : null, - }) - .where(eq(refunds.id, existingRefund.id)); - } else { - // Insert new refund - await db - .insert(refunds) - .values({ - orderId, - refundAmount: calculatedRefundAmount.toString(), - refundStatus, - merchantRefundId, - }); - } - - return { - refundId: merchantRefundId || `cod_${orderId}`, - amount: calculatedRefundAmount, - status: refundStatus, - message: order.isCod ? "COD refund processed successfully" : "Refund initiated successfully", - }; - } - catch(e) { - console.log(e); - throw new ApiError("Failed to initiate refund") - } - }), -}); diff --git a/verifier/apis/product-availability-schedules.ts b/verifier/apis/product-availability-schedules.ts deleted file mode 100644 index 47cdc43..0000000 --- a/verifier/apis/product-availability-schedules.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { router, protectedProcedure } from '@/src/trpc/trpc-index' -import { z } from 'zod'; -import { db } from '@/src/db/db_index' -import { productAvailabilitySchedules } from '@/src/db/schema' -import { eq } from 'drizzle-orm'; -import { refreshScheduleJobs } from '@/src/lib/automatedJobs'; - -const createScheduleSchema = z.object({ - scheduleName: z.string().min(1, "Schedule name is required"), - time: z.string().min(1, "Time is required").regex(/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/, "Invalid time format. Use HH:MM"), - action: z.enum(['in', 'out']), - productIds: z.array(z.number().int().positive()).min(1, "At least one product is required"), - groupIds: z.array(z.number().int().positive()).default([]), -}); - -const updateScheduleSchema = z.object({ - id: z.number().int().positive(), - updates: createScheduleSchema.partial().extend({ - scheduleName: z.string().min(1).optional(), - productIds: z.array(z.number().int().positive()).optional(), - groupIds: z.array(z.number().int().positive()).optional(), - }), -}); - -export const productAvailabilitySchedulesRouter = router({ - create: protectedProcedure - .input(createScheduleSchema) - .mutation(async ({ input, ctx }) => { - const { scheduleName, time, action, productIds, groupIds } = input; - - // Get staff user ID from auth middleware - const staffUserId = ctx.staffUser?.id; - if (!staffUserId) { - throw new Error("Unauthorized"); - } - - // Check if schedule name already exists - const existingSchedule = await db.query.productAvailabilitySchedules.findFirst({ - where: eq(productAvailabilitySchedules.scheduleName, scheduleName), - }); - if (existingSchedule) { - throw new Error("Schedule name already exists"); - } - - // Create schedule with arrays - const scheduleResult = await db.insert(productAvailabilitySchedules).values({ - scheduleName, - time, - action, - productIds, - groupIds, - }).returning(); - - // Refresh cron jobs to include new schedule - await refreshScheduleJobs(); - - return scheduleResult[0]; - }), - - getAll: protectedProcedure - .query(async () => { - const schedules = await db.query.productAvailabilitySchedules.findMany({ - orderBy: (productAvailabilitySchedules, { desc }) => [desc(productAvailabilitySchedules.createdAt)], - }); - - return schedules.map(schedule => ({ - ...schedule, - productCount: schedule.productIds.length, - groupCount: schedule.groupIds.length, - })); - }), - - getById: protectedProcedure - .input(z.object({ id: z.number().int().positive() })) - .query(async ({ input }) => { - const { id } = input; - - const schedule = await db.query.productAvailabilitySchedules.findFirst({ - where: eq(productAvailabilitySchedules.id, id), - }); - - if (!schedule) { - throw new Error("Schedule not found"); - } - - return schedule; - }), - - update: protectedProcedure - .input(updateScheduleSchema) - .mutation(async ({ input }) => { - const { id, updates } = input; - - // Check if schedule exists - const existingSchedule = await db.query.productAvailabilitySchedules.findFirst({ - where: eq(productAvailabilitySchedules.id, id), - }); - if (!existingSchedule) { - throw new Error("Schedule not found"); - } - - // Check schedule name uniqueness if being updated - if (updates.scheduleName && updates.scheduleName !== existingSchedule.scheduleName) { - const duplicateSchedule = await db.query.productAvailabilitySchedules.findFirst({ - where: eq(productAvailabilitySchedules.scheduleName, updates.scheduleName), - }); - if (duplicateSchedule) { - throw new Error("Schedule name already exists"); - } - } - - // Update schedule - const updateData: any = {}; - if (updates.scheduleName !== undefined) updateData.scheduleName = updates.scheduleName; - if (updates.time !== undefined) updateData.time = updates.time; - if (updates.action !== undefined) updateData.action = updates.action; - if (updates.productIds !== undefined) updateData.productIds = updates.productIds; - if (updates.groupIds !== undefined) updateData.groupIds = updates.groupIds; - updateData.lastUpdated = new Date(); - - const result = await db.update(productAvailabilitySchedules) - .set(updateData) - .where(eq(productAvailabilitySchedules.id, id)) - .returning(); - - if (result.length === 0) { - throw new Error("Failed to update schedule"); - } - - // Refresh cron jobs to reflect changes - await refreshScheduleJobs(); - - return result[0]; - }), - - delete: protectedProcedure - .input(z.object({ id: z.number().int().positive() })) - .mutation(async ({ input }) => { - const { id } = input; - - const result = await db.delete(productAvailabilitySchedules) - .where(eq(productAvailabilitySchedules.id, id)) - .returning(); - - if (result.length === 0) { - throw new Error("Schedule not found"); - } - - // Refresh cron jobs to remove deleted schedule - await refreshScheduleJobs(); - - return { message: "Schedule deleted successfully" }; - }), -}); diff --git a/verifier/apis/product.ts b/verifier/apis/product.ts deleted file mode 100644 index 4677c40..0000000 --- a/verifier/apis/product.ts +++ /dev/null @@ -1,758 +0,0 @@ -import { router, protectedProcedure } from '@/src/trpc/trpc-index' -import { z } from 'zod'; -import { db } from '@/src/db/db_index' -import { productInfo, units, specialDeals, productSlots, productTags, productReviews, users, productGroupInfo, productGroupMembership } from '@/src/db/schema' -import { eq, and, inArray, desc, sql } from 'drizzle-orm'; -import { ApiError } from '@/src/lib/api-error' -import { imageUploadS3, scaffoldAssetUrl, getOriginalUrlFromSignedUrl, claimUploadUrl } from '@/src/lib/s3-client' -import { deleteS3Image } from '@/src/lib/delete-image' -import type { SpecialDeal } from '@/src/db/types' -import { scheduleStoreInitialization } from '@/src/stores/store-initializer' - - -type CreateDeal = { - quantity: number; - price: number; - validTill: string; -}; - -export const productRouter = router({ - getProducts: protectedProcedure - .query(async ({ ctx }) => { - const products = await db.query.productInfo.findMany({ - orderBy: productInfo.name, - with: { - unit: true, - store: true, - }, - }); - - // Generate signed URLs for all product images - const productsWithSignedUrls = await Promise.all( - products.map(async (product) => ({ - ...product, - images: scaffoldAssetUrl((product.images as string[]) || []), - })) - ); - - return { - products: productsWithSignedUrls, - count: productsWithSignedUrls.length, - }; - }), - - getProductById: protectedProcedure - .input(z.object({ - id: z.number(), - })) - .query(async ({ input, ctx }) => { - const { id } = input; - - const product = await db.query.productInfo.findFirst({ - where: eq(productInfo.id, id), - with: { - unit: true, - }, - }); - - if (!product) { - throw new ApiError("Product not found", 404); - } - - // Fetch special deals for this product - const deals = await db.query.specialDeals.findMany({ - where: eq(specialDeals.productId, id), - orderBy: specialDeals.quantity, - }); - - // Fetch associated tags for this product - const productTagsData = await db.query.productTags.findMany({ - where: eq(productTags.productId, id), - with: { - tag: true, - }, - }); - - // Generate signed URLs for product images - const productWithSignedUrls = { - ...product, - images: scaffoldAssetUrl((product.images as string[]) || []), - deals, - tags: productTagsData.map(pt => pt.tag), - }; - - return { - product: productWithSignedUrls, - }; - }), - - deleteProduct: protectedProcedure - .input(z.object({ - id: z.number(), - })) - .mutation(async ({ input, ctx }) => { - const { id } = input; - - const [deletedProduct] = await db - .delete(productInfo) - .where(eq(productInfo.id, id)) - .returning(); - - if (!deletedProduct) { - throw new ApiError("Product not found", 404); - } - - // Reinitialize stores to reflect changes - scheduleStoreInitialization() - - return { - message: "Product deleted successfully", - }; - }), - - createProduct: protectedProcedure - .input(z.object({ - name: z.string().min(1), - shortDescription: z.string().optional(), - longDescription: z.string().optional(), - unitId: z.number(), - storeId: z.number(), - price: z.number(), - marketPrice: z.number().optional(), - incrementStep: z.number().default(1), - productQuantity: z.number().default(1), - isSuspended: z.boolean().default(false), - isFlashAvailable: z.boolean().default(false), - flashPrice: z.number().optional(), - deals: z.array(z.object({ - quantity: z.number(), - price: z.number(), - validTill: z.string(), - })).optional(), - tagIds: z.array(z.number()).optional(), - imageKeys: z.array(z.string()).optional(), - })) - .mutation(async ({ input }) => { - const { - name, shortDescription, longDescription, unitId, storeId, - price, marketPrice, incrementStep, productQuantity, - isSuspended, isFlashAvailable, flashPrice, - deals, tagIds, imageKeys - } = input; - - // Validation - if (!name || !unitId || !storeId || !price) { - throw new ApiError("Name, unitId, storeId, and price are required", 400); - } - - // Check for duplicate name - const existingProduct = await db.query.productInfo.findFirst({ - where: eq(productInfo.name, name.trim()), - }); - if (existingProduct) { - throw new ApiError("A product with this name already exists", 400); - } - - // Check if unit exists - const unit = await db.query.units.findFirst({ - where: eq(units.id, unitId), - }); - if (!unit) { - throw new ApiError("Invalid unit ID", 400); - } - - console.log(imageKeys) - const [newProduct] = await db - .insert(productInfo) - .values({ - name: name.trim(), - shortDescription, - longDescription, - unitId, - storeId, - price: price.toString(), - marketPrice: marketPrice?.toString(), - incrementStep, - productQuantity, - isSuspended, - isFlashAvailable, - flashPrice: flashPrice?.toString(), - images: imageKeys || [], - }) - .returning(); - - // Handle deals - if (deals && deals.length > 0) { - const dealInserts = deals.map(deal => ({ - productId: newProduct.id, - quantity: deal.quantity.toString(), - price: deal.price.toString(), - validTill: new Date(deal.validTill), - })); - await db.insert(specialDeals).values(dealInserts); - } - - // Handle tags - if (tagIds && tagIds.length > 0) { - const tagAssociations = tagIds.map(tagId => ({ - productId: newProduct.id, - tagId, - })); - await db.insert(productTags).values(tagAssociations); - } - - // Claim upload URLs - if (imageKeys && imageKeys.length > 0) { - for (const key of imageKeys) { - try { - await claimUploadUrl(key); - } catch (e) { - console.warn(`Failed to claim upload URL for key: ${key}`, e); - } - } - } - - scheduleStoreInitialization(); - - return { - product: newProduct, - message: "Product created successfully", - }; - }), - - updateProduct: protectedProcedure - .input(z.object({ - id: z.number(), - name: z.string().min(1).optional(), - shortDescription: z.string().optional(), - longDescription: z.string().optional(), - unitId: z.number().optional(), - storeId: z.number().optional(), - price: z.number().optional(), - marketPrice: z.number().optional(), - incrementStep: z.number().optional(), - productQuantity: z.number().optional(), - isSuspended: z.boolean().optional(), - isFlashAvailable: z.boolean().optional(), - flashPrice: z.number().optional(), - deals: z.array(z.object({ - quantity: z.number(), - price: z.number(), - validTill: z.string(), - })).optional(), - tagIds: z.array(z.number()).optional(), - newImageKeys: z.array(z.string()).optional(), - imagesToDelete: z.array(z.string()).optional(), - })) - .mutation(async ({ input }) => { - const { id, newImageKeys, imagesToDelete, deals, tagIds, ...updateData } = input; - - // Get current product - const currentProduct = await db.query.productInfo.findFirst({ - where: eq(productInfo.id, id), - }); - if (!currentProduct) { - throw new ApiError("Product not found", 404); - } - - // Handle image deletions - let currentImages = (currentProduct.images as string[]) || []; - if (imagesToDelete && imagesToDelete.length > 0) { - for (const imageUrl of imagesToDelete) { - try { - await deleteS3Image(imageUrl); - } catch (e) { - console.error(`Failed to delete image: ${imageUrl}`, e); - } - } - currentImages = currentImages.filter(img => { - //!imagesToDelete.includes(img) - const isRemoved = imagesToDelete.some(item => item.includes(img)); - return !isRemoved; - }); - } - - // Add new images - if (newImageKeys && newImageKeys.length > 0) { - currentImages = [...currentImages, ...newImageKeys]; - - for (const key of newImageKeys) { - try { - await claimUploadUrl(key); - } catch (e) { - console.warn(`Failed to claim upload URL for key: ${key}`, e); - } - } - } - - // Update product - convert numeric fields to strings for PostgreSQL numeric type - const { price, marketPrice, flashPrice, ...otherData } = updateData; - const [updatedProduct] = await db - .update(productInfo) - .set({ - ...otherData, - ...(price !== undefined && { price: price.toString() }), - ...(marketPrice !== undefined && { marketPrice: marketPrice.toString() }), - ...(flashPrice !== undefined && { flashPrice: flashPrice.toString() }), - images: currentImages, - }) - .where(eq(productInfo.id, id)) - .returning(); - - // Handle deals update - if (deals !== undefined) { - await db.delete(specialDeals).where(eq(specialDeals.productId, id)); - if (deals.length > 0) { - const dealInserts = deals.map(deal => ({ - productId: id, - quantity: deal.quantity.toString(), - price: deal.price.toString(), - validTill: new Date(deal.validTill), - })); - await db.insert(specialDeals).values(dealInserts); - } - } - - // Handle tags update - if (tagIds !== undefined) { - await db.delete(productTags).where(eq(productTags.productId, id)); - if (tagIds.length > 0) { - const tagAssociations = tagIds.map(tagId => ({ - productId: id, - tagId, - })); - await db.insert(productTags).values(tagAssociations); - } - } - - scheduleStoreInitialization(); - - return { - product: updatedProduct, - message: "Product updated successfully", - }; - }), - - toggleOutOfStock: protectedProcedure - .input(z.object({ - id: z.number(), - })) - .mutation(async ({ input, ctx }) => { - const { id } = input; - - const product = await db.query.productInfo.findFirst({ - where: eq(productInfo.id, id), - }); - - if (!product) { - throw new ApiError("Product not found", 404); - } - - const [updatedProduct] = await db - .update(productInfo) - .set({ - isOutOfStock: !product.isOutOfStock, - }) - .where(eq(productInfo.id, id)) - .returning(); - - // Reinitialize stores to reflect changes - scheduleStoreInitialization() - - return { - product: updatedProduct, - message: `Product marked as ${updatedProduct.isOutOfStock ? 'out of stock' : 'in stock'}`, - }; - }), - - updateSlotProducts: protectedProcedure - .input(z.object({ - slotId: z.string(), - productIds: z.array(z.string()), - })) - .mutation(async ({ input, ctx }) => { - const { slotId, productIds } = input; - - if (!Array.isArray(productIds)) { - throw new ApiError("productIds must be an array", 400); - } - - // Get current associations - const currentAssociations = await db.query.productSlots.findMany({ - where: eq(productSlots.slotId, parseInt(slotId)), - columns: { - productId: true, - }, - }); - - const currentProductIds = currentAssociations.map(assoc => assoc.productId); - const newProductIds = productIds.map((id: string) => parseInt(id)); - - // Find products to add and remove - const productsToAdd = newProductIds.filter(id => !currentProductIds.includes(id)); - const productsToRemove = currentProductIds.filter(id => !newProductIds.includes(id)); - - // Remove associations for products that are no longer selected - if (productsToRemove.length > 0) { - await db.delete(productSlots).where( - and( - eq(productSlots.slotId, parseInt(slotId)), - inArray(productSlots.productId, productsToRemove) - ) - ); - } - - // Add associations for newly selected products - if (productsToAdd.length > 0) { - const newAssociations = productsToAdd.map(productId => ({ - productId, - slotId: parseInt(slotId), - })); - - await db.insert(productSlots).values(newAssociations); - } - - // Reinitialize stores to reflect changes - scheduleStoreInitialization() - - return { - message: "Slot products updated successfully", - added: productsToAdd.length, - removed: productsToRemove.length, - }; - }), - - getSlotProductIds: protectedProcedure - .input(z.object({ - slotId: z.string(), - })) - .query(async ({ input, ctx }) => { - const { slotId } = input; - - const associations = await db.query.productSlots.findMany({ - where: eq(productSlots.slotId, parseInt(slotId)), - columns: { - productId: true, - }, - }); - - const productIds = associations.map(assoc => assoc.productId); - - return { - productIds, - }; - }), - - getSlotsProductIds: protectedProcedure - .input(z.object({ - slotIds: z.array(z.number()), - })) - .query(async ({ input, ctx }) => { - const { slotIds } = input; - - if (!Array.isArray(slotIds)) { - throw new ApiError("slotIds must be an array", 400); - } - - if (slotIds.length === 0) { - return {}; - } - - // Fetch all associations for the requested slots - const associations = await db.query.productSlots.findMany({ - where: inArray(productSlots.slotId, slotIds), - columns: { - slotId: true, - productId: true, - }, - }); - - // Group by slotId - const result = associations.reduce((acc, assoc) => { - if (!acc[assoc.slotId]) { - acc[assoc.slotId] = []; - } - acc[assoc.slotId].push(assoc.productId); - return acc; - }, {} as Record); - - // Ensure all requested slots have entries (even if empty) - slotIds.forEach(slotId => { - if (!result[slotId]) { - result[slotId] = []; - } - }); - - return result; - }), - - getProductReviews: protectedProcedure - .input(z.object({ - productId: z.number().int().positive(), - limit: z.number().int().min(1).max(50).optional().default(10), - offset: z.number().int().min(0).optional().default(0), - })) - .query(async ({ input }) => { - const { productId, limit, offset } = input; - - const reviews = await db - .select({ - id: productReviews.id, - reviewBody: productReviews.reviewBody, - ratings: productReviews.ratings, - imageUrls: productReviews.imageUrls, - reviewTime: productReviews.reviewTime, - adminResponse: productReviews.adminResponse, - adminResponseImages: productReviews.adminResponseImages, - 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); - - // Generate signed URLs for images - const reviewsWithSignedUrls = await Promise.all( - reviews.map(async (review) => ({ - ...review, - signedImageUrls: scaffoldAssetUrl((review.imageUrls as string[]) || []), - signedAdminImageUrls: scaffoldAssetUrl((review.adminResponseImages as string[]) || []), - })) - ); - - // Check if more reviews exist - const totalCountResult = await db - .select({ count: sql`count(*)` }) - .from(productReviews) - .where(eq(productReviews.productId, productId)); - - const totalCount = Number(totalCountResult[0].count); - const hasMore = offset + limit < totalCount; - - return { reviews: reviewsWithSignedUrls, hasMore }; - }), - - respondToReview: protectedProcedure - .input(z.object({ - reviewId: z.number().int().positive(), - adminResponse: z.string().optional(), - adminResponseImages: z.array(z.string()).optional().default([]), - uploadUrls: z.array(z.string()).optional().default([]), - })) - .mutation(async ({ input }) => { - const { reviewId, adminResponse, adminResponseImages, uploadUrls } = input; - - const [updatedReview] = await db - .update(productReviews) - .set({ - adminResponse, - adminResponseImages, - }) - .where(eq(productReviews.id, reviewId)) - .returning(); - - if (!updatedReview) { - throw new ApiError('Review not found', 404); - } - - // Claim upload URLs - if (uploadUrls && uploadUrls.length > 0) { - // const { claimUploadUrl } = await import('@/src/lib/s3-client'); - await Promise.all(uploadUrls.map(url => claimUploadUrl(url))); - } - - return { success: true, review: updatedReview }; - }), - - getGroups: protectedProcedure - .query(async ({ ctx }) => { - const groups = await db.query.productGroupInfo.findMany({ - with: { - memberships: { - with: { - product: true, - }, - }, - }, - orderBy: desc(productGroupInfo.createdAt), - }); - - return { - groups: groups.map(group => ({ - ...group, - products: group.memberships.map(m => m.product), - productCount: group.memberships.length, - })), - }; - }), - - createGroup: protectedProcedure - .input(z.object({ - group_name: z.string().min(1), - description: z.string().optional(), - product_ids: z.array(z.number()).default([]), - })) - .mutation(async ({ input, ctx }) => { - const { group_name, description, product_ids } = input; - - const [newGroup] = await db - .insert(productGroupInfo) - .values({ - groupName: group_name, - description, - }) - .returning(); - - if (product_ids.length > 0) { - const memberships = product_ids.map(productId => ({ - productId, - groupId: newGroup.id, - })); - - await db.insert(productGroupMembership).values(memberships); - } - - // Reinitialize stores to reflect changes - scheduleStoreInitialization() - - return { - group: newGroup, - message: 'Group created successfully', - }; - }), - - updateGroup: protectedProcedure - .input(z.object({ - id: z.number(), - group_name: z.string().optional(), - description: z.string().optional(), - product_ids: z.array(z.number()).optional(), - })) - .mutation(async ({ input, ctx }) => { - const { id, group_name, description, product_ids } = input; - - const updateData: any = {}; - if (group_name !== undefined) updateData.groupName = group_name; - if (description !== undefined) updateData.description = description; - - const [updatedGroup] = await db - .update(productGroupInfo) - .set(updateData) - .where(eq(productGroupInfo.id, id)) - .returning(); - - if (!updatedGroup) { - throw new ApiError('Group not found', 404); - } - - if (product_ids !== undefined) { - // Delete existing memberships - await db.delete(productGroupMembership).where(eq(productGroupMembership.groupId, id)); - - // Insert new memberships - if (product_ids.length > 0) { - const memberships = product_ids.map(productId => ({ - productId, - groupId: id, - })); - - await db.insert(productGroupMembership).values(memberships); - } - } - - // Reinitialize stores to reflect changes - scheduleStoreInitialization() - - return { - group: updatedGroup, - message: 'Group updated successfully', - }; - }), - - deleteGroup: protectedProcedure - .input(z.object({ - id: z.number(), - })) - .mutation(async ({ input, ctx }) => { - const { id } = input; - - // Delete memberships first - await db.delete(productGroupMembership).where(eq(productGroupMembership.groupId, id)); - - // Delete group - const [deletedGroup] = await db - .delete(productGroupInfo) - .where(eq(productGroupInfo.id, id)) - .returning(); - - if (!deletedGroup) { - throw new ApiError('Group not found', 404); - } - - // Reinitialize stores to reflect changes - scheduleStoreInitialization() - - return { - message: 'Group deleted successfully', - }; - }), - - updateProductPrices: protectedProcedure - .input(z.object({ - updates: z.array(z.object({ - productId: z.number(), - price: z.number().optional(), - marketPrice: z.number().nullable().optional(), - flashPrice: z.number().nullable().optional(), - isFlashAvailable: z.boolean().optional(), - })), - })) - .mutation(async ({ input, ctx }) => { - const { updates } = input; - - if (updates.length === 0) { - throw new ApiError('No updates provided', 400); - } - - // Validate that all productIds exist - const productIds = updates.map(u => u.productId); - const existingProducts = await db.query.productInfo.findMany({ - where: inArray(productInfo.id, productIds), - columns: { id: true }, - }); - - const existingIds = new Set(existingProducts.map(p => p.id)); - const invalidIds = productIds.filter(id => !existingIds.has(id)); - - if (invalidIds.length > 0) { - throw new ApiError(`Invalid product IDs: ${invalidIds.join(', ')}`, 400); - } - - // Perform batch update - const updatePromises = updates.map(async (update) => { - const { productId, price, marketPrice, flashPrice, isFlashAvailable } = update; - const updateData: any = {}; - if (price !== undefined) updateData.price = price; - if (marketPrice !== undefined) updateData.marketPrice = marketPrice; - if (flashPrice !== undefined) updateData.flashPrice = flashPrice; - if (isFlashAvailable !== undefined) updateData.isFlashAvailable = isFlashAvailable; - - return db - .update(productInfo) - .set(updateData) - .where(eq(productInfo.id, productId)); - }); - - await Promise.all(updatePromises); - - // Reinitialize stores to reflect changes - scheduleStoreInitialization() - - return { - message: `Updated prices for ${updates.length} product(s)`, - updatedCount: updates.length, - }; - }), - }); diff --git a/verifier/apis/slots.ts b/verifier/apis/slots.ts deleted file mode 100644 index 1cc40e8..0000000 --- a/verifier/apis/slots.ts +++ /dev/null @@ -1,610 +0,0 @@ -import { router, protectedProcedure } from "@/src/trpc/trpc-index" -import { TRPCError } from "@trpc/server"; -import { z } from "zod"; -import { db } from "@/src/db/db_index" -import { deliverySlotInfo, productSlots, productInfo, vendorSnippets, productGroupInfo } from "@/src/db/schema" -import { eq, inArray, and, desc } from "drizzle-orm"; -import { ApiError } from "@/src/lib/api-error" -import { appUrl } from "@/src/lib/env-exporter" -import redisClient from "@/src/lib/redis-client" -import { getSlotSequenceKey } from "@/src/lib/redisKeyGetters" -import { scheduleStoreInitialization } from '@/src/stores/store-initializer' - - -interface CachedDeliverySequence { - [userId: string]: number[]; -} - -const cachedSequenceSchema = z.record(z.string(), z.array(z.number())); - -const createSlotSchema = z.object({ - deliveryTime: z.string(), - freezeTime: z.string(), - isActive: z.boolean().optional(), - productIds: z.array(z.number()).optional(), - vendorSnippets: z.array(z.object({ - name: z.string().min(1), - productIds: z.array(z.number().int().positive()).min(1), - validTill: z.string().optional(), - })).optional(), - groupIds: z.array(z.number()).optional(), -}); - -const getSlotByIdSchema = z.object({ - id: z.number(), -}); - -const updateSlotSchema = z.object({ - id: z.number(), - deliveryTime: z.string(), - freezeTime: z.string(), - isActive: z.boolean().optional(), - productIds: z.array(z.number()).optional(), - vendorSnippets: z.array(z.object({ - name: z.string().min(1), - productIds: z.array(z.number().int().positive()).min(1), - validTill: z.string().optional(), - })).optional(), - groupIds: z.array(z.number()).optional(), -}); - -const deleteSlotSchema = z.object({ - id: z.number(), -}); - -const getDeliverySequenceSchema = z.object({ - id: z.string(), -}); - -const updateDeliverySequenceSchema = z.object({ - id: z.number(), - // deliverySequence: z.array(z.number()), - deliverySequence: z.any(), -}); - -export const slotsRouter = router({ - // Exact replica of GET /av/slots - getAll: protectedProcedure.query(async ({ ctx }) => { - if (!ctx.staffUser?.id) { - throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); - } - - const slots = await db.query.deliverySlotInfo - .findMany({ - where: eq(deliverySlotInfo.isActive, true), - orderBy: desc(deliverySlotInfo.deliveryTime), - with: { - productSlots: { - with: { - product: { - columns: { - id: true, - name: true, - images: true, - }, - }, - }, - }, - }, - }) - .then((slots) => - slots.map((slot) => ({ - ...slot, - deliverySequence: slot.deliverySequence as number[], - products: slot.productSlots.map((ps) => ps.product), - })) - ); - - return { - slots, - count: slots.length, - }; - }), - - // Exact replica of POST /av/products/slots/product-ids - getSlotsProductIds: protectedProcedure - .input(z.object({ slotIds: z.array(z.number()) })) - .query(async ({ input, ctx }) => { - if (!ctx.staffUser?.id) { - throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); - } - - const { slotIds } = input; - - if (!Array.isArray(slotIds)) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "slotIds must be an array", - }); - } - - if (slotIds.length === 0) { - return {}; - } - - // Fetch all associations for the requested slots - const associations = await db.query.productSlots.findMany({ - where: inArray(productSlots.slotId, slotIds), - columns: { - slotId: true, - productId: true, - }, - }); - - // Group by slotId - const result = associations.reduce((acc, assoc) => { - if (!acc[assoc.slotId]) { - acc[assoc.slotId] = []; - } - acc[assoc.slotId].push(assoc.productId); - return acc; - }, {} as Record); - - // Ensure all requested slots have entries (even if empty) - slotIds.forEach((slotId) => { - if (!result[slotId]) { - result[slotId] = []; - } - }); - - return result; - }), - - // Exact replica of PUT /av/products/slots/:slotId/products - updateSlotProducts: protectedProcedure - .input( - z.object({ - slotId: z.number(), - productIds: z.array(z.number()), - }) - ) - .mutation(async ({ input, ctx }) => { - if (!ctx.staffUser?.id) { - throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); - } - - const { slotId, productIds } = input; - - if (!Array.isArray(productIds)) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "productIds must be an array", - }); - } - - // Get current associations - const currentAssociations = await db.query.productSlots.findMany({ - where: eq(productSlots.slotId, slotId), - columns: { - productId: true, - }, - }); - - const currentProductIds = currentAssociations.map( - (assoc) => assoc.productId - ); - const newProductIds = productIds; - - // Find products to add and remove - const productsToAdd = newProductIds.filter( - (id) => !currentProductIds.includes(id) - ); - const productsToRemove = currentProductIds.filter( - (id) => !newProductIds.includes(id) - ); - - // Remove associations for products that are no longer selected - if (productsToRemove.length > 0) { - await db - .delete(productSlots) - .where( - and( - eq(productSlots.slotId, slotId), - inArray(productSlots.productId, productsToRemove) - ) - ); - } - - // Add associations for newly selected products - if (productsToAdd.length > 0) { - const newAssociations = productsToAdd.map((productId) => ({ - productId, - slotId, - })); - - await db.insert(productSlots).values(newAssociations); - } - - // Reinitialize stores to reflect changes - scheduleStoreInitialization() - - return { - message: "Slot products updated successfully", - added: productsToAdd.length, - removed: productsToRemove.length, - }; - }), - - createSlot: protectedProcedure - .input(createSlotSchema) - .mutation(async ({ input, ctx }) => { - if (!ctx.staffUser?.id) { - throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); - } - - const { deliveryTime, freezeTime, isActive, productIds, vendorSnippets: snippets, groupIds } = input; - - // Validate required fields - if (!deliveryTime || !freezeTime) { - throw new ApiError("Delivery time and orders close time are required", 400); - } - - const result = await db.transaction(async (tx) => { - // Create slot - const [newSlot] = await tx - .insert(deliverySlotInfo) - .values({ - deliveryTime: new Date(deliveryTime), - freezeTime: new Date(freezeTime), - isActive: isActive !== undefined ? isActive : true, - groupIds: groupIds !== undefined ? groupIds : [], - }) - .returning(); - - // Insert product associations if provided - if (productIds && productIds.length > 0) { - const associations = productIds.map((productId) => ({ - productId, - slotId: newSlot.id, - })); - await tx.insert(productSlots).values(associations); - } - - // Create vendor snippets if provided - let createdSnippets: any[] = []; - if (snippets && snippets.length > 0) { - for (const snippet of snippets) { - // Validate products exist - const products = await tx.query.productInfo.findMany({ - where: inArray(productInfo.id, snippet.productIds), - }); - if (products.length !== snippet.productIds.length) { - throw new ApiError(`One or more invalid product IDs in snippet "${snippet.name}"`, 400); - } - - // Check if snippet name already exists - const existingSnippet = await tx.query.vendorSnippets.findFirst({ - where: eq(vendorSnippets.snippetCode, snippet.name), - }); - if (existingSnippet) { - throw new ApiError(`Snippet name "${snippet.name}" already exists`, 400); - } - - const [createdSnippet] = await tx.insert(vendorSnippets).values({ - snippetCode: snippet.name, - slotId: newSlot.id, - productIds: snippet.productIds, - validTill: snippet.validTill ? new Date(snippet.validTill) : undefined, - }).returning(); - - createdSnippets.push(createdSnippet); - } - } - - return { - slot: newSlot, - createdSnippets, - message: "Slot created successfully", - }; - }); - - // Reinitialize stores to reflect changes (outside transaction) - scheduleStoreInitialization() - - return result; - }), - - getSlots: protectedProcedure.query(async ({ ctx }) => { - if (!ctx.staffUser?.id) { - throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); - } - - const slots = await db.query.deliverySlotInfo.findMany({ - where: eq(deliverySlotInfo.isActive, true), - }); - - return { - slots, - count: slots.length, - }; - }), - - getSlotById: protectedProcedure - .input(getSlotByIdSchema) - .query(async ({ input, ctx }) => { - if (!ctx.staffUser?.id) { - throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); - } - - const { id } = input; - - const slot = await db.query.deliverySlotInfo.findFirst({ - where: eq(deliverySlotInfo.id, id), - with: { - productSlots: { - with: { - product: { - columns: { - id: true, - name: true, - images: true, - }, - }, - }, - }, - vendorSnippets: true, - }, - }); - - if (!slot) { - throw new ApiError("Slot not found", 404); - } - - return { - slot: { - ...slot, - deliverySequence: slot.deliverySequence as number[], - groupIds: slot.groupIds as number[], - products: slot.productSlots.map((ps) => ps.product), - vendorSnippets: slot.vendorSnippets?.map(snippet => ({ - ...snippet, - accessUrl: `${appUrl}/vendor-order-list?id=${snippet.snippetCode}` - })), - }, - }; - }), - - updateSlot: protectedProcedure - .input(updateSlotSchema) - .mutation(async ({ input, ctx }) => { - if (!ctx.staffUser?.id) { - throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); - } - try{ - const { id, deliveryTime, freezeTime, isActive, productIds, vendorSnippets: snippets, groupIds } = input; - - if (!deliveryTime || !freezeTime) { - throw new ApiError("Delivery time and orders close time are required", 400); - } - - // Filter groupIds to only include valid (existing) groups - let validGroupIds = groupIds; - if (groupIds && groupIds.length > 0) { - const existingGroups = await db.query.productGroupInfo.findMany({ - where: inArray(productGroupInfo.id, groupIds), - columns: { id: true }, - }); - validGroupIds = existingGroups.map(g => g.id); - } - - const result = await db.transaction(async (tx) => { - const [updatedSlot] = await tx - .update(deliverySlotInfo) - .set({ - deliveryTime: new Date(deliveryTime), - freezeTime: new Date(freezeTime), - isActive: isActive !== undefined ? isActive : true, - groupIds: validGroupIds !== undefined ? validGroupIds : [], - }) - .where(eq(deliverySlotInfo.id, id)) - .returning(); - - if (!updatedSlot) { - throw new ApiError("Slot not found", 404); - } - - // Update product associations - if (productIds !== undefined) { - // Delete existing associations - await tx.delete(productSlots).where(eq(productSlots.slotId, id)); - - // Insert new associations - if (productIds.length > 0) { - const associations = productIds.map((productId) => ({ - productId, - slotId: id, - })); - await tx.insert(productSlots).values(associations); - } - } - - // Create vendor snippets if provided - let createdSnippets: any[] = []; - if (snippets && snippets.length > 0) { - for (const snippet of snippets) { - // Validate products exist - const products = await tx.query.productInfo.findMany({ - where: inArray(productInfo.id, snippet.productIds), - }); - if (products.length !== snippet.productIds.length) { - throw new ApiError(`One or more invalid product IDs in snippet "${snippet.name}"`, 400); - } - - // Check if snippet name already exists - const existingSnippet = await tx.query.vendorSnippets.findFirst({ - where: eq(vendorSnippets.snippetCode, snippet.name), - }); - if (existingSnippet) { - throw new ApiError(`Snippet name "${snippet.name}" already exists`, 400); - } - - const [createdSnippet] = await tx.insert(vendorSnippets).values({ - snippetCode: snippet.name, - slotId: id, - productIds: snippet.productIds, - validTill: snippet.validTill ? new Date(snippet.validTill) : undefined, - - }).returning(); - - createdSnippets.push(createdSnippet); - } - } - - return { - slot: updatedSlot, - createdSnippets, - message: "Slot updated successfully", - }; - }); - - // Reinitialize stores to reflect changes (outside transaction) - scheduleStoreInitialization() - - return result; - } - catch(e) { - console.log(e) - throw new ApiError("Unable to Update Slot"); - } - }), - - deleteSlot: protectedProcedure - .input(deleteSlotSchema) - .mutation(async ({ input, ctx }) => { - if (!ctx.staffUser?.id) { - throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); - } - - const { id } = input; - - const [deletedSlot] = await db - .update(deliverySlotInfo) - .set({ isActive: false }) - .where(eq(deliverySlotInfo.id, id)) - .returning(); - - if (!deletedSlot) { - throw new ApiError("Slot not found", 404); - } - - // Reinitialize stores to reflect changes - scheduleStoreInitialization() - - return { - message: "Slot deleted successfully", - }; - }), - - getDeliverySequence: protectedProcedure - .input(getDeliverySequenceSchema) - .query(async ({ input, ctx }) => { - - const { id } = input; - const slotId = parseInt(id); - const cacheKey = getSlotSequenceKey(slotId); - - try { - const cached = await redisClient.get(cacheKey); - if (cached) { - const parsed = JSON.parse(cached); - const validated = cachedSequenceSchema.parse(parsed) as CachedDeliverySequence; - console.log('sending cached response') - - return { deliverySequence: validated }; - } - } catch (error) { - console.warn('Redis cache read/validation failed, falling back to DB:', error); - // Continue to DB fallback - } - - // Fallback to DB - const slot = await db.query.deliverySlotInfo.findFirst({ - where: eq(deliverySlotInfo.id, slotId), - }); - - if (!slot) { - throw new ApiError("Slot not found", 404); - } - - const sequence = (slot.deliverySequence || {}) as CachedDeliverySequence; - - // Cache the validated result - try { - const validated = cachedSequenceSchema.parse(sequence); - await redisClient.set(cacheKey, JSON.stringify(validated), 3600); - } catch (cacheError) { - console.warn('Redis cache write failed:', cacheError); - } - - return { deliverySequence: sequence }; - }), - - updateDeliverySequence: protectedProcedure - .input(updateDeliverySequenceSchema) - .mutation(async ({ input, ctx }) => { - if (!ctx.staffUser?.id) { - throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); - } - - const { id, deliverySequence } = input; - - const [updatedSlot] = await db - .update(deliverySlotInfo) - .set({ deliverySequence }) - .where(eq(deliverySlotInfo.id, id)) - .returning({ - id: deliverySlotInfo.id, - deliverySequence: deliverySlotInfo.deliverySequence, - }); - - if (!updatedSlot) { - throw new ApiError("Slot not found", 404); - } - - // Cache the updated sequence - const cacheKey = getSlotSequenceKey(id); - try { - const validated = cachedSequenceSchema.parse(deliverySequence); - await redisClient.set(cacheKey, JSON.stringify(validated), 3600); - } catch (cacheError) { - console.warn('Redis cache write failed:', cacheError); - } - - return { - slot: updatedSlot, - message: "Delivery sequence updated successfully", - }; - }), - - updateSlotCapacity: protectedProcedure - .input(z.object({ - slotId: z.number(), - isCapacityFull: z.boolean(), - })) - .mutation(async ({ input, ctx }) => { - if (!ctx.staffUser?.id) { - throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); - } - - const { slotId, isCapacityFull } = input; - - const [updatedSlot] = await db - .update(deliverySlotInfo) - .set({ isCapacityFull }) - .where(eq(deliverySlotInfo.id, slotId)) - .returning(); - - if (!updatedSlot) { - throw new ApiError("Slot not found", 404); - } - - // Reinitialize stores to reflect changes - scheduleStoreInitialization() - - return { - success: true, - slot: updatedSlot, - message: `Slot ${isCapacityFull ? 'marked as full capacity' : 'capacity reset'}`, - }; - }), -}); diff --git a/verifier/apis/staff-user.ts b/verifier/apis/staff-user.ts deleted file mode 100644 index 428f36e..0000000 --- a/verifier/apis/staff-user.ts +++ /dev/null @@ -1,242 +0,0 @@ -import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index' -import { z } from 'zod'; -import { db } from '@/src/db/db_index' -import { staffUsers, staffRoles, users, userDetails, orders } from '@/src/db/schema' -import { eq, or, ilike, and, lt, desc } from 'drizzle-orm'; -import bcrypt from 'bcryptjs'; -import { ApiError } from '@/src/lib/api-error' -import { signToken } from '@/src/lib/jwt-utils' - -export const staffUserRouter = router({ - login: publicProcedure - .input(z.object({ - name: z.string(), - password: z.string(), - })) - .mutation(async ({ input }) => { - const { name, password } = input; - - if (!name || !password) { - throw new ApiError('Name and password are required', 400); - } - - const staff = await db.query.staffUsers.findFirst({ - where: eq(staffUsers.name, name), - }); - - if (!staff) { - throw new ApiError('Invalid credentials', 401); - } - - const isPasswordValid = await bcrypt.compare(password, staff.password); - if (!isPasswordValid) { - throw new ApiError('Invalid credentials', 401); - } - - const token = await signToken( - { staffId: staff.id, name: staff.name }, - '30d' - ); - - return { - message: 'Login successful', - token, - staff: { id: staff.id, name: staff.name }, - }; - }), - - getStaff: protectedProcedure - .query(async ({ ctx }) => { - const staff = await db.query.staffUsers.findMany({ - columns: { - id: true, - name: true, - }, - with: { - role: { - with: { - rolePermissions: { - with: { - permission: true, - }, - }, - }, - }, - }, - }); - - // Transform the data to include role and permissions in a cleaner format - const transformedStaff = staff.map((user) => ({ - id: user.id, - name: user.name, - role: user.role ? { - id: user.role.id, - name: user.role.roleName, - } : null, - permissions: user.role?.rolePermissions.map((rp) => ({ - id: rp.permission.id, - name: rp.permission.permissionName, - })) || [], - })); - - return { - staff: transformedStaff, - }; - }), - - getUsers: protectedProcedure - .input(z.object({ - cursor: z.number().optional(), - limit: z.number().default(20), - search: z.string().optional(), - })) - .query(async ({ input }) => { - const { cursor, limit, search } = input; - - let whereCondition = undefined; - - if (search) { - whereCondition = or( - ilike(users.name, `%${search}%`), - ilike(users.email, `%${search}%`), - ilike(users.mobile, `%${search}%`) - ); - } - - if (cursor) { - const cursorCondition = lt(users.id, cursor); - whereCondition = whereCondition ? and(whereCondition, cursorCondition) : cursorCondition; - } - - const allUsers = await db.query.users.findMany({ - where: whereCondition, - with: { - userDetails: true, - }, - orderBy: desc(users.id), - limit: limit + 1, // fetch one extra to check if there's more - }); - - const hasMore = allUsers.length > limit; - const usersToReturn = hasMore ? allUsers.slice(0, limit) : allUsers; - - const formattedUsers = usersToReturn.map(user => ({ - id: user.id, - name: user.name, - email: user.email, - mobile: user.mobile, - image: user.userDetails?.profileImage || null, - })); - - return { - users: formattedUsers, - nextCursor: hasMore ? usersToReturn[usersToReturn.length - 1].id : undefined, - }; - }), - - getUserDetails: protectedProcedure - .input(z.object({ userId: z.number() })) - .query(async ({ input }) => { - const { userId } = input; - - const user = await db.query.users.findFirst({ - where: eq(users.id, userId), - with: { - userDetails: true, - orders: { - orderBy: desc(orders.createdAt), - limit: 1, - }, - }, - }); - - if (!user) { - throw new ApiError("User not found", 404); - } - - const lastOrder = user.orders[0]; - - return { - id: user.id, - name: user.name, - email: user.email, - mobile: user.mobile, - addedOn: user.createdAt, - lastOrdered: lastOrder?.createdAt || null, - isSuspended: user.userDetails?.isSuspended || false, - }; - }), - - updateUserSuspension: protectedProcedure - .input(z.object({ userId: z.number(), isSuspended: z.boolean() })) - .mutation(async ({ input }) => { - const { userId, isSuspended } = input; - - await db - .insert(userDetails) - .values({ userId, isSuspended }) - .onConflictDoUpdate({ - target: userDetails.userId, - set: { isSuspended }, - }); - - return { success: true }; - }), - - createStaffUser: protectedProcedure - .input(z.object({ - name: z.string().min(1, 'Name is required'), - password: z.string().min(6, 'Password must be at least 6 characters'), - roleId: z.number().int().positive('Role is required'), - })) - .mutation(async ({ input, ctx }) => { - const { name, password, roleId } = input; - - // Check if staff user already exists - const existingUser = await db.query.staffUsers.findFirst({ - where: eq(staffUsers.name, name), - }); - - if (existingUser) { - throw new ApiError('Staff user with this name already exists', 409); - } - - // Check if role exists - const role = await db.query.staffRoles.findFirst({ - where: eq(staffRoles.id, roleId), - }); - - if (!role) { - throw new ApiError('Invalid role selected', 400); - } - - // Hash password - const hashedPassword = await bcrypt.hash(password, 12); - - // Create staff user - const [newUser] = await db.insert(staffUsers).values({ - name: name.trim(), - password: hashedPassword, - staffRoleId: roleId, - }).returning(); - - return { success: true, user: { id: newUser.id, name: newUser.name } }; - }), - - getRoles: protectedProcedure - .query(async ({ ctx }) => { - const roles = await db.query.staffRoles.findMany({ - columns: { - id: true, - roleName: true, - }, - }); - - return { - roles: roles.map(role => ({ - id: role.id, - name: role.roleName, - })), - }; - }), -}); \ No newline at end of file diff --git a/verifier/apis/store.ts b/verifier/apis/store.ts deleted file mode 100644 index 5925843..0000000 --- a/verifier/apis/store.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { router, protectedProcedure } from '@/src/trpc/trpc-index' -import { z } from 'zod'; -import { db } from '@/src/db/db_index' -import { storeInfo, productInfo } from '@/src/db/schema' -import { eq, inArray } from 'drizzle-orm'; -import { ApiError } from '@/src/lib/api-error' - import { extractKeyFromPresignedUrl, deleteImageUtil, scaffoldAssetUrl } from '@/src/lib/s3-client' -import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; -import { scheduleStoreInitialization } from '@/src/stores/store-initializer' - - -export const storeRouter = router({ - getStores: protectedProcedure - .query(async ({ ctx }) => { - const stores = await db.query.storeInfo.findMany({ - with: { - owner: true, - }, - }); - - Promise.all(stores.map(async store => { - if(store.imageUrl) - store.imageUrl = scaffoldAssetUrl(store.imageUrl) - })).catch((e) => { - throw new ApiError("Unable to find store image urls") - } - ) - return { - stores, - count: stores.length, - }; - }), - - getStoreById: protectedProcedure - .input(z.object({ - id: z.number(), - })) - .query(async ({ input, ctx }) => { - const { id } = input; - - const store = await db.query.storeInfo.findFirst({ - where: eq(storeInfo.id, id), - with: { - owner: true, - }, - }); - - if (!store) { - throw new ApiError("Store not found", 404); - } - store.imageUrl = scaffoldAssetUrl(store.imageUrl); - return { - store, - }; - }), - - createStore: protectedProcedure - .input(z.object({ - name: z.string().min(1, "Name is required"), - description: z.string().optional(), - imageUrl: z.string().optional(), - owner: z.number().min(1, "Owner is required"), - products: z.array(z.number()).optional(), - })) - .mutation(async ({ input, ctx }) => { - const { name, description, imageUrl, owner, products } = input; - - // const imageKey = imageUrl ? extractKeyFromPresignedUrl(imageUrl) : undefined; - const imageKey = imageUrl - - const [newStore] = await db - .insert(storeInfo) - .values({ - name, - description, - imageUrl: imageKey, - owner, - }) - .returning(); - - // Assign selected products to this store - if (products && products.length > 0) { - await db - .update(productInfo) - .set({ storeId: newStore.id }) - .where(inArray(productInfo.id, products)); - } - - // Reinitialize stores to reflect changes - scheduleStoreInitialization() - - return { - store: newStore, - message: "Store created successfully", - }; - }), - - updateStore: protectedProcedure - .input(z.object({ - id: z.number(), - name: z.string().min(1, "Name is required"), - description: z.string().optional(), - imageUrl: z.string().optional(), - owner: z.number().min(1, "Owner is required"), - products: z.array(z.number()).optional(), - })) - .mutation(async ({ input, ctx }) => { - const { id, name, description, imageUrl, owner, products } = input; - - const existingStore = await db.query.storeInfo.findFirst({ - where: eq(storeInfo.id, id), - }); - - if (!existingStore) { - throw new ApiError("Store not found", 404); - } - - const oldImageKey = existingStore.imageUrl; - const newImageKey = imageUrl ? extractKeyFromPresignedUrl(imageUrl) : oldImageKey; - - // Delete old image only if: - // 1. New image provided and keys are different, OR - // 2. No new image but old exists (clearing the image) - if (oldImageKey && ( - (newImageKey && newImageKey !== oldImageKey) || - (!newImageKey) - )) { - try { - await deleteImageUtil({keys: [oldImageKey]}); - } catch (error) { - console.error('Failed to delete old image:', error); - // Continue with update even if deletion fails - } - } - - const [updatedStore] = await db - .update(storeInfo) - .set({ - name, - description, - imageUrl: newImageKey, - owner, - }) - .where(eq(storeInfo.id, id)) - .returning(); - - if (!updatedStore) { - throw new ApiError("Store not found", 404); - } - - // Update products if provided - if (products) { - // First, set storeId to null for products not in the list but currently assigned to this store - await db - .update(productInfo) - .set({ storeId: null }) - .where(eq(productInfo.storeId, id)); - - // Then, assign the selected products to this store - if (products.length > 0) { - await db - .update(productInfo) - .set({ storeId: id }) - .where(inArray(productInfo.id, products)); - } - } - - // Reinitialize stores to reflect changes - scheduleStoreInitialization() - - return { - store: updatedStore, - message: "Store updated successfully", - }; - }), - - deleteStore: protectedProcedure - .input(z.object({ - storeId: z.number(), - })) - .mutation(async ({ input, ctx }) => { - const { storeId } = input; - - const result = await db.transaction(async (tx) => { - // First, update all products of this store to set storeId to null - await tx - .update(productInfo) - .set({ storeId: null }) - .where(eq(productInfo.storeId, storeId)); - - // Then delete the store - const [deletedStore] = await tx - .delete(storeInfo) - .where(eq(storeInfo.id, storeId)) - .returning(); - - if (!deletedStore) { - throw new ApiError("Store not found", 404); - } - - return { - message: "Store deleted successfully", - }; - }); - - // Reinitialize stores to reflect changes (outside transaction) - scheduleStoreInitialization() - - return result; - }), - }); diff --git a/verifier/apis/tag.ts b/verifier/apis/tag.ts deleted file mode 100644 index 66dac83..0000000 --- a/verifier/apis/tag.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { router, protectedProcedure } from '@/src/trpc/trpc-index' -import { z } from 'zod'; -import { db } from '@/src/db/db_index' -import { productTagInfo } from '@/src/db/schema' -import { eq } from 'drizzle-orm'; -import { ApiError } from '@/src/lib/api-error' -import { scaffoldAssetUrl, claimUploadUrl } from '@/src/lib/s3-client' -import { deleteS3Image } from '@/src/lib/delete-image' -import { scheduleStoreInitialization } from '@/src/stores/store-initializer' - -export const tagRouter = router({ - getTags: protectedProcedure - .query(async () => { - const tags = await db - .select() - .from(productTagInfo) - .orderBy(productTagInfo.tagName); - - // Generate asset URLs for tag images - const tagsWithUrls = tags.map(tag => ({ - ...tag, - imageUrl: tag.imageUrl ? scaffoldAssetUrl(tag.imageUrl) : null, - })); - - return { - tags: tagsWithUrls, - message: "Tags retrieved successfully", - }; - }), - - getTagById: protectedProcedure - .input(z.object({ - id: z.number(), - })) - .query(async ({ input }) => { - const tag = await db.query.productTagInfo.findFirst({ - where: eq(productTagInfo.id, input.id), - }); - - if (!tag) { - throw new ApiError("Tag not found", 404); - } - - // Generate asset URL for tag image - const tagWithUrl = { - ...tag, - imageUrl: tag.imageUrl ? scaffoldAssetUrl(tag.imageUrl) : null, - }; - - return { - tag: tagWithUrl, - message: "Tag retrieved successfully", - }; - }), - - createTag: protectedProcedure - .input(z.object({ - tagName: z.string().min(1), - tagDescription: z.string().optional(), - isDashboardTag: z.boolean().default(false), - relatedStores: z.array(z.number()).default([]), - imageKey: z.string().optional(), - })) - .mutation(async ({ input }) => { - const { tagName, tagDescription, isDashboardTag, relatedStores, imageKey } = input; - - // Check for duplicate tag name - const existingTag = await db.query.productTagInfo.findFirst({ - where: eq(productTagInfo.tagName, tagName.trim()), - }); - - if (existingTag) { - throw new ApiError("A tag with this name already exists", 400); - } - - const [newTag] = await db - .insert(productTagInfo) - .values({ - tagName: tagName.trim(), - tagDescription, - imageUrl: imageKey || null, - isDashboardTag, - relatedStores, - }) - .returning(); - - // Claim upload URL if image was provided - if (imageKey) { - try { - await claimUploadUrl(imageKey); - } catch (e) { - console.warn(`Failed to claim upload URL for key: ${imageKey}`, e); - } - } - - scheduleStoreInitialization(); - - return { - tag: newTag, - message: "Tag created successfully", - }; - }), - - updateTag: protectedProcedure - .input(z.object({ - id: z.number(), - tagName: z.string().min(1), - tagDescription: z.string().optional(), - isDashboardTag: z.boolean(), - relatedStores: z.array(z.number()), - imageKey: z.string().optional(), - deleteExistingImage: z.boolean().optional(), - })) - .mutation(async ({ input }) => { - const { id, imageKey, deleteExistingImage, ...updateData } = input; - - // Get current tag - const currentTag = await db.query.productTagInfo.findFirst({ - where: eq(productTagInfo.id, id), - }); - - if (!currentTag) { - throw new ApiError("Tag not found", 404); - } - - let newImageUrl = currentTag.imageUrl; - - // Handle image deletion - if (deleteExistingImage && currentTag.imageUrl) { - try { - await deleteS3Image(currentTag.imageUrl); - } catch (e) { - console.error(`Failed to delete old image: ${currentTag.imageUrl}`, e); - } - newImageUrl = null; - } - - // Handle new image upload (only if different from existing) - if (imageKey && imageKey !== currentTag.imageUrl) { - // Delete old image if exists and not already deleted - if (currentTag.imageUrl && !deleteExistingImage) { - try { - await deleteS3Image(currentTag.imageUrl); - } catch (e) { - console.error(`Failed to delete old image: ${currentTag.imageUrl}`, e); - } - } - newImageUrl = imageKey; - - // Claim upload URL - try { - await claimUploadUrl(imageKey); - } catch (e) { - console.warn(`Failed to claim upload URL for key: ${imageKey}`, e); - } - } - - const [updatedTag] = await db - .update(productTagInfo) - .set({ - tagName: updateData.tagName.trim(), - tagDescription: updateData.tagDescription, - isDashboardTag: updateData.isDashboardTag, - relatedStores: updateData.relatedStores, - imageUrl: newImageUrl, - }) - .where(eq(productTagInfo.id, id)) - .returning(); - - scheduleStoreInitialization(); - - return { - tag: updatedTag, - message: "Tag updated successfully", - }; - }), - - deleteTag: protectedProcedure - .input(z.object({ - id: z.number(), - })) - .mutation(async ({ input }) => { - const { id } = input; - - // Get tag to check for image - const tag = await db.query.productTagInfo.findFirst({ - where: eq(productTagInfo.id, id), - }); - - if (!tag) { - throw new ApiError("Tag not found", 404); - } - - // Delete image from S3 if exists - if (tag.imageUrl) { - try { - await deleteS3Image(tag.imageUrl); - } catch (e) { - console.error(`Failed to delete image: ${tag.imageUrl}`, e); - } - } - - // Delete tag (will fail if tag is assigned to products due to FK constraint) - await db.delete(productTagInfo).where(eq(productTagInfo.id, id)); - - scheduleStoreInitialization(); - - return { - message: "Tag deleted successfully", - }; - }), -}); - -export type TagRouter = typeof tagRouter; diff --git a/verifier/apis/user.ts b/verifier/apis/user.ts deleted file mode 100644 index 2b176b2..0000000 --- a/verifier/apis/user.ts +++ /dev/null @@ -1,489 +0,0 @@ -import { protectedProcedure } from '@/src/trpc/trpc-index'; -import { z } from 'zod'; -import { db } from '@/src/db/db_index'; -import { users, complaints, orders, orderItems, notifCreds, unloggedUserTokens, userDetails, userIncidents } from '@/src/db/schema'; -import { eq, sql, desc, asc, count, max, inArray } from 'drizzle-orm'; -import { ApiError } from '@/src/lib/api-error'; -import { notificationQueue } from '@/src/lib/notif-job'; -import { recomputeUserNegativityScore } from '@/src/stores/user-negativity-store'; - -async function createUserByMobile(mobile: string): Promise { - // Clean mobile number (remove non-digits) - const cleanMobile = mobile.replace(/\D/g, ''); - - // Validate: exactly 10 digits - if (cleanMobile.length !== 10) { - throw new ApiError('Mobile number must be exactly 10 digits', 400); - } - - // Check if user already exists - const [existingUser] = await db - .select() - .from(users) - .where(eq(users.mobile, cleanMobile)) - .limit(1); - - if (existingUser) { - throw new ApiError('User with this mobile number already exists', 409); - } - - // Create user - const [newUser] = await db - .insert(users) - .values({ - name: null, - email: null, - mobile: cleanMobile, - }) - .returning(); - - return newUser; -} - -export const userRouter = { - createUserByMobile: protectedProcedure - .input(z.object({ - mobile: z.string().min(1, 'Mobile number is required'), - })) - .mutation(async ({ input }) => { - const newUser = await createUserByMobile(input.mobile); - - return { - success: true, - data: newUser, - }; - }), - - getEssentials: protectedProcedure - .query(async () => { - const count = await db.$count(complaints, eq(complaints.isResolved, false)); - - return { - unresolvedComplaints: count || 0, - }; - }), - - getAllUsers: protectedProcedure - .input(z.object({ - limit: z.number().min(1).max(100).default(50), - cursor: z.number().optional(), - search: z.string().optional(), - })) - .query(async ({ input }) => { - const { limit, cursor, search } = input; - - // Build where conditions - const whereConditions = []; - - if (search && search.trim()) { - whereConditions.push(sql`${users.mobile} ILIKE ${`%${search.trim()}%`}`); - } - - if (cursor) { - whereConditions.push(sql`${users.id} > ${cursor}`); - } - - // Get users with filters applied - const usersList = await db - .select({ - id: users.id, - name: users.name, - mobile: users.mobile, - createdAt: users.createdAt, - }) - .from(users) - .where(whereConditions.length > 0 ? sql.join(whereConditions, sql` AND `) : undefined) - .orderBy(asc(users.id)) - .limit(limit + 1); // Get one extra to determine if there's more - - // Check if there are more results - const hasMore = usersList.length > limit; - const usersToReturn = hasMore ? usersList.slice(0, limit) : usersList; - - // Get order stats for each user - const userIds = usersToReturn.map(u => u.id); - - let orderCounts: { userId: number; totalOrders: number }[] = []; - let lastOrders: { userId: number; lastOrderDate: Date | null }[] = []; - let suspensionStatuses: { userId: number; isSuspended: boolean }[] = []; - - if (userIds.length > 0) { - // Get total orders per user - orderCounts = await db - .select({ - userId: orders.userId, - totalOrders: count(orders.id), - }) - .from(orders) - .where(sql`${orders.userId} IN (${sql.join(userIds, sql`, `)})`) - .groupBy(orders.userId); - - // Get last order date per user - lastOrders = await db - .select({ - userId: orders.userId, - lastOrderDate: max(orders.createdAt), - }) - .from(orders) - .where(sql`${orders.userId} IN (${sql.join(userIds, sql`, `)})`) - .groupBy(orders.userId); - - // Get suspension status for each user - suspensionStatuses = await db - .select({ - userId: userDetails.userId, - isSuspended: userDetails.isSuspended, - }) - .from(userDetails) - .where(sql`${userDetails.userId} IN (${sql.join(userIds, sql`, `)})`); - } - - // Create lookup maps - const orderCountMap = new Map(orderCounts.map(o => [o.userId, o.totalOrders])); - const lastOrderMap = new Map(lastOrders.map(o => [o.userId, o.lastOrderDate])); - const suspensionMap = new Map(suspensionStatuses.map(s => [s.userId, s.isSuspended])); - - // Combine data - const usersWithStats = usersToReturn.map(user => ({ - ...user, - totalOrders: orderCountMap.get(user.id) || 0, - lastOrderDate: lastOrderMap.get(user.id) || null, - isSuspended: suspensionMap.get(user.id) ?? false, - })); - - // Get next cursor - const nextCursor = hasMore ? usersToReturn[usersToReturn.length - 1].id : undefined; - - return { - users: usersWithStats, - nextCursor, - hasMore, - }; - }), - - getUserDetails: protectedProcedure - .input(z.object({ - userId: z.number(), - })) - .query(async ({ input }) => { - const { userId } = input; - - // Get user info - const user = await db - .select({ - id: users.id, - name: users.name, - mobile: users.mobile, - createdAt: users.createdAt, - }) - .from(users) - .where(eq(users.id, userId)) - .limit(1); - - if (!user || user.length === 0) { - throw new ApiError('User not found', 404); - } - - // Get user suspension status - const userDetail = await db - .select({ - isSuspended: userDetails.isSuspended, - }) - .from(userDetails) - .where(eq(userDetails.userId, userId)) - .limit(1); - - // Get all orders for this user with order items count - const userOrders = await db - .select({ - id: orders.id, - readableId: orders.readableId, - totalAmount: orders.totalAmount, - createdAt: orders.createdAt, - isFlashDelivery: orders.isFlashDelivery, - }) - .from(orders) - .where(eq(orders.userId, userId)) - .orderBy(desc(orders.createdAt)); - - // Get order status for each order - const orderIds = userOrders.map(o => o.id); - - let orderStatuses: { orderId: number; isDelivered: boolean; isCancelled: boolean }[] = []; - - if (orderIds.length > 0) { - const { orderStatus } = await import('@/src/db/schema'); - orderStatuses = await db - .select({ - orderId: orderStatus.orderId, - isDelivered: orderStatus.isDelivered, - isCancelled: orderStatus.isCancelled, - }) - .from(orderStatus) - .where(sql`${orderStatus.orderId} IN (${sql.join(orderIds, sql`, `)})`); - } - - // Get item counts for each order - const itemCounts = await db - .select({ - orderId: orderItems.orderId, - itemCount: count(orderItems.id), - }) - .from(orderItems) - .where(sql`${orderItems.orderId} IN (${sql.join(orderIds, sql`, `)})`) - .groupBy(orderItems.orderId); - - // Create lookup maps - const statusMap = new Map(orderStatuses.map(s => [s.orderId, s])); - const itemCountMap = new Map(itemCounts.map(c => [c.orderId, c.itemCount])); - - // Determine status string - const getStatus = (status: { isDelivered: boolean; isCancelled: boolean } | undefined) => { - if (!status) return 'pending'; - if (status.isCancelled) return 'cancelled'; - if (status.isDelivered) return 'delivered'; - return 'pending'; - }; - - // Combine data - const ordersWithDetails = userOrders.map(order => { - const status = statusMap.get(order.id); - return { - id: order.id, - readableId: order.readableId, - totalAmount: order.totalAmount, - createdAt: order.createdAt, - isFlashDelivery: order.isFlashDelivery, - status: getStatus(status), - itemCount: itemCountMap.get(order.id) || 0, - }; - }); - - return { - user: { - ...user[0], - isSuspended: userDetail[0]?.isSuspended ?? false, - }, - orders: ordersWithDetails, - }; - }), - - updateUserSuspension: protectedProcedure - .input(z.object({ - userId: z.number(), - isSuspended: z.boolean(), - })) - .mutation(async ({ input }) => { - const { userId, isSuspended } = input; - - // Check if user exists - const user = await db - .select({ id: users.id }) - .from(users) - .where(eq(users.id, userId)) - .limit(1); - - if (!user || user.length === 0) { - throw new ApiError('User not found', 404); - } - - // Check if user_details record exists - const existingDetail = await db - .select({ id: userDetails.id }) - .from(userDetails) - .where(eq(userDetails.userId, userId)) - .limit(1); - - if (existingDetail.length > 0) { - // Update existing record - await db - .update(userDetails) - .set({ isSuspended }) - .where(eq(userDetails.userId, userId)); - } else { - // Insert new record - await db - .insert(userDetails) - .values({ - userId, - isSuspended, - }); - } - - return { - success: true, - message: `User ${isSuspended ? 'suspended' : 'unsuspended'} successfully`, - }; - }), - - getUsersForNotification: protectedProcedure - .input(z.object({ - search: z.string().optional(), - })) - .query(async ({ input }) => { - const { search } = input; - - // Get all users - let usersList; - if (search && search.trim()) { - usersList = await db - .select({ - id: users.id, - name: users.name, - mobile: users.mobile, - }) - .from(users) - .where(sql`${users.mobile} ILIKE ${`%${search.trim()}%`} OR ${users.name} ILIKE ${`%${search.trim()}%`}`); - } else { - usersList = await db - .select({ - id: users.id, - name: users.name, - mobile: users.mobile, - }) - .from(users); - } - - // Get eligible users (have notif_creds entry) - const eligibleUsers = await db - .select({ userId: notifCreds.userId }) - .from(notifCreds); - - const eligibleSet = new Set(eligibleUsers.map(u => u.userId)); - - return { - users: usersList.map(user => ({ - id: user.id, - name: user.name, - mobile: user.mobile, - isEligibleForNotif: eligibleSet.has(user.id), - })), - }; - }), - - sendNotification: protectedProcedure - .input(z.object({ - userIds: z.array(z.number()).default([]), - title: z.string().min(1, 'Title is required'), - text: z.string().min(1, 'Message is required'), - imageUrl: z.string().optional(), - })) - .mutation(async ({ input }) => { - const { userIds, title, text, imageUrl } = input; - - let tokens: string[] = []; - - if (userIds.length === 0) { - // Send to all users - get tokens from both logged-in and unlogged users - const loggedInTokens = await db.select({ token: notifCreds.token }).from(notifCreds); - const unloggedTokens = await db.select({ token: unloggedUserTokens.token }).from(unloggedUserTokens); - - tokens = [ - ...loggedInTokens.map(t => t.token), - ...unloggedTokens.map(t => t.token) - ]; - } else { - // Send to specific users - get their tokens - const userTokens = await db - .select({ token: notifCreds.token }) - .from(notifCreds) - .where(inArray(notifCreds.userId, userIds)); - - tokens = userTokens.map(t => t.token); - } - - // Queue one job per token - let queuedCount = 0; - for (const token of tokens) { - try { - await notificationQueue.add('send-admin-notification', { - token, - title, - body: text, - imageUrl: imageUrl || null, - }, { - attempts: 3, - backoff: { - type: 'exponential', - delay: 2000, - }, - }); - queuedCount++; - } catch (error) { - console.error(`Failed to queue notification for token:`, error); - } - } - - return { - success: true, - message: `Notification queued for ${queuedCount} users`, - }; - }), - - getUserIncidents: protectedProcedure - .input(z.object({ - userId: z.number(), - })) - .query(async ({ input }) => { - const { userId } = input; - - const incidents = await db.query.userIncidents.findMany({ - where: eq(userIncidents.userId, userId), - with: { - order: { - with: { - orderStatus: true, - }, - }, - addedBy: true, - }, - orderBy: desc(userIncidents.dateAdded), - }); - - return { - incidents: incidents.map(incident => ({ - id: incident.id, - userId: incident.userId, - orderId: incident.orderId, - dateAdded: incident.dateAdded, - adminComment: incident.adminComment, - addedBy: incident.addedBy?.name || 'Unknown', - negativityScore: incident.negativityScore, - orderStatus: incident.order?.orderStatus?.[0]?.isCancelled ? 'cancelled' : 'active', - })), - }; - }), - - addUserIncident: protectedProcedure - .input(z.object({ - userId: z.number(), - orderId: z.number().optional(), - adminComment: z.string().optional(), - negativityScore: z.number().optional(), - })) - .mutation(async ({ input, ctx }) => { - const { userId, orderId, adminComment, negativityScore } = input; - - const adminUserId = ctx.staffUser?.id; - - if (!adminUserId) { - throw new ApiError('Admin user not authenticated', 401); - } - - - const incidentObj = { userId, orderId, adminComment, addedBy: adminUserId, negativityScore }; - - const [incident] = await db.insert(userIncidents) - .values({ - ...incidentObj, - }) - .returning(); - - recomputeUserNegativityScore(userId); - - return { - success: true, - data: incident, - }; - }), -}; diff --git a/verifier/apis/vendor-snippets.ts b/verifier/apis/vendor-snippets.ts deleted file mode 100644 index 4a307d3..0000000 --- a/verifier/apis/vendor-snippets.ts +++ /dev/null @@ -1,531 +0,0 @@ -import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index' -import { z } from 'zod'; -import dayjs from 'dayjs'; -import { db } from '@/src/db/db_index' -import { vendorSnippets, deliverySlotInfo, productInfo, orders, orderItems, users, orderStatus } from '@/src/db/schema' -import { eq, and, inArray, isNotNull, gt, sql, asc, ne } from 'drizzle-orm'; -import { appUrl } from '@/src/lib/env-exporter' - -const createSnippetSchema = z.object({ - snippetCode: z.string().min(1, "Snippet code is required"), - slotId: z.number().optional(), - productIds: z.array(z.number().int().positive()).min(1, "At least one product is required"), - validTill: z.string().optional(), - isPermanent: z.boolean().default(false) -}); - -const updateSnippetSchema = z.object({ - id: z.number().int().positive(), - updates: createSnippetSchema.partial().extend({ - snippetCode: z.string().min(1).optional(), - productIds: z.array(z.number().int().positive()).optional(), - isPermanent: z.boolean().default(false) - }), -}); - -export const vendorSnippetsRouter = router({ - create: protectedProcedure - .input(createSnippetSchema) - .mutation(async ({ input, ctx }) => { - const { snippetCode, slotId, productIds, validTill, isPermanent } = input; - - // Get staff user ID from auth middleware - const staffUserId = ctx.staffUser?.id; - if (!staffUserId) { - throw new Error("Unauthorized"); - } - - // Validate slot exists - if(slotId) { - const slot = await db.query.deliverySlotInfo.findFirst({ - where: eq(deliverySlotInfo.id, slotId), - }); - if (!slot) { - throw new Error("Invalid slot ID"); - } - } - - // Validate products exist - const products = await db.query.productInfo.findMany({ - where: inArray(productInfo.id, productIds), - }); - if (products.length !== productIds.length) { - throw new Error("One or more invalid product IDs"); - } - - // Check if snippet code already exists - const existingSnippet = await db.query.vendorSnippets.findFirst({ - where: eq(vendorSnippets.snippetCode, snippetCode), - }); - if (existingSnippet) { - throw new Error("Snippet code already exists"); - } - - const result = await db.insert(vendorSnippets).values({ - snippetCode, - slotId, - productIds, - isPermanent, - validTill: validTill ? new Date(validTill) : undefined, - }).returning(); - - return result[0]; - }), - - getAll: protectedProcedure - .query(async () => { - console.log('from the vendor snipptes methods') - - try { - const result = await db.query.vendorSnippets.findMany({ - with: { - slot: true, - }, - orderBy: (vendorSnippets, { desc }) => [desc(vendorSnippets.createdAt)], - }); - - const snippetsWithProducts = await Promise.all( - result.map(async (snippet) => { - const products = await db.query.productInfo.findMany({ - where: inArray(productInfo.id, snippet.productIds), - columns: { id: true, name: true }, - }); - - return { - ...snippet, - accessUrl: `${appUrl}/vendor-order-list?id=${snippet.snippetCode}`, - products: products.map(p => ({ id: p.id, name: p.name })), - }; - }) - ); - - return snippetsWithProducts; - } - catch(e) { - console.log(e) - } - return []; - }), - - getById: protectedProcedure - .input(z.object({ id: z.number().int().positive() })) - .query(async ({ input }) => { - const { id } = input; - - const result = await db.query.vendorSnippets.findFirst({ - where: eq(vendorSnippets.id, id), - with: { - slot: true, - }, - }); - - if (!result) { - throw new Error("Vendor snippet not found"); - } - - return result; - }), - - update: protectedProcedure - .input(updateSnippetSchema) - .mutation(async ({ input }) => { - const { id, updates } = input; - - // Check if snippet exists - const existingSnippet = await db.query.vendorSnippets.findFirst({ - where: eq(vendorSnippets.id, id), - }); - if (!existingSnippet) { - throw new Error("Vendor snippet not found"); - } - - // Validate slot if being updated - if (updates.slotId) { - const slot = await db.query.deliverySlotInfo.findFirst({ - where: eq(deliverySlotInfo.id, updates.slotId), - }); - if (!slot) { - throw new Error("Invalid slot ID"); - } - } - - // Validate products if being updated - if (updates.productIds) { - const products = await db.query.productInfo.findMany({ - where: inArray(productInfo.id, updates.productIds), - }); - if (products.length !== updates.productIds.length) { - throw new Error("One or more invalid product IDs"); - } - } - - // Check snippet code uniqueness if being updated - if (updates.snippetCode && updates.snippetCode !== existingSnippet.snippetCode) { - const duplicateSnippet = await db.query.vendorSnippets.findFirst({ - where: eq(vendorSnippets.snippetCode, updates.snippetCode), - }); - if (duplicateSnippet) { - throw new Error("Snippet code already exists"); - } - } - - const updateData: any = { ...updates }; - if (updates.validTill !== undefined) { - updateData.validTill = updates.validTill ? new Date(updates.validTill) : null; - } - - const result = await db.update(vendorSnippets) - .set(updateData) - .where(eq(vendorSnippets.id, id)) - .returning(); - - if (result.length === 0) { - throw new Error("Failed to update vendor snippet"); - } - - return result[0]; - }), - - delete: protectedProcedure - .input(z.object({ id: z.number().int().positive() })) - .mutation(async ({ input }) => { - const { id } = input; - - const result = await db.delete(vendorSnippets) - .where(eq(vendorSnippets.id, id)) - .returning(); - - if (result.length === 0) { - throw new Error("Vendor snippet not found"); - } - - return { message: "Vendor snippet deleted successfully" }; - }), - - getOrdersBySnippet: publicProcedure - .input(z.object({ - snippetCode: z.string().min(1, "Snippet code is required") - })) - .query(async ({ input }) => { - const { snippetCode } = input; - - // Find the snippet - const snippet = await db.query.vendorSnippets.findFirst({ - where: eq(vendorSnippets.snippetCode, snippetCode), - }); - - if (!snippet) { - throw new Error("Vendor snippet not found"); - } - - // Check if snippet is still valid - if (snippet.validTill && new Date(snippet.validTill) < new Date()) { - throw new Error("Vendor snippet has expired"); - } - - // Query orders that match the snippet criteria - const matchingOrders = await db.query.orders.findMany({ - where: eq(orders.slotId, snippet.slotId!), - with: { - orderItems: { - with: { - product: { - with: { - unit: true, - }, - }, - }, - }, - orderStatus: true, - user: true, - slot: true, - }, - orderBy: (orders, { desc }) => [desc(orders.createdAt)], - }); - - // Filter orders that contain at least one of the snippet's products - const filteredOrders = matchingOrders.filter(order => { - const status = order.orderStatus; - if (status[0].isCancelled) return false; - const orderProductIds = order.orderItems.map(item => item.productId); - return snippet.productIds.some(productId => orderProductIds.includes(productId)); - }); - - // Format the response - const formattedOrders = filteredOrders.map(order => { - // Filter orderItems to only include products attached to the snippet - const attachedOrderItems = order.orderItems.filter(item => - snippet.productIds.includes(item.productId) - ); - - const products = attachedOrderItems.map(item => ({ - orderItemId: item.id, - productId: item.productId, - productName: item.product.name, - quantity: parseFloat(item.quantity), - productSize: item.product.productQuantity, - price: parseFloat(item.price.toString()), - unit: item.product.unit?.shortNotation || 'unit', - subtotal: parseFloat(item.price.toString()) * parseFloat(item.quantity), - is_packaged: item.is_packaged, - is_package_verified: item.is_package_verified, - })); - - const orderTotal = products.reduce((sum, p) => sum + p.subtotal, 0); - - return { - orderId: `ORD${order.id}`, - orderDate: order.createdAt.toISOString(), - customerName: order.user.name, - totalAmount: orderTotal, - slotInfo: order.slot ? { - time: order.slot.deliveryTime.toISOString(), - sequence: order.slot.deliverySequence, - } : null, - products, - matchedProducts: snippet.productIds, // All snippet products are considered matched - snippetCode: snippet.snippetCode, - }; - }); - - return { - success: true, - data: formattedOrders, - snippet: { - id: snippet.id, - snippetCode: snippet.snippetCode, - slotId: snippet.slotId, - productIds: snippet.productIds, - validTill: snippet.validTill?.toISOString(), - createdAt: snippet.createdAt.toISOString(), - isPermanent: snippet.isPermanent, - }, - }; - }), - - getVendorOrders: protectedProcedure - .query(async () => { - const vendorOrders = await db.query.orders.findMany({ - with: { - user: true, - orderItems: { - with: { - product: { - with: { - unit: true, - }, - }, - }, - }, - }, - orderBy: (orders, { desc }) => [desc(orders.createdAt)], - }); - - return vendorOrders.map(order => ({ - id: order.id, - status: 'pending', // Default status since orders table may not have status field - orderDate: order.createdAt.toISOString(), - totalQuantity: order.orderItems.reduce((sum, item) => sum + parseFloat(item.quantity || '0'), 0), - products: order.orderItems.map(item => ({ - name: item.product.name, - quantity: parseFloat(item.quantity || '0'), - unit: item.product.unit?.shortNotation || 'unit', - })), - })); - }), - - getUpcomingSlots: publicProcedure - .query(async () => { - const threeHoursAgo = dayjs().subtract(3, 'hour').toDate(); - const slots = await db.query.deliverySlotInfo.findMany({ - where: and( - eq(deliverySlotInfo.isActive, true), - gt(deliverySlotInfo.deliveryTime, threeHoursAgo) - ), - orderBy: asc(deliverySlotInfo.deliveryTime), - }); - - return { - success: true, - data: slots.map(slot => ({ - id: slot.id, - deliveryTime: slot.deliveryTime.toISOString(), - freezeTime: slot.freezeTime.toISOString(), - deliverySequence: slot.deliverySequence, - })), - }; - }), - - getOrdersBySnippetAndSlot: publicProcedure - .input(z.object({ - snippetCode: z.string().min(1, "Snippet code is required"), - slotId: z.number().int().positive("Valid slot ID is required"), - })) - .query(async ({ input }) => { - const { snippetCode, slotId } = input; - - // Find the snippet - const snippet = await db.query.vendorSnippets.findFirst({ - where: eq(vendorSnippets.snippetCode, snippetCode), - }); - - if (!snippet) { - throw new Error("Vendor snippet not found"); - } - - // Find the slot - const slot = await db.query.deliverySlotInfo.findFirst({ - where: eq(deliverySlotInfo.id, slotId), - }); - - if (!slot) { - throw new Error("Slot not found"); - } - - // Query orders that match the slot and snippet criteria - const matchingOrders = await db.query.orders.findMany({ - where: eq(orders.slotId, slotId), - with: { - orderItems: { - with: { - product: { - with: { - unit: true, - }, - }, - }, - }, - orderStatus: true, - user: true, - slot: true, - }, - orderBy: (orders, { desc }) => [desc(orders.createdAt)], - }); - - // Filter orders that contain at least one of the snippet's products - const filteredOrders = matchingOrders.filter(order => { - const status = order.orderStatus; - if (status[0]?.isCancelled) return false; - const orderProductIds = order.orderItems.map(item => item.productId); - return snippet.productIds.some(productId => orderProductIds.includes(productId)); - }); - - // Format the response - const formattedOrders = filteredOrders.map(order => { - // Filter orderItems to only include products attached to the snippet - const attachedOrderItems = order.orderItems.filter(item => - snippet.productIds.includes(item.productId) - ); - - const products = attachedOrderItems.map(item => ({ - orderItemId: item.id, - productId: item.productId, - productName: item.product.name, - quantity: parseFloat(item.quantity), - price: parseFloat(item.price.toString()), - unit: item.product.unit?.shortNotation || 'unit', - subtotal: parseFloat(item.price.toString()) * parseFloat(item.quantity), - productSize: item.product.productQuantity, - is_packaged: item.is_packaged, - is_package_verified: item.is_package_verified, - })); - - const orderTotal = products.reduce((sum, p) => sum + p.subtotal, 0); - - return { - orderId: `ORD${order.id}`, - orderDate: order.createdAt.toISOString(), - customerName: order.user.name, - totalAmount: orderTotal, - slotInfo: order.slot ? { - time: order.slot.deliveryTime.toISOString(), - sequence: order.slot.deliverySequence, - } : null, - products, - matchedProducts: snippet.productIds, - snippetCode: snippet.snippetCode, - }; - }); - - return { - success: true, - data: formattedOrders, - snippet: { - id: snippet.id, - snippetCode: snippet.snippetCode, - slotId: snippet.slotId, - productIds: snippet.productIds, - validTill: snippet.validTill?.toISOString(), - createdAt: snippet.createdAt.toISOString(), - isPermanent: snippet.isPermanent, - }, - selectedSlot: { - id: slot.id, - deliveryTime: slot.deliveryTime.toISOString(), - freezeTime: slot.freezeTime.toISOString(), - deliverySequence: slot.deliverySequence, - }, - }; - }), - - updateOrderItemPackaging: publicProcedure - .input(z.object({ - orderItemId: z.number().int().positive("Valid order item ID required"), - is_packaged: z.boolean() - })) - .mutation(async ({ input, ctx }) => { - const { orderItemId, is_packaged } = input; - - // Get staff user ID from auth middleware - // const staffUserId = ctx.staffUser?.id; - // if (!staffUserId) { - // throw new Error("Unauthorized"); - // } - - // Check if order item exists and get related data - const orderItem = await db.query.orderItems.findFirst({ - where: eq(orderItems.id, orderItemId), - with: { - order: { - with: { - slot: true - } - } - } - }); - - if (!orderItem) { - throw new Error("Order item not found"); - } - - // Check if this order item belongs to a slot that has vendor snippets - // This ensures only order items from vendor-accessible orders can be updated - if (!orderItem.order.slotId) { - throw new Error("Order item not associated with a vendor slot"); - } - - const snippetExists = await db.query.vendorSnippets.findFirst({ - where: eq(vendorSnippets.slotId, orderItem.order.slotId), - }); - - if (!snippetExists) { - throw new Error("No vendor snippet found for this order's slot"); - } - - // Update the is_packaged field - const result = await db.update(orderItems) - .set({ is_packaged }) - .where(eq(orderItems.id, orderItemId)) - .returning(); - - if (result.length === 0) { - throw new Error("Failed to update packaging status"); - } - - return { - success: true, - orderItemId, - is_packaged - }; - }), -}); \ No newline at end of file diff --git a/verifier/dataAccessors/demo.txt b/verifier/dataAccessors/demo.txt deleted file mode 100644 index e69de29..0000000 diff --git a/verifier/user-apis/apis/address.ts b/verifier/user-apis/apis/address.ts deleted file mode 100644 index 0022c7e..0000000 --- a/verifier/user-apis/apis/address.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { router, protectedProcedure } from '@/src/trpc/trpc-index'; -import { z } from 'zod'; -import { db } from '@/src/db/db_index'; -import { addresses, orders, orderStatus, deliverySlotInfo } from '@/src/db/schema'; -import { eq, and, gte } from 'drizzle-orm'; -import dayjs from 'dayjs'; -import { extractCoordsFromRedirectUrl } from '@/src/lib/license-util'; - -export const addressRouter = router({ - getDefaultAddress: protectedProcedure - .query(async ({ ctx }) => { - const userId = ctx.user.userId; - - const [defaultAddress] = await db - .select() - .from(addresses) - .where(and(eq(addresses.userId, userId), eq(addresses.isDefault, true))) - .limit(1); - - return { success: true, data: defaultAddress || null }; - }), - - getUserAddresses: protectedProcedure - .query(async ({ ctx }) => { - const userId = ctx.user.userId; - const userAddresses = await db.select().from(addresses).where(eq(addresses.userId, userId)); - return { success: true, data: userAddresses }; - }), - - createAddress: protectedProcedure - .input(z.object({ - name: z.string().min(1, 'Name is required'), - phone: z.string().min(1, 'Phone is required'), - addressLine1: z.string().min(1, 'Address line 1 is required'), - addressLine2: z.string().optional(), - city: z.string().min(1, 'City is required'), - state: z.string().min(1, 'State is required'), - pincode: z.string().min(1, 'Pincode is required'), - isDefault: z.boolean().optional(), - latitude: z.number().optional(), - longitude: z.number().optional(), - googleMapsUrl: z.string().optional(), - })) - .mutation(async ({ input, ctx }) => { - const userId = ctx.user.userId; - const { name, phone, addressLine1, addressLine2, city, state, pincode, isDefault, googleMapsUrl } = input; - - let { latitude, longitude } = input; - - if (googleMapsUrl && latitude === undefined && longitude === undefined) { - const coords = await extractCoordsFromRedirectUrl(googleMapsUrl); - if (coords) { - latitude = Number(coords.latitude); - longitude = Number(coords.longitude); - } - } - - // Validate required fields - if (!name || !phone || !addressLine1 || !city || !state || !pincode) { - throw new Error('Missing required fields'); - } - - // If setting as default, unset other defaults - if (isDefault) { - await db.update(addresses).set({ isDefault: false }).where(eq(addresses.userId, userId)); - } - - const [newAddress] = await db.insert(addresses).values({ - userId, - name, - phone, - addressLine1, - addressLine2, - city, - state, - pincode, - isDefault: isDefault || false, - latitude, - longitude, - googleMapsUrl, - }).returning(); - - return { success: true, data: newAddress }; - }), - - updateAddress: protectedProcedure - .input(z.object({ - id: z.number().int().positive(), - name: z.string().min(1, 'Name is required'), - phone: z.string().min(1, 'Phone is required'), - addressLine1: z.string().min(1, 'Address line 1 is required'), - addressLine2: z.string().optional(), - city: z.string().min(1, 'City is required'), - state: z.string().min(1, 'State is required'), - pincode: z.string().min(1, 'Pincode is required'), - isDefault: z.boolean().optional(), - latitude: z.number().optional(), - longitude: z.number().optional(), - googleMapsUrl: z.string().optional(), - })) - .mutation(async ({ input, ctx }) => { - const userId = ctx.user.userId; - const { id, name, phone, addressLine1, addressLine2, city, state, pincode, isDefault, googleMapsUrl } = input; - - let { latitude, longitude } = input; - - if (googleMapsUrl && latitude === undefined && longitude === undefined) { - const coords = await extractCoordsFromRedirectUrl(googleMapsUrl); - if (coords) { - latitude = Number(coords.latitude); - longitude = Number(coords.longitude); - } - } - - // Check if address exists and belongs to user - const existingAddress = await db.select().from(addresses).where(and(eq(addresses.id, id), eq(addresses.userId, userId))).limit(1); - if (existingAddress.length === 0) { - throw new Error('Address not found'); - } - - // If setting as default, unset other defaults - if (isDefault) { - await db.update(addresses).set({ isDefault: false }).where(eq(addresses.userId, userId)); - } - - const updateData: any = { - name, - phone, - addressLine1, - addressLine2, - city, - state, - pincode, - isDefault: isDefault || false, - googleMapsUrl, - }; - - if (latitude !== undefined) { - updateData.latitude = latitude; - } - if (longitude !== undefined) { - updateData.longitude = longitude; - } - - const [updatedAddress] = await db.update(addresses).set(updateData).where(and(eq(addresses.id, id), eq(addresses.userId, userId))).returning(); - - return { success: true, data: updatedAddress }; - }), - - deleteAddress: protectedProcedure - .input(z.object({ - id: z.number().int().positive(), - })) - .mutation(async ({ input, ctx }) => { - const userId = ctx.user.userId; - const { id } = input; - - // Check if address exists and belongs to user - const existingAddress = await db.select().from(addresses).where(and(eq(addresses.id, id), eq(addresses.userId, userId))).limit(1); - if (existingAddress.length === 0) { - throw new Error('Address not found or does not belong to user'); - } - - // Check if address is attached to any ongoing orders using joins - const ongoingOrders = await db.select({ - order: orders, - status: orderStatus, - slot: deliverySlotInfo - }) - .from(orders) - .innerJoin(orderStatus, eq(orders.id, orderStatus.orderId)) - .innerJoin(deliverySlotInfo, eq(orders.slotId, deliverySlotInfo.id)) - .where(and( - eq(orders.addressId, id), - eq(orderStatus.isCancelled, false), - gte(deliverySlotInfo.deliveryTime, new Date()) - )) - .limit(1); - - if (ongoingOrders.length > 0) { - throw new Error('Address is attached to an ongoing order. Please cancel the order first.'); - } - - // Prevent deletion of default address - if (existingAddress[0].isDefault) { - throw new Error('Cannot delete default address. Please set another address as default first.'); - } - - // Delete the address - await db.delete(addresses).where(and(eq(addresses.id, id), eq(addresses.userId, userId))); - - return { success: true, message: 'Address deleted successfully' }; - }), -}); diff --git a/verifier/user-apis/apis/auth.ts b/verifier/user-apis/apis/auth.ts deleted file mode 100644 index 986f8e8..0000000 --- a/verifier/user-apis/apis/auth.ts +++ /dev/null @@ -1,581 +0,0 @@ -import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index'; -import { z } from 'zod'; -import bcrypt from 'bcryptjs'; -import { eq } from 'drizzle-orm'; -import { db } from '@/src/db/db_index'; -import { - users, userCreds, userDetails, addresses, cartItems, complaints, - couponApplicableUsers, couponUsage, notifCreds, notifications, - orderItems, orderStatus, orders, payments, refunds, - productReviews, reservedCoupons -} from '@/src/db/schema'; -import { generateSignedUrlFromS3Url, claimUploadUrl, scaffoldAssetUrl } from '@/src/lib/s3-client'; -import { deleteS3Image } from '@/src/lib/delete-image'; -import { ApiError } from '@/src/lib/api-error'; -import catchAsync from '@/src/lib/catch-async'; -import { sendOtp, verifyOtpUtil, getOtpCreds } from '@/src/lib/otp-utils'; -import { signToken } from '@/src/lib/jwt-utils'; - -interface LoginRequest { - identifier: string; // email or mobile - password: string; -} - -interface RegisterRequest { - name: string; - email: string; - mobile: 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 = async (userId: number): Promise => { - return signToken({ userId }); -}; - - - -export const authRouter = router({ - login: publicProcedure - .input(z.object({ - identifier: z.string().min(1, 'Email/mobile is required'), - password: z.string().min(1, 'Password is required'), - })) - .mutation(async ({ input }) => { - const { identifier, password }: LoginRequest = input; - - if (!identifier || !password) { - throw new ApiError('Email/mobile and password are required', 400); - } - - // Find user by email or mobile - const [user] = await db - .select() - .from(users) - .where(eq(users.email, identifier.toLowerCase())) - .limit(1); - - let foundUser = user; - - if (!foundUser) { - // Try mobile if email didn't work - const [userByMobile] = await db - .select() - .from(users) - .where(eq(users.mobile, identifier)) - .limit(1); - foundUser = userByMobile; - } - - if (!foundUser) { - throw new ApiError('Invalid credentials', 401); - } - - // Get user credentials - const [userCredentials] = await db - .select() - .from(userCreds) - .where(eq(userCreds.userId, foundUser.id)) - .limit(1); - - if (!userCredentials) { - throw new ApiError('Account setup incomplete. Please contact support.', 401); - } - - // Get user details for profile image - const [userDetail] = await db - .select() - .from(userDetails) - .where(eq(userDetails.userId, foundUser.id)) - .limit(1); - - // Generate signed URL for profile image if it exists - const profileImageSignedUrl = userDetail?.profileImage - ? await generateSignedUrlFromS3Url(userDetail.profileImage) - : null; - - // Verify password - const isPasswordValid = await bcrypt.compare(password, userCredentials.userPassword); - if (!isPasswordValid) { - throw new ApiError('Invalid credentials', 401); - } - - const token = await generateToken(foundUser.id); - - const response: AuthResponse = { - token, - user: { - id: foundUser.id, - name: foundUser.name, - email: foundUser.email, - mobile: foundUser.mobile, - createdAt: foundUser.createdAt.toISOString(), - profileImage: profileImageSignedUrl, - bio: userDetail?.bio || null, - dateOfBirth: userDetail?.dateOfBirth || null, - gender: userDetail?.gender || null, - occupation: userDetail?.occupation || null, - }, - }; - - return { - success: true, - data: response, - }; - }), - - register: publicProcedure - .input(z.object({ - name: z.string().min(1, 'Name is required'), - email: z.string().email('Invalid email format'), - mobile: z.string().min(1, 'Mobile is required'), - password: z.string().min(1, 'Password is required'), - imageKey: z.string().optional(), - })) - .mutation(async ({ input }) => { - const { name, email, mobile, password, imageKey } = input; - - if (!name || !email || !mobile || !password) { - throw new ApiError('All fields are required', 400); - } - - // Validate email format - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - if (!emailRegex.test(email)) { - throw new ApiError('Invalid email format', 400); - } - - // Validate mobile format (Indian mobile numbers) - const cleanMobile = mobile.replace(/\D/g, ''); - if (cleanMobile.length !== 10 || !/^[6-9]/.test(cleanMobile)) { - throw new ApiError('Invalid mobile number', 400); - } - - // Check if email already exists - const [existingEmail] = await db - .select() - .from(users) - .where(eq(users.email, email.toLowerCase())) - .limit(1); - - if (existingEmail) { - throw new ApiError('Email already registered', 409); - } - - // Check if mobile already exists - const [existingMobile] = await db - .select() - .from(users) - .where(eq(users.mobile, cleanMobile)) - .limit(1); - - if (existingMobile) { - throw new ApiError('Mobile number already registered', 409); - } - - // Hash password - const hashedPassword = await bcrypt.hash(password, 12); - - // Create user and credentials in a transaction - const newUser = await db.transaction(async (tx) => { - // Create user - const [user] = await tx - .insert(users) - .values({ - name: name.trim(), - email: email.toLowerCase().trim(), - mobile: cleanMobile, - }) - .returning(); - - // Create user credentials - await tx - .insert(userCreds) - .values({ - userId: user.id, - userPassword: hashedPassword, - }); - - // Create user details with profile image if provided - if (imageKey) { - await tx.insert(userDetails).values({ - userId: user.id, - profileImage: imageKey, - }); - } - - return user; - }); - - // Claim upload URL if image was provided - if (imageKey) { - try { - await claimUploadUrl(imageKey); - } catch (e) { - console.warn(`Failed to claim upload URL for key: ${imageKey}`, e); - } - } - - const token = await generateToken(newUser.id); - - // Get user details for profile image - const [userDetail] = await db - .select() - .from(userDetails) - .where(eq(userDetails.userId, newUser.id)) - .limit(1); - - const profileImageUrl = userDetail?.profileImage - ? scaffoldAssetUrl(userDetail.profileImage) - : null; - - const response: AuthResponse = { - token, - user: { - id: newUser.id, - name: newUser.name, - email: newUser.email, - mobile: newUser.mobile, - createdAt: newUser.createdAt.toISOString(), - profileImage: profileImageUrl, - }, - }; - - return { - success: true, - data: response, - }; - }), - - sendOtp: publicProcedure - .input(z.object({ - mobile: z.string(), - })) - .mutation(async ({ input }) => { - - return await sendOtp(input.mobile); - }), - - verifyOtp: publicProcedure - .input(z.object({ - mobile: z.string(), - otp: z.string(), - })) - .mutation(async ({ input }) => { - const verificationId = getOtpCreds(input.mobile); - if (!verificationId) { - throw new ApiError("OTP not sent or expired", 400); - } - const isVerified = await verifyOtpUtil(input.mobile, input.otp, verificationId); - - if (!isVerified) { - throw new ApiError("Invalid OTP", 400); - } - - // Find user - let user = await db.query.users.findFirst({ - where: eq(users.mobile, input.mobile), - }); - - // If user doesn't exist, create one - if (!user) { - const [newUser] = await db - .insert(users) - .values({ - name: null, - email: null, - mobile: input.mobile, - }) - .returning(); - user = newUser; - } - - // Generate JWT - const token = await generateToken(user.id); - - return { - success: true, - token, - user: { - id: user.id, - name: user.name, - email: user.email, - mobile: user.mobile, - createdAt: user.createdAt.toISOString(), - profileImage: null, - }, - }; - }), - - updatePassword: protectedProcedure - .input(z.object({ - password: z.string().min(6, 'Password must be at least 6 characters'), - })) - .mutation(async ({ input, ctx }) => { - const userId = ctx.user.userId; - if (!userId) { - throw new ApiError('User not authenticated', 401); - } - - const hashedPassword = await bcrypt.hash(input.password, 10); - - // Insert if not exists, then update if exists - try { - await db.insert(userCreds).values({ - userId: userId, - userPassword: hashedPassword, - }); - // Insert succeeded - new credentials created - } catch (error: any) { - // Insert failed - check if it's a unique constraint violation - if (error.code === '23505') { // PostgreSQL unique constraint violation - // Update existing credentials - await db.update(userCreds).set({ - userPassword: hashedPassword, - }).where(eq(userCreds.userId, userId)); - } else { - // Re-throw if it's a different error - throw error; - } - } - - return { success: true, message: 'Password updated successfully' }; - }), - - getProfile: protectedProcedure - .query(async ({ ctx }) => { - const userId = ctx.user.userId; - - if (!userId) { - throw new ApiError('User not authenticated', 401); - } - - const [user] = await db - .select() - .from(users) - .where(eq(users.id, userId)) - .limit(1); - - if (!user) { - throw new ApiError('User not found', 404); - } - - // Get user details for profile image - const [userDetail] = await db - .select() - .from(userDetails) - .where(eq(userDetails.userId, userId)) - .limit(1); - - const profileImageUrl = userDetail?.profileImage - ? scaffoldAssetUrl(userDetail.profileImage) - : null; - - return { - success: true, - data: { - id: user.id, - name: user.name, - email: user.email, - mobile: user.mobile, - profileImage: profileImageUrl, - bio: userDetail?.bio || null, - dateOfBirth: userDetail?.dateOfBirth || null, - gender: userDetail?.gender || null, - occupation: userDetail?.occupation || null, - }, - }; - }), - - updateProfile: protectedProcedure - .input(z.object({ - name: z.string().min(1, 'Name is required').optional(), - email: z.string().email('Invalid email format').optional(), - bio: z.string().optional(), - dateOfBirth: z.string().optional(), - gender: z.string().optional(), - occupation: z.string().optional(), - imageKey: z.string().optional(), - })) - .mutation(async ({ input, ctx }) => { - const userId = ctx.user.userId; - const { imageKey, ...updateData } = input; - - if (!userId) { - throw new ApiError('User not authenticated', 401); - } - - // Get current user details - const currentDetail = await db.query.userDetails.findFirst({ - where: eq(userDetails.userId, userId), - }); - - let newImageUrl: string | null | undefined = currentDetail?.profileImage; - - // Handle new image upload (only if different from existing) - if (imageKey && imageKey !== currentDetail?.profileImage) { - // Delete old image if exists - if (currentDetail?.profileImage) { - try { - await deleteS3Image(currentDetail.profileImage); - } catch (e) { - console.error(`Failed to delete old image: ${currentDetail.profileImage}`, e); - } - } - newImageUrl = imageKey; - - // Claim upload URL - try { - await claimUploadUrl(imageKey); - } catch (e) { - console.warn(`Failed to claim upload URL for key: ${imageKey}`, e); - } - } - - // Update user name if provided - if (updateData.name) { - await db.update(users) - .set({ name: updateData.name.trim() }) - .where(eq(users.id, userId)); - } - - // Update user email if provided - if (updateData.email) { - // Check if email already exists (but belongs to different user) - const [existingUser] = await db - .select() - .from(users) - .where(eq(users.email, updateData.email.toLowerCase().trim())) - .limit(1); - - if (existingUser && existingUser.id !== userId) { - throw new ApiError('Email already in use by another account', 409); - } - - await db.update(users) - .set({ email: updateData.email.toLowerCase().trim() }) - .where(eq(users.id, userId)); - } - - // Upsert user details - if (currentDetail) { - // Update existing - await db.update(userDetails) - .set({ - ...updateData, - profileImage: newImageUrl, - }) - .where(eq(userDetails.userId, userId)); - } else { - // Insert new - await db.insert(userDetails).values({ - userId: userId, - ...updateData, - profileImage: newImageUrl, - }); - } - - return { - success: true, - message: 'Profile updated successfully', - }; - }), - - deleteAccount: protectedProcedure - .input(z.object({ - mobile: z.string().min(10, 'Mobile number is required'), - })) - .mutation(async ({ ctx, input }) => { - const userId = ctx.user.userId; - const { mobile } = input; - - if (!userId) { - throw new ApiError('User not authenticated', 401); - } - - // Double-check: verify user exists and is the authenticated user - const existingUser = await db.query.users.findFirst({ - where: eq(users.id, userId), - columns: { id: true, mobile: true }, - }); - - if (!existingUser) { - throw new ApiError('User not found', 404); - } - - // Additional verification: ensure we're not deleting someone else's data - // The JWT token should already ensure this, but double-checking - if (existingUser.id !== userId) { - throw new ApiError('Unauthorized: Cannot delete another user\'s account', 403); - } - - // Verify mobile number matches user's registered mobile - const cleanInputMobile = mobile.replace(/\D/g, ''); - const cleanUserMobile = existingUser.mobile?.replace(/\D/g, ''); - - if (cleanInputMobile !== cleanUserMobile) { - throw new ApiError('Mobile number does not match your registered number', 400); - } - - // Use transaction for atomic deletion - await db.transaction(async (tx) => { - // Phase 1: Direct references (safe to delete first) - await tx.delete(notifCreds).where(eq(notifCreds.userId, userId)); - await tx.delete(couponApplicableUsers).where(eq(couponApplicableUsers.userId, userId)); - await tx.delete(couponUsage).where(eq(couponUsage.userId, userId)); - 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)); - - // Update reserved coupons (set redeemedBy to null) - await tx.update(reservedCoupons) - .set({ redeemedBy: null }) - .where(eq(reservedCoupons.redeemedBy, userId)); - - // Phase 2: Order dependencies - const userOrders = await tx - .select({ id: orders.id }) - .from(orders) - .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)); - // Additional coupon usage entries linked to specific orders - await tx.delete(couponUsage).where(eq(couponUsage.orderId, order.id)); - await tx.delete(complaints).where(eq(complaints.orderId, order.id)); - } - - // Delete orders - await tx.delete(orders).where(eq(orders.userId, userId)); - - // Phase 3: Addresses (now safe since orders are deleted) - await tx.delete(addresses).where(eq(addresses.userId, userId)); - - // Phase 4: Core user data - await tx.delete(userDetails).where(eq(userDetails.userId, userId)); - await tx.delete(userCreds).where(eq(userCreds.userId, userId)); - await tx.delete(users).where(eq(users.id, userId)); - }); - - return { success: true, message: 'Account deleted successfully' }; - }), -}); diff --git a/verifier/user-apis/apis/banners.ts b/verifier/user-apis/apis/banners.ts deleted file mode 100644 index 8e6a001..0000000 --- a/verifier/user-apis/apis/banners.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { db } from '@/src/db/db_index'; -import { homeBanners } from '@/src/db/schema'; -import { publicProcedure, router } from '@/src/trpc/trpc-index'; -import { scaffoldAssetUrl } from '@/src/lib/s3-client'; -import { isNotNull, asc } from 'drizzle-orm'; - -export async function scaffoldBanners() { - const banners = await db.query.homeBanners.findMany({ - where: isNotNull(homeBanners.serialNum), // Only show assigned banners - orderBy: asc(homeBanners.serialNum), // Order by slot number 1-4 - }); - - // Convert S3 keys to signed URLs for client - const bannersWithSignedUrls = banners.map((banner) => ({ - ...banner, - imageUrl: banner.imageUrl ? scaffoldAssetUrl(banner.imageUrl) : banner.imageUrl, - })); - - return { - banners: bannersWithSignedUrls, - }; -} - -export const bannerRouter = router({ - getBanners: publicProcedure - .query(async () => { - const response = await scaffoldBanners(); - return response; - }), -}); diff --git a/verifier/user-apis/apis/cart.ts b/verifier/user-apis/apis/cart.ts deleted file mode 100644 index a2495bc..0000000 --- a/verifier/user-apis/apis/cart.ts +++ /dev/null @@ -1,244 +0,0 @@ -import { router, protectedProcedure, publicProcedure } from '@/src/trpc/trpc-index'; -import { z } from 'zod'; -import { db } from '@/src/db/db_index'; -import { cartItems, productInfo, units, productSlots, deliverySlotInfo } from '@/src/db/schema'; -import { eq, and, sql, inArray, gt } from 'drizzle-orm'; -import { ApiError } from '@/src/lib/api-error'; -import { generateSignedUrlsFromS3Urls, scaffoldAssetUrl } from '@/src/lib/s3-client'; -import { getProductSlots, getMultipleProductsSlots } from '@/src/stores/slot-store'; - -interface CartResponse { - items: any[]; - totalItems: number; - totalAmount: number; -} - -const getCartData = async (userId: number): Promise => { - const cartItemsWithProducts = await db - .select({ - cartId: cartItems.id, - productId: productInfo.id, - productName: productInfo.name, - productPrice: productInfo.price, - productImages: productInfo.images, - productQuantity: productInfo.productQuantity, - isOutOfStock: productInfo.isOutOfStock, - unitShortNotation: units.shortNotation, - quantity: cartItems.quantity, - addedAt: cartItems.addedAt, - }) - .from(cartItems) - .innerJoin(productInfo, eq(cartItems.productId, productInfo.id)) - .innerJoin(units, eq(productInfo.unitId, units.id)) - .where(eq(cartItems.userId, userId)); - - // Generate signed URLs for images - const cartWithSignedUrls = await Promise.all( - cartItemsWithProducts.map(async (item) => ({ - id: item.cartId, - productId: item.productId, - quantity: parseFloat(item.quantity), - addedAt: item.addedAt, - product: { - id: item.productId, - name: item.productName, - price: item.productPrice, - productQuantity: item.productQuantity, - unit: item.unitShortNotation, - isOutOfStock: item.isOutOfStock, - images: scaffoldAssetUrl((item.productImages as string[]) || []), - }, - subtotal: parseFloat(item.productPrice.toString()) * parseFloat(item.quantity), - })) - ); - - const totalAmount = cartWithSignedUrls.reduce((sum, item) => sum + item.subtotal, 0); - - return { - items: cartWithSignedUrls, - totalItems: cartWithSignedUrls.length, - totalAmount, - }; -}; - -export const cartRouter = router({ - getCart: protectedProcedure - .query(async ({ ctx }): Promise => { - const userId = ctx.user.userId; - return await getCartData(userId); - }), - - addToCart: protectedProcedure - .input(z.object({ - productId: z.number().int().positive(), - quantity: z.number().int().positive(), - })) - .mutation(async ({ input, ctx }): Promise => { - const userId = ctx.user.userId; - const { productId, quantity } = input; - - // Validate input - if (!productId || !quantity || quantity <= 0) { - throw new ApiError("Product ID and positive quantity required", 400); - } - - // Check if product exists - const product = await db.query.productInfo.findFirst({ - where: eq(productInfo.id, productId), - }); - - if (!product) { - throw new ApiError("Product not found", 404); - } - - // Check if item already exists in cart - const existingItem = await db.query.cartItems.findFirst({ - where: and(eq(cartItems.userId, userId), eq(cartItems.productId, productId)), - }); - - if (existingItem) { - // Update quantity - await db.update(cartItems) - .set({ - quantity: sql`${cartItems.quantity} + ${quantity}`, - }) - .where(eq(cartItems.id, existingItem.id)); - } else { - // Insert new item - await db.insert(cartItems).values({ - userId, - productId, - quantity: quantity.toString(), - }); - } - - // Return updated cart - return await getCartData(userId); - }), - - updateCartItem: protectedProcedure - .input(z.object({ - itemId: z.number().int().positive(), - quantity: z.number().int().min(0), - })) - .mutation(async ({ input, ctx }): Promise => { - const userId = ctx.user.userId; - const { itemId, quantity } = input; - - if (!quantity || quantity <= 0) { - throw new ApiError("Positive quantity required", 400); - } - - const [updatedItem] = await db.update(cartItems) - .set({ quantity: quantity.toString() }) - .where(and( - eq(cartItems.id, itemId), - eq(cartItems.userId, userId) - )) - .returning(); - - if (!updatedItem) { - throw new ApiError("Cart item not found", 404); - } - - // Return updated cart - return await getCartData(userId); - }), - - removeFromCart: protectedProcedure - .input(z.object({ - itemId: z.number().int().positive(), - })) - .mutation(async ({ input, ctx }): Promise => { - const userId = ctx.user.userId; - const { itemId } = input; - - const [deletedItem] = await db.delete(cartItems) - .where(and( - eq(cartItems.id, itemId), - eq(cartItems.userId, userId) - )) - .returning(); - - if (!deletedItem) { - throw new ApiError("Cart item not found", 404); - } - - // Return updated cart - return await getCartData(userId); - }), - - clearCart: protectedProcedure - .mutation(async ({ ctx }) => { - const userId = ctx.user.userId; - - await db.delete(cartItems).where(eq(cartItems.userId, userId)); - - return { - items: [], - totalItems: 0, - totalAmount: 0, - message: "Cart cleared successfully", - }; - }), - - // Original DB-based getCartSlots (commented out) - // getCartSlots: publicProcedure - // .input(z.object({ - // productIds: z.array(z.number().int().positive()) - // })) - // .query(async ({ input }) => { - // const { productIds } = input; - // - // if (productIds.length === 0) { - // return {}; - // } - // - // // Get slots for these products where freeze time is after current time - // const slotsData = await db - // .select({ - // productId: productSlots.productId, - // slotId: deliverySlotInfo.id, - // deliveryTime: deliverySlotInfo.deliveryTime, - // freezeTime: deliverySlotInfo.freezeTime, - // isActive: deliverySlotInfo.isActive, - // }) - // .from(productSlots) - // .innerJoin(deliverySlotInfo, eq(productSlots.slotId, deliverySlotInfo.id)) - // .where(and( - // inArray(productSlots.productId, productIds), - // gt(deliverySlotInfo.freezeTime, sql`NOW()`), - // eq(deliverySlotInfo.isActive, true) - // )); - // - // // Group by productId - // const result: Record = {}; - // slotsData.forEach(slot => { - // if (!result[slot.productId]) { - // result[slot.productId] = []; - // } - // result[slot.productId].push({ - // id: slot.slotId, - // deliveryTime: slot.deliveryTime, - // freezeTime: slot.freezeTime, - // }); - // }); - // - // return result; - // }), - - // Cache-based getCartSlots - getCartSlots: publicProcedure - .input(z.object({ - productIds: z.array(z.number().int().positive()) - })) - .query(async ({ input }) => { - const { productIds } = input; - - if (productIds.length === 0) { - return {}; - } - - return await getMultipleProductsSlots(productIds); - }), -}); diff --git a/verifier/user-apis/apis/complaint.ts b/verifier/user-apis/apis/complaint.ts deleted file mode 100644 index 8006f13..0000000 --- a/verifier/user-apis/apis/complaint.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { router, protectedProcedure } from '@/src/trpc/trpc-index'; -import { z } from 'zod'; -import { db } from '@/src/db/db_index'; -import { complaints } from '@/src/db/schema'; -import { eq } from 'drizzle-orm'; -import { scaffoldAssetUrl, claimUploadUrl } from '@/src/lib/s3-client'; - -export const complaintRouter = router({ - getAll: protectedProcedure - .query(async ({ ctx }) => { - const userId = ctx.user.userId; - - const userComplaints = await db - .select({ - id: complaints.id, - complaintBody: complaints.complaintBody, - response: complaints.response, - isResolved: complaints.isResolved, - createdAt: complaints.createdAt, - orderId: complaints.orderId, - images: complaints.images, - }) - .from(complaints) - .where(eq(complaints.userId, userId)) - .orderBy(complaints.createdAt); - - return { - complaints: userComplaints.map(c => ({ - id: c.id, - complaintBody: c.complaintBody, - response: c.response, - isResolved: c.isResolved, - createdAt: c.createdAt, - orderId: c.orderId, - images: c.images ? scaffoldAssetUrl(c.images as string[]) : [], - })), - }; - }), - - raise: protectedProcedure - .input(z.object({ - orderId: z.number().optional(), - complaintBody: z.string().min(1, 'Complaint body is required'), - imageKeys: z.array(z.string()).optional(), - })) - .mutation(async ({ input, ctx }) => { - const userId = ctx.user.userId; - const { orderId, complaintBody, imageKeys } = input; - - await db.insert(complaints).values({ - userId, - orderId: orderId || null, - complaintBody: complaintBody.trim(), - images: imageKeys || [], - }); - - // Claim upload URLs for images - if (imageKeys && imageKeys.length > 0) { - for (const key of imageKeys) { - try { - await claimUploadUrl(key); - } catch (e) { - console.warn(`Failed to claim upload URL for key: ${key}`, e); - } - } - } - - return { success: true, message: 'Complaint raised successfully' }; - }), -}); diff --git a/verifier/user-apis/apis/coupon.ts b/verifier/user-apis/apis/coupon.ts deleted file mode 100644 index 6eab804..0000000 --- a/verifier/user-apis/apis/coupon.ts +++ /dev/null @@ -1,296 +0,0 @@ -import { router, protectedProcedure } from '@/src/trpc/trpc-index'; -import { z } from 'zod'; -import { db } from '@/src/db/db_index'; -import { coupons, couponUsage, couponApplicableUsers, reservedCoupons, couponApplicableProducts } from '@/src/db/schema'; -import { eq, and, or, gt, isNull, sql } from 'drizzle-orm'; -import { ApiError } from '@/src/lib/api-error'; - -import { users } from '@/src/db/schema'; - -type CouponWithRelations = typeof coupons.$inferSelect & { - applicableUsers: (typeof couponApplicableUsers.$inferSelect & { user: typeof users.$inferSelect })[]; - usages: typeof couponUsage.$inferSelect[]; -}; - -export interface EligibleCoupon { - id: number; - code: string; - discountType: 'percentage' | 'flat'; - discountValue: number; - maxValue?: number; - minOrder?: number; - description: string; - exclusiveApply?: boolean; - isEligible: boolean; - ineligibilityReason?: string; -} - -const generateCouponDescription = (coupon: any): string => { - let desc = ''; - - if (coupon.discountPercent) { - desc += `${coupon.discountPercent}% off`; - } else if (coupon.flatDiscount) { - desc += `₹${coupon.flatDiscount} off`; - } - - if (coupon.minOrder) { - desc += ` on orders above ₹${coupon.minOrder}`; - } - - if (coupon.maxValue) { - desc += ` (max discount ₹${coupon.maxValue})`; - } - - return desc; -}; - -export interface CouponDisplay { - id: number; - code: string; - discountType: 'percentage' | 'flat'; - discountValue: number; - maxValue?: number; - minOrder?: number; - description: string; - validTill?: Date; - usageCount: number; - maxLimitForUser?: number; - isExpired: boolean; - isUsedUp: boolean; -} - -export const userCouponRouter = router({ - getEligible: protectedProcedure - .query(async ({ ctx }) => { - try { - - const userId = ctx.user.userId; - - // Get all active, non-expired coupons - 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: { - with: { - user: true - } - }, - applicableProducts: { - with: { - product: true - } - }, - } - }); - - // Filter to only coupons applicable to current user - const applicableCoupons = allCoupons.filter(coupon => { - if(!coupon.isUserBased) return true; - const applicableUsers = coupon.applicableUsers || []; - return applicableUsers.some(au => au.userId === userId); - }); - - return { success: true, data: applicableCoupons }; - } - catch(e) { - console.log(e) - throw new ApiError("Unable to get coupons") - } - }), - - getProductCoupons: protectedProcedure - .input(z.object({ productId: z.number().int().positive() })) - .query(async ({ input, ctx }) => { - const userId = ctx.user.userId; - const { productId } = input; - - // Get all active, non-expired coupons - 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: { - with: { - user: true - } - }, - applicableProducts: { - with: { - product: true - } - }, - } - }); - - // Filter to only coupons applicable to current user and product - const applicableCoupons = allCoupons.filter(coupon => { - const applicableUsers = coupon.applicableUsers || []; - const userApplicable = !coupon.isUserBased || applicableUsers.some(au => au.userId === userId); - - const applicableProducts = coupon.applicableProducts || []; - const productApplicable = applicableProducts.length === 0 || applicableProducts.some(ap => ap.productId === productId); - - return userApplicable && productApplicable; - }); - - return { success: true, data: applicableCoupons }; - }), - - getMyCoupons: protectedProcedure - .query(async ({ ctx }) => { - const userId = ctx.user.userId; - - // Get all coupons - const allCoupons = await db.query.coupons.findMany({ - with: { - usages: { - where: eq(couponUsage.userId, userId) - }, - applicableUsers: { - with: { - user: true - } - } - } - }); - - // Filter coupons in JS: not invalidated, applicable to user, and not expired - const applicableCoupons = (allCoupons as CouponWithRelations[]).filter(coupon => { - const isNotInvalidated = !coupon.isInvalidated; - const applicableUsers = coupon.applicableUsers || []; - const isApplicable = coupon.isApplyForAll || applicableUsers.some(au => au.userId === userId); - const isNotExpired = !coupon.validTill || new Date(coupon.validTill) > new Date(); - return isNotInvalidated && isApplicable && isNotExpired; - }); - - // Categorize coupons - const personalCoupons: CouponDisplay[] = []; - const generalCoupons: CouponDisplay[] = []; - - applicableCoupons.forEach(coupon => { - const usageCount = coupon.usages.length; - const isExpired = false; // Already filtered out expired coupons - const isUsedUp = Boolean(coupon.maxLimitForUser && usageCount >= coupon.maxLimitForUser); - - const couponDisplay: CouponDisplay = { - id: coupon.id, - code: coupon.couponCode, - discountType: coupon.discountPercent ? 'percentage' : 'flat', - discountValue: parseFloat(coupon.discountPercent || coupon.flatDiscount || '0'), - maxValue: coupon.maxValue ? parseFloat(coupon.maxValue) : undefined, - minOrder: coupon.minOrder ? parseFloat(coupon.minOrder) : undefined, - description: generateCouponDescription(coupon), - validTill: coupon.validTill ? new Date(coupon.validTill) : undefined, - usageCount, - maxLimitForUser: coupon.maxLimitForUser ? parseInt(coupon.maxLimitForUser.toString()) : undefined, - isExpired, - isUsedUp, - }; - - if ((coupon.applicableUsers || []).some(au => au.userId === userId) && !coupon.isApplyForAll) { - // Personal coupon - personalCoupons.push(couponDisplay); - } else if (coupon.isApplyForAll) { - // General coupon - generalCoupons.push(couponDisplay); - } - }); - - return { - success: true, - data: { - personal: personalCoupons, - general: generalCoupons, - } - }; - }), - - redeemReservedCoupon: protectedProcedure - .input(z.object({ secretCode: z.string() })) - .mutation(async ({ input, ctx }) => { - const userId = ctx.user.userId; - const { secretCode } = input; - - // Find the reserved coupon - const reservedCoupon = await db.query.reservedCoupons.findFirst({ - where: and( - eq(reservedCoupons.secretCode, secretCode.toUpperCase()), - eq(reservedCoupons.isRedeemed, false) - ), - }); - - if (!reservedCoupon) { - throw new ApiError("Invalid or already redeemed coupon code", 400); - } - - // Check if already redeemed by this user (in case of multiple attempts) - if (reservedCoupon.redeemedBy === userId) { - throw new ApiError("You have already redeemed this coupon", 400); - } - - // Create the coupon in the main table - const couponResult = await db.transaction(async (tx) => { - // Insert into coupons - const couponInsert = 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(); - - const coupon = couponInsert[0]; - - // Insert into couponApplicableUsers - await tx.insert(couponApplicableUsers).values({ - couponId: coupon.id, - userId, - }); - - // Copy applicable products - if (reservedCoupon.productIds && Array.isArray(reservedCoupon.productIds) && reservedCoupon.productIds.length > 0) { - // Assuming productIds are the IDs, but wait, in schema, productIds is jsonb, but in relations, couponApplicableProducts has productId - // For simplicity, since reservedCoupons has productIds as jsonb, but to match, perhaps insert into couponApplicableProducts if needed - // But in createReservedCoupon, I inserted applicableProducts into couponApplicableProducts - // So for reserved, perhaps do the same, but since it's jsonb, maybe not. - // For now, skip, as the coupon will have productIds in coupons table. - } - - // Update reserved coupon as redeemed - await tx.update(reservedCoupons).set({ - isRedeemed: true, - redeemedBy: userId, - redeemedAt: new Date(), - }).where(eq(reservedCoupons.id, reservedCoupon.id)); - - return coupon; - }); - - return { success: true, coupon: couponResult }; - }), -}); diff --git a/verifier/user-apis/apis/file-upload.ts b/verifier/user-apis/apis/file-upload.ts deleted file mode 100644 index bacf1f7..0000000 --- a/verifier/user-apis/apis/file-upload.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { router, protectedProcedure } from '@/src/trpc/trpc-index'; -import { z } from 'zod'; -import { generateUploadUrl } from '@/src/lib/s3-client'; -import { ApiError } from '@/src/lib/api-error'; - -export const fileUploadRouter = router({ - generateUploadUrls: protectedProcedure - .input(z.object({ - contextString: z.enum(['review', 'product_info', 'notification']), - mimeTypes: z.array(z.string()), - })) - .mutation(async ({ input }): Promise<{ uploadUrls: string[] }> => { - const { contextString, mimeTypes } = input; - - const uploadUrls: string[] = []; - const keys: string[] = []; - - for (const mimeType of mimeTypes) { - // Generate key based on context and mime type - let folder: string; - if (contextString === 'review') { - folder = 'review-images'; - } else if(contextString === 'product_info') { - folder = 'product-images'; - } - // else if(contextString === 'review_response') { - // folder = 'review-response-images' - // } - else if(contextString === 'notification') { - folder = 'notification-images' - } else { - folder = ''; - } - - const extension = mimeType === 'image/jpeg' ? '.jpg' : - mimeType === 'image/png' ? '.png' : - mimeType === 'image/gif' ? '.gif' : '.jpg'; - const key = `${folder}/${Date.now()}${extension}`; - - try { - const uploadUrl = await generateUploadUrl(key, mimeType); - uploadUrls.push(uploadUrl); - keys.push(key); - - } catch (error) { - console.error('Error generating upload URL:', error); - throw new ApiError('Failed to generate upload URL', 500); - } - } - - return { uploadUrls }; - }), -}); - -export type FileUploadRouter = typeof fileUploadRouter; diff --git a/verifier/user-apis/apis/order.ts b/verifier/user-apis/apis/order.ts deleted file mode 100644 index 08ebabf..0000000 --- a/verifier/user-apis/apis/order.ts +++ /dev/null @@ -1,979 +0,0 @@ -import { router, protectedProcedure } from "@/src/trpc/trpc-index"; -import { z } from "zod"; -import { db } from "@/src/db/db_index"; -import { - orders, - orderItems, - orderStatus, - addresses, - productInfo, - paymentInfoTable, - coupons, - couponUsage, - cartItems, - refunds, - units, - userDetails, -} from "@/src/db/schema"; -import { eq, and, inArray, desc, gte, lte } from "drizzle-orm"; -import { scaffoldAssetUrl } from "@/src/lib/s3-client"; -import { ApiError } from "@/src/lib/api-error"; -import { - sendOrderPlacedNotification, - sendOrderCancelledNotification, -} from "@/src/lib/notif-job"; -import { getNextDeliveryDate } from "@/src/trpc/apis/common-apis/common"; -import { CONST_KEYS, getConstant, getConstants } from "@/src/lib/const-store"; -import { publishFormattedOrder, publishCancellation } from "@/src/lib/post-order-handler"; -import { getSlotById } from "@/src/stores/slot-store"; - - -const validateAndGetCoupon = async ( - couponId: number | undefined, - userId: number, - totalAmount: number -) => { - if (!couponId) return null; - - const coupon = await db.query.coupons.findFirst({ - where: eq(coupons.id, couponId), - with: { - usages: { where: eq(couponUsage.userId, userId) }, - }, - }); - - if (!coupon) throw new ApiError("Invalid coupon", 400); - if (coupon.isInvalidated) - throw new ApiError("Coupon is no longer valid", 400); - if (coupon.validTill && new Date(coupon.validTill) < new Date()) - throw new ApiError("Coupon has expired", 400); - if ( - coupon.maxLimitForUser && - coupon.usages.length >= coupon.maxLimitForUser - ) - throw new ApiError("Coupon usage limit exceeded", 400); - if ( - coupon.minOrder && - parseFloat(coupon.minOrder.toString()) > totalAmount - ) - throw new ApiError( - "Order amount does not meet coupon minimum requirement", - 400 - ); - - return coupon; -}; - -const applyDiscountToOrder = ( - orderTotal: number, - appliedCoupon: typeof coupons.$inferSelect | null, - proportion: number -) => { - let finalOrderTotal = orderTotal; - // const proportion = totalAmount / orderTotal; - if (appliedCoupon) { - if (appliedCoupon.discountPercent) { - const discount = Math.min( - (orderTotal * - parseFloat(appliedCoupon.discountPercent.toString())) / - 100, - appliedCoupon.maxValue - ? parseFloat(appliedCoupon.maxValue.toString()) * proportion - : Infinity - ); - finalOrderTotal -= discount; - } else if (appliedCoupon.flatDiscount) { - const discount = Math.min( - parseFloat(appliedCoupon.flatDiscount.toString()) * proportion, - appliedCoupon.maxValue - ? parseFloat(appliedCoupon.maxValue.toString()) * proportion - : finalOrderTotal - ); - finalOrderTotal -= discount; - } - } - - // let orderDeliveryCharge = 0; - // if (isFirstOrder && finalOrderTotal < minOrderValue) { - // orderDeliveryCharge = deliveryCharge; - // finalOrderTotal += deliveryCharge; - // } - - - return { finalOrderTotal, orderGroupProportion: proportion }; -}; - -const placeOrderUtil = async (params: { - userId: number; - selectedItems: Array<{ - productId: number; - quantity: number; - slotId: number | null; - }>; - addressId: number; - paymentMethod: "online" | "cod"; - couponId?: number; - userNotes?: string; - isFlash?: boolean; -}) => { - const { - userId, - selectedItems, - addressId, - paymentMethod, - couponId, - userNotes, - } = params; - - const constants = await getConstants([ - CONST_KEYS.minRegularOrderValue, - CONST_KEYS.deliveryCharge, - CONST_KEYS.flashFreeDeliveryThreshold, - CONST_KEYS.flashDeliveryCharge, - ]); - - const isFlashDelivery = params.isFlash; - const minOrderValue = (isFlashDelivery ? constants[CONST_KEYS.flashFreeDeliveryThreshold] : constants[CONST_KEYS.minRegularOrderValue]) || 0; - const deliveryCharge = (isFlashDelivery ? constants[CONST_KEYS.flashDeliveryCharge] : constants[CONST_KEYS.deliveryCharge]) || 0; - - const orderGroupId = `${Date.now()}-${userId}`; - - const address = await db.query.addresses.findFirst({ - where: and(eq(addresses.userId, userId), eq(addresses.id, addressId)), - }); - if (!address) { - throw new ApiError("Invalid address", 400); - } - - const ordersBySlot = new Map< - number | null, - Array<{ - productId: number; - quantity: number; - slotId: number | null; - product: any; - }> - >(); - - for (const item of selectedItems) { - const product = await db.query.productInfo.findFirst({ - where: eq(productInfo.id, item.productId), - }); - if (!product) { - throw new ApiError(`Product ${item.productId} not found`, 400); - } - - if (!ordersBySlot.has(item.slotId)) { - ordersBySlot.set(item.slotId, []); - } - ordersBySlot.get(item.slotId)!.push({ ...item, product }); - } - - if (params.isFlash) { - for (const item of selectedItems) { - const product = await db.query.productInfo.findFirst({ - where: eq(productInfo.id, item.productId), - }); - if (!product?.isFlashAvailable) { - throw new ApiError(`Product ${item.productId} is not available for flash delivery`, 400); - } - } - } - - let totalAmount = 0; - for (const [slotId, items] of ordersBySlot) { - const orderTotal = items.reduce( - (sum, item) => { - const itemPrice = params.isFlash - ? parseFloat((item.product.flashPrice || item.product.price).toString()) - : parseFloat(item.product.price.toString()); - return sum + itemPrice * item.quantity; - }, - 0 - ); - totalAmount += orderTotal; - } - - const appliedCoupon = await validateAndGetCoupon(couponId, userId, totalAmount); - - const expectedDeliveryCharge = - totalAmount < minOrderValue ? deliveryCharge : 0; - - const totalWithDelivery = totalAmount + expectedDeliveryCharge; - - type OrderData = { - order: Omit; - orderItems: Omit[]; - orderStatus: Omit; - }; - - const ordersData: OrderData[] = []; - let isFirstOrder = true; - - for (const [slotId, items] of ordersBySlot) { - const subOrderTotal = items.reduce( - (sum, item) => { - const itemPrice = params.isFlash - ? parseFloat((item.product.flashPrice || item.product.price).toString()) - : parseFloat(item.product.price.toString()); - return sum + itemPrice * item.quantity; - }, - 0 - ); - const subOrderTotalWithDelivery = subOrderTotal + expectedDeliveryCharge; - - const orderGroupProportion = subOrderTotal / totalAmount; - const orderTotalAmount = isFirstOrder ? subOrderTotalWithDelivery : subOrderTotal; - - const { finalOrderTotal: finalOrderAmount } = applyDiscountToOrder( - orderTotalAmount, - appliedCoupon, - orderGroupProportion - ); - - const order: Omit = { - userId, - addressId, - slotId: params.isFlash ? null : slotId, - isCod: paymentMethod === "cod", - isOnlinePayment: paymentMethod === "online", - paymentInfoId: null, - totalAmount: finalOrderAmount.toString(), - deliveryCharge: isFirstOrder ? expectedDeliveryCharge.toString() : "0", - readableId: -1, - userNotes: userNotes || null, - orderGroupId, - orderGroupProportion: orderGroupProportion.toString(), - isFlashDelivery: params.isFlash, - }; - - const orderItemsData: Omit[] = items.map( - (item) => ({ - orderId: 0, - productId: item.productId, - quantity: item.quantity.toString(), - price: params.isFlash - ? item.product.flashPrice || item.product.price - : item.product.price, - discountedPrice: ( - params.isFlash - ? item.product.flashPrice || item.product.price - : item.product.price - ).toString(), - }) - ); - - const orderStatusData: Omit = { - userId, - orderId: 0, - paymentStatus: paymentMethod === "cod" ? "cod" : "pending", - }; - - ordersData.push({ order, orderItems: orderItemsData, orderStatus: orderStatusData }); - isFirstOrder = false; - } - - const createdOrders = await db.transaction(async (tx) => { - let sharedPaymentInfoId: number | null = null; - if (paymentMethod === "online") { - const [paymentInfo] = await tx - .insert(paymentInfoTable) - .values({ - status: "pending", - gateway: "razorpay", - merchantOrderId: `multi_order_${Date.now()}`, - }) - .returning(); - sharedPaymentInfoId = paymentInfo.id; - } - - const ordersToInsert: Omit[] = ordersData.map( - (od) => ({ - ...od.order, - paymentInfoId: sharedPaymentInfoId, - }) - ); - - const insertedOrders = await tx.insert(orders).values(ordersToInsert).returning(); - - const allOrderItems: Omit[] = []; - const allOrderStatuses: Omit[] = []; - - insertedOrders.forEach((order, index) => { - const od = ordersData[index]; - od.orderItems.forEach((item) => { - allOrderItems.push({ ...item, orderId: order.id as number }); - }); - allOrderStatuses.push({ - ...od.orderStatus, - orderId: order.id as number, - }); - }); - - await tx.insert(orderItems).values(allOrderItems); - await tx.insert(orderStatus).values(allOrderStatuses); - - if (paymentMethod === "online" && sharedPaymentInfoId) { - - } - - return insertedOrders; - }); - - await db.delete(cartItems).where( - and( - eq(cartItems.userId, userId), - inArray( - cartItems.productId, - selectedItems.map((item) => item.productId) - ) - ) - ); - - if (appliedCoupon && createdOrders.length > 0) { - await db.insert(couponUsage).values({ - userId, - couponId: appliedCoupon.id, - orderId: createdOrders[0].id as number, - orderItemId: null, - usedAt: new Date(), - }); - } - - for (const order of createdOrders) { - sendOrderPlacedNotification(userId, order.id.toString()); - } - - await publishFormattedOrder(createdOrders, ordersBySlot); - - return { success: true, data: createdOrders }; -}; - -export const orderRouter = router({ - placeOrder: protectedProcedure - .input( - z.object({ - selectedItems: z.array( - z.object({ - productId: z.number().int().positive(), - quantity: z.number().int().positive(), - slotId: z.union([z.number().int(), z.null()]), - }) - ), - addressId: z.number().int().positive(), - paymentMethod: z.enum(["online", "cod"]), - couponId: z.number().int().positive().optional(), - userNotes: z.string().optional(), - isFlashDelivery: z.boolean().optional().default(false), - }) - ) - .mutation(async ({ input, ctx }) => { - const userId = ctx.user.userId; - - // Check if user is suspended from placing orders - const userDetail = await db.query.userDetails.findFirst({ - where: eq(userDetails.userId, userId), - }); - - if (userDetail?.isSuspended) { - throw new ApiError("Unable to place order", 403); - } - - const { - selectedItems, - addressId, - paymentMethod, - couponId, - userNotes, - isFlashDelivery, - } = input; - - // Check if flash delivery is enabled when placing a flash delivery order - if (isFlashDelivery) { - const isFlashDeliveryEnabled = await getConstant(CONST_KEYS.isFlashDeliveryEnabled); - if (!isFlashDeliveryEnabled) { - throw new ApiError("Flash delivery is currently unavailable. Please opt for scheduled delivery.", 403); - } - } - - // Check if any selected slot is at full capacity (only for regular delivery) - if (!isFlashDelivery) { - const slotIds = [...new Set(selectedItems.filter(i => i.slotId !== null).map(i => i.slotId as number))]; - for (const slotId of slotIds) { - const slot = await getSlotById(slotId); - if (slot?.isCapacityFull) { - throw new ApiError("Selected delivery slot is at full capacity. Please choose another slot.", 403); - } - } - } - - let processedItems = selectedItems; - - // Handle flash delivery slot resolution - if (isFlashDelivery) { - // For flash delivery, set slotId to null (no specific slot assigned) - processedItems = selectedItems.map(item => ({ - ...item, - slotId: null as any, // Type override for flash delivery - })); - } - - return await placeOrderUtil({ - userId, - selectedItems: processedItems, - addressId, - paymentMethod, - couponId, - userNotes, - isFlash: isFlashDelivery, - }); - }), - - getOrders: protectedProcedure - .input( - z - .object({ - page: z.number().min(1).default(1), - pageSize: z.number().min(1).max(50).default(10), - }) - .optional() - ) - .query(async ({ input, ctx }) => { - const { page = 1, pageSize = 10 } = input || {}; - const userId = ctx.user.userId; - const offset = (page - 1) * pageSize; - - // Get total count for pagination - const totalCountResult = await db.$count( - orders, - eq(orders.userId, userId) - ); - const totalCount = totalCountResult; - - const userOrders = await db.query.orders.findMany({ - where: eq(orders.userId, userId), - with: { - orderItems: { - with: { - product: true, - }, - }, - slot: true, - paymentInfo: true, - orderStatus: true, - refunds: true, - }, - orderBy: (orders, { desc }) => [desc(orders.createdAt)], - limit: pageSize, - offset: offset, - }); - - const mappedOrders = await Promise.all( - userOrders.map(async (order) => { - const status = order.orderStatus[0]; - const refund = order.refunds[0]; - - type DeliveryStatus = "cancelled" | "success" | "pending" | "packaged"; - type OrderStatus = "cancelled" | "success"; - - let deliveryStatus: DeliveryStatus; - let orderStatus: OrderStatus; - - const allItemsPackaged = order.orderItems.every( - (item) => item.is_packaged - ); - - if (status?.isCancelled) { - deliveryStatus = "cancelled"; - orderStatus = "cancelled"; - } else if (status?.isDelivered) { - deliveryStatus = "success"; - orderStatus = "success"; - } else if (allItemsPackaged) { - deliveryStatus = "packaged"; - orderStatus = "success"; - } else { - deliveryStatus = "pending"; - orderStatus = "success"; - } - - const paymentMode = order.isCod ? "CoD" : "Online"; - const paymentStatus = status?.paymentStatus || "pending"; - const refundStatus = refund?.refundStatus || "none"; - const refundAmount = refund?.refundAmount - ? parseFloat(refund.refundAmount.toString()) - : null; - - const items = await Promise.all( - order.orderItems.map(async (item) => { - - const signedImages = item.product.images - ? scaffoldAssetUrl( - item.product.images as string[] - ) - : []; - return { - productName: item.product.name, - quantity: parseFloat(item.quantity), - price: parseFloat(item.price.toString()), - discountedPrice: parseFloat( - item.discountedPrice?.toString() || item.price.toString() - ), - amount: - parseFloat(item.price.toString()) * parseFloat(item.quantity), - image: signedImages[0] || null, - }; - }) - ); - - return { - id: order.id, - orderId: `ORD${order.id}`, - orderDate: order.createdAt.toISOString(), - deliveryStatus, - deliveryDate: order.slot?.deliveryTime.toISOString(), - orderStatus, - cancelReason: status?.cancelReason || null, - paymentMode, - totalAmount: Number(order.totalAmount), - deliveryCharge: Number(order.deliveryCharge), - paymentStatus, - refundStatus, - refundAmount, - userNotes: order.userNotes || null, - items, - isFlashDelivery: order.isFlashDelivery, - createdAt: order.createdAt.toISOString(), - }; - }) - ); - - return { - success: true, - data: mappedOrders, - pagination: { - page, - pageSize, - totalCount, - totalPages: Math.ceil(totalCount / pageSize), - }, - }; - }), - - getOrderById: protectedProcedure - .input(z.object({ orderId: z.string() })) - .query(async ({ input, ctx }) => { - const { orderId } = input; - const userId = ctx.user.userId; - - const order = await db.query.orders.findFirst({ - where: and(eq(orders.id, parseInt(orderId)), eq(orders.userId, userId)), - with: { - orderItems: { - with: { - product: true, - }, - }, - slot: true, - paymentInfo: true, - orderStatus: { - with: { - refundCoupon: true, - }, - }, - refunds: true, - }, - }); - - if (!order) { - throw new Error("Order not found"); - } - - // Get coupon usage for this specific order using new orderId field - const couponUsageData = await db.query.couponUsage.findMany({ - where: eq(couponUsage.orderId, order.id), // Use new orderId field - with: { - coupon: true, - }, - }); - - let couponData = null; - if (couponUsageData.length > 0) { - // Calculate total discount from multiple coupons - let totalDiscountAmount = 0; - const orderTotal = parseFloat(order.totalAmount.toString()); - - for (const usage of couponUsageData) { - let discountAmount = 0; - - if (usage.coupon.discountPercent) { - discountAmount = - (orderTotal * - parseFloat(usage.coupon.discountPercent.toString())) / - 100; - } else if (usage.coupon.flatDiscount) { - discountAmount = parseFloat(usage.coupon.flatDiscount.toString()); - } - - // Apply max value limit if set - if ( - usage.coupon.maxValue && - discountAmount > parseFloat(usage.coupon.maxValue.toString()) - ) { - discountAmount = parseFloat(usage.coupon.maxValue.toString()); - } - - totalDiscountAmount += discountAmount; - } - - couponData = { - couponCode: couponUsageData - .map((u) => u.coupon.couponCode) - .join(", "), - couponDescription: `${couponUsageData.length} coupons applied`, - discountAmount: totalDiscountAmount, - }; - } - - const status = order.orderStatus[0]; - const refund = order.refunds[0]; - - type DeliveryStatus = "cancelled" | "success" | "pending" | "packaged"; - type OrderStatus = "cancelled" | "success"; - - let deliveryStatus: DeliveryStatus; - let orderStatus: OrderStatus; - - const allItemsPackaged = order.orderItems.every( - (item) => item.is_packaged - ); - - if (status?.isCancelled) { - deliveryStatus = "cancelled"; - orderStatus = "cancelled"; - } else if (status?.isDelivered) { - deliveryStatus = "success"; - orderStatus = "success"; - } else if (allItemsPackaged) { - deliveryStatus = "packaged"; - orderStatus = "success"; - } else { - deliveryStatus = "pending"; - orderStatus = "success"; - } - - const paymentMode = order.isCod ? "CoD" : "Online"; - const paymentStatus = status?.paymentStatus || "pending"; - const refundStatus = refund?.refundStatus || "none"; - const refundAmount = refund?.refundAmount - ? parseFloat(refund.refundAmount.toString()) - : null; - - const items = await Promise.all( - order.orderItems.map(async (item) => { - const signedImages = item.product.images - ? scaffoldAssetUrl( - item.product.images as string[] - ) - : []; - return { - productName: item.product.name, - quantity: parseFloat(item.quantity), - price: parseFloat(item.price.toString()), - discountedPrice: parseFloat( - item.discountedPrice?.toString() || item.price.toString() - ), - amount: - parseFloat(item.price.toString()) * parseFloat(item.quantity), - image: signedImages[0] || null, - }; - }) - ); - - return { - id: order.id, - orderId: `ORD${order.id}`, - orderDate: order.createdAt.toISOString(), - deliveryStatus, - deliveryDate: order.slot?.deliveryTime.toISOString(), - orderStatus: order.orderStatus, - cancellationStatus: orderStatus, - cancelReason: status?.cancelReason || null, - paymentMode, - paymentStatus, - refundStatus, - refundAmount, - userNotes: order.userNotes || null, - items, - couponCode: couponData?.couponCode || null, - couponDescription: couponData?.couponDescription || null, - discountAmount: couponData?.discountAmount || null, - orderAmount: parseFloat(order.totalAmount.toString()), - isFlashDelivery: order.isFlashDelivery, - createdAt: order.createdAt.toISOString(), - }; - }), - - cancelOrder: protectedProcedure - .input( - z.object({ - // id: z.string().regex(/^ORD\d+$/, "Invalid order ID format"), - id: z.number(), - reason: z.string().min(1, "Cancellation reason is required"), - }) - ) - .mutation(async ({ input, ctx }) => { - try { - const userId = ctx.user.userId; - const { id, reason } = input; - - // Check if order exists and belongs to user - const order = await db.query.orders.findFirst({ - where: eq(orders.id, Number(id)), - with: { - orderStatus: true, - }, - }); - - if (!order) { - console.error("Order not found:", id); - throw new ApiError("Order not found", 404); - } - - if (order.userId !== userId) { - console.error("Order does not belong to user:", { - orderId: id, - orderUserId: order.userId, - requestUserId: userId, - }); - - throw new ApiError("Order not found", 404); - } - - const status = order.orderStatus[0]; - if (!status) { - console.error("Order status not found for order:", id); - throw new ApiError("Order status not found", 400); - } - - if (status.isCancelled) { - console.error("Order is already cancelled:", id); - throw new ApiError("Order is already cancelled", 400); - } - - if (status.isDelivered) { - console.error("Cannot cancel delivered order:", id); - throw new ApiError("Cannot cancel delivered order", 400); - } - - // Perform database operations in transaction - const result = await db.transaction(async (tx) => { - // Update order status - await tx - .update(orderStatus) - .set({ - isCancelled: true, - cancelReason: reason, - cancellationUserNotes: reason, - cancellationReviewed: false, - }) - .where(eq(orderStatus.id, status.id)); - - // Determine refund status based on payment method - const refundStatus = order.isCod ? "na" : "pending"; - - // Insert refund record - await tx.insert(refunds).values({ - orderId: order.id, - refundStatus, - }); - - return { orderId: order.id, userId }; - }); - - // Send notification outside transaction (idempotent operation) - await sendOrderCancelledNotification( - result.userId, - result.orderId.toString() - ); - - // Publish to Redis for Telegram notification - await publishCancellation(result.orderId, 'user', reason); - - return { success: true, message: "Order cancelled successfully" }; - } catch (e) { - console.log(e); - throw new ApiError("failed to cancel order"); - } - }), - - updateUserNotes: protectedProcedure - .input( - z.object({ - id: z.number(), - userNotes: z.string(), - }) - ) - .mutation(async ({ input, ctx }) => { - const userId = ctx.user.userId; - const { id, userNotes } = input; - - // Extract readable ID from orderId (e.g., ORD001 -> 1) - // const readableIdMatch = id.match(/^ORD(\d+)$/); - // if (!readableIdMatch) { - // console.error("Invalid order ID format:", id); - // throw new ApiError("Invalid order ID format", 400); - // } - // const readableId = parseInt(readableIdMatch[1]); - - // Check if order exists and belongs to user - const order = await db.query.orders.findFirst({ - where: eq(orders.id, Number(id)), - with: { - orderStatus: true, - }, - }); - - if (!order) { - console.error("Order not found:", id); - throw new ApiError("Order not found", 404); - } - - if (order.userId !== userId) { - console.error("Order does not belong to user:", { - orderId: id, - orderUserId: order.userId, - requestUserId: userId, - }); - throw new ApiError("Order not found", 404); - } - - const status = order.orderStatus[0]; - if (!status) { - console.error("Order status not found for order:", id); - throw new ApiError("Order status not found", 400); - } - - // Only allow updating notes for orders that are not delivered or cancelled - if (status.isDelivered) { - console.error("Cannot update notes for delivered order:", id); - throw new ApiError("Cannot update notes for delivered order", 400); - } - - if (status.isCancelled) { - console.error("Cannot update notes for cancelled order:", id); - throw new ApiError("Cannot update notes for cancelled order", 400); - } - - // Update user notes - await db - .update(orders) - .set({ - userNotes: userNotes || null, - }) - .where(eq(orders.id, order.id)); - - return { success: true, message: "Notes updated successfully" }; - }), - - getRecentlyOrderedProducts: protectedProcedure - .input( - z - .object({ - limit: z.number().min(1).max(50).default(20), - }) - .optional() - ) - .query(async ({ input, ctx }) => { - const { limit = 20 } = input || {}; - const userId = ctx.user.userId; - - // Get user's recent delivered orders (last 30 days) - const thirtyDaysAgo = new Date(); - thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); - - const recentOrders = await db - .select({ id: orders.id }) - .from(orders) - .innerJoin(orderStatus, eq(orders.id, orderStatus.orderId)) - .where( - and( - eq(orders.userId, userId), - eq(orderStatus.isDelivered, true), - gte(orders.createdAt, thirtyDaysAgo) - ) - ) - .orderBy(desc(orders.createdAt)) - .limit(10); // Get last 10 orders - - if (recentOrders.length === 0) { - return { success: true, products: [] }; - } - - const orderIds = recentOrders.map((order) => order.id); - - // Get unique product IDs from recent orders - const orderItemsResult = await db - .select({ productId: orderItems.productId }) - .from(orderItems) - .where(inArray(orderItems.orderId, orderIds)); - - const productIds = [ - ...new Set(orderItemsResult.map((item) => item.productId)), - ]; - - if (productIds.length === 0) { - return { success: true, products: [] }; - } - - // Get product details - const productsWithUnits = await db - .select({ - id: productInfo.id, - name: productInfo.name, - shortDescription: productInfo.shortDescription, - price: productInfo.price, - images: productInfo.images, - isOutOfStock: productInfo.isOutOfStock, - unitShortNotation: units.shortNotation, - incrementStep: productInfo.incrementStep, - }) - .from(productInfo) - .innerJoin(units, eq(productInfo.unitId, units.id)) - .where( - and( - inArray(productInfo.id, productIds), - eq(productInfo.isSuspended, false) - ) - ) - .orderBy(desc(productInfo.createdAt)) - .limit(limit); - - // Generate signed URLs for product images - const formattedProducts = await Promise.all( - productsWithUnits.map(async (product) => { - const nextDeliveryDate = await getNextDeliveryDate(product.id); - return { - id: product.id, - name: product.name, - shortDescription: product.shortDescription, - price: product.price, - unit: product.unitShortNotation, - incrementStep: product.incrementStep, - isOutOfStock: product.isOutOfStock, - nextDeliveryDate: nextDeliveryDate - ? nextDeliveryDate.toISOString() - : null, - images: scaffoldAssetUrl( - (product.images as string[]) || [] - ), - }; - }) - ); - - return { - success: true, - products: formattedProducts, - }; - }), -}); diff --git a/verifier/user-apis/apis/product.ts b/verifier/user-apis/apis/product.ts deleted file mode 100644 index 0fc86ee..0000000 --- a/verifier/user-apis/apis/product.ts +++ /dev/null @@ -1,266 +0,0 @@ -import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index'; -import { z } from 'zod'; -import { db } from '@/src/db/db_index'; -import { productInfo, units, productSlots, deliverySlotInfo, specialDeals, storeInfo, productTagInfo, productTags, productReviews, users } from '@/src/db/schema'; -import { claimUploadUrl, extractKeyFromPresignedUrl, scaffoldAssetUrl } from '@/src/lib/s3-client'; -import { ApiError } from '@/src/lib/api-error'; -import { eq, and, gt, sql, inArray, desc } from 'drizzle-orm'; -import { getProductById as getProductByIdFromCache, getAllProducts as getAllProductsFromCache } from '@/src/stores/product-store'; -import dayjs from 'dayjs'; - -// Uniform Product Type -interface Product { - id: number; - name: string; - shortDescription: string | null; - longDescription: string | null; - price: string; - marketPrice: string | null; - unitNotation: string; - images: string[]; - isOutOfStock: boolean; - store: { id: number; name: string; description: string | null } | null; - incrementStep: number; - productQuantity: number; - isFlashAvailable: boolean; - flashPrice: string | null; - deliverySlots: Array<{ id: number; deliveryTime: Date; freezeTime: Date }>; - specialDeals: Array<{ quantity: string; price: string; validTill: Date }>; -} - -export const productRouter = router({ - getProductDetails: publicProcedure - .input(z.object({ - id: z.string().regex(/^\d+$/, 'Invalid product ID'), - })) - .query(async ({ input }): Promise => { - const { id } = input; - const productId = parseInt(id); - - if (isNaN(productId)) { - throw new Error('Invalid product ID'); - } - - console.log('from the api to get product details') - -// First, try to get the product from Redis cache - const cachedProduct = await getProductByIdFromCache(productId); - - if (cachedProduct) { - // Filter delivery slots to only include those with future freeze times and not at full capacity - const currentTime = new Date(); - const filteredSlots = cachedProduct.deliverySlots.filter(slot => - dayjs(slot.freezeTime).isAfter(currentTime) && !slot.isCapacityFull - ); - - return { - ...cachedProduct, - deliverySlots: filteredSlots - }; - } - - // If not in cache, fetch from database (fallback) - const productData = await db - .select({ - id: productInfo.id, - name: productInfo.name, - shortDescription: productInfo.shortDescription, - longDescription: productInfo.longDescription, - price: productInfo.price, - marketPrice: productInfo.marketPrice, - images: productInfo.images, - isOutOfStock: productInfo.isOutOfStock, - storeId: productInfo.storeId, - unitShortNotation: units.shortNotation, - incrementStep: productInfo.incrementStep, - productQuantity: productInfo.productQuantity, - isFlashAvailable: productInfo.isFlashAvailable, - flashPrice: productInfo.flashPrice, - }) - .from(productInfo) - .innerJoin(units, eq(productInfo.unitId, units.id)) - .where(eq(productInfo.id, productId)) - .limit(1); - - if (productData.length === 0) { - throw new Error('Product not found'); - } - - const product = productData[0]; - - // Fetch store info for this product - const storeData = product.storeId ? await db.query.storeInfo.findFirst({ - where: eq(storeInfo.id, product.storeId), - columns: { id: true, name: true, description: true }, - }) : null; - - // Fetch delivery slots for this product - const deliverySlotsData = await db - .select({ - id: deliverySlotInfo.id, - deliveryTime: deliverySlotInfo.deliveryTime, - freezeTime: deliverySlotInfo.freezeTime, - }) - .from(productSlots) - .innerJoin(deliverySlotInfo, eq(productSlots.slotId, deliverySlotInfo.id)) - .where( - and( - eq(productSlots.productId, productId), - eq(deliverySlotInfo.isActive, true), - eq(deliverySlotInfo.isCapacityFull, false), - gt(deliverySlotInfo.deliveryTime, sql`NOW()`), - gt(deliverySlotInfo.freezeTime, sql`NOW()`) - ) - ) - .orderBy(deliverySlotInfo.deliveryTime); - - // Fetch special deals for this product - const specialDealsData = await db - .select({ - quantity: specialDeals.quantity, - price: specialDeals.price, - validTill: specialDeals.validTill, - }) - .from(specialDeals) - .where( - and( - eq(specialDeals.productId, productId), - gt(specialDeals.validTill, sql`NOW()`) - ) - ) - .orderBy(specialDeals.quantity); - - // Generate signed URLs for images - const signedImages = scaffoldAssetUrl((product.images as string[]) || []); - - const response: Product = { - id: product.id, - name: product.name, - shortDescription: product.shortDescription, - longDescription: product.longDescription, - price: product.price.toString(), - marketPrice: product.marketPrice?.toString() || null, - unitNotation: product.unitShortNotation, - images: signedImages, - isOutOfStock: product.isOutOfStock, - store: storeData ? { - id: storeData.id, - name: storeData.name, - description: storeData.description, - } : null, - incrementStep: product.incrementStep, - productQuantity: product.productQuantity, - isFlashAvailable: product.isFlashAvailable, - flashPrice: product.flashPrice?.toString() || null, - deliverySlots: deliverySlotsData, - specialDeals: specialDealsData.map(d => ({ quantity: d.quantity.toString(), price: d.price.toString(), validTill: d.validTill })), - }; - - return response; - }), - - getProductReviews: publicProcedure - .input(z.object({ - productId: z.number().int().positive(), - limit: z.number().int().min(1).max(50).optional().default(10), - offset: z.number().int().min(0).optional().default(0), - })) - .query(async ({ input }) => { - const { productId, limit, offset } = input; - - 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); - - // Generate signed URLs for images - const reviewsWithSignedUrls = await Promise.all( - reviews.map(async (review) => ({ - ...review, - signedImageUrls: scaffoldAssetUrl((review.imageUrls as string[]) || []), - })) - ); - - // Check if more reviews exist - const totalCountResult = await db - .select({ count: sql`count(*)` }) - .from(productReviews) - .where(eq(productReviews.productId, productId)); - - const totalCount = Number(totalCountResult[0].count); - const hasMore = offset + limit < totalCount; - - return { reviews: reviewsWithSignedUrls, hasMore }; - }), - - createReview: protectedProcedure - .input(z.object({ - productId: z.number().int().positive(), - reviewBody: z.string().min(1, 'Review body is required'), - ratings: z.number().int().min(1).max(5), - imageUrls: z.array(z.string()).optional().default([]), - uploadUrls: z.array(z.string()).optional().default([]), - })) - .mutation(async ({ input, ctx }) => { - const { productId, reviewBody, ratings, imageUrls, uploadUrls } = input; - const userId = ctx.user.userId; - - // Optional: Check if product exists - const product = await db.query.productInfo.findFirst({ - where: eq(productInfo.id, productId), - }); - if (!product) { - throw new ApiError('Product not found', 404); - } - - // Insert review - const [newReview] = await db.insert(productReviews).values({ - userId, - productId, - reviewBody, - ratings, - imageUrls: uploadUrls.map(item => extractKeyFromPresignedUrl(item)), - }).returning(); - - // Claim upload URLs - if (uploadUrls && uploadUrls.length > 0) { - try { - await Promise.all(uploadUrls.map(url => claimUploadUrl(url))); - } catch (error) { - console.error('Error claiming upload URLs:', error); - // Don't fail the review creation - } - } - - return { success: true, review: newReview }; - }), - - - getAllProductsSummary: publicProcedure - .query(async (): Promise => { - // Get all products from cache - const allCachedProducts = await getAllProductsFromCache(); - - // Transform the cached products to match the expected summary format - // (with empty deliverySlots and specialDeals arrays for summary view) - const transformedProducts = allCachedProducts.map(product => ({ - ...product, - deliverySlots: [], // Empty for summary view - specialDeals: [], // Empty for summary view - })); - - return transformedProducts; - }), - -}); diff --git a/verifier/user-apis/apis/slots.ts b/verifier/user-apis/apis/slots.ts deleted file mode 100644 index 92dd37f..0000000 --- a/verifier/user-apis/apis/slots.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { router, publicProcedure } from "@/src/trpc/trpc-index"; -import { z } from "zod"; -import { db } from "@/src/db/db_index"; -import { - deliverySlotInfo, - productSlots, - productInfo, - units, -} from "@/src/db/schema"; -import { eq, and } from "drizzle-orm"; -import { getAllSlots as getAllSlotsFromCache, getSlotById as getSlotByIdFromCache } from "@/src/stores/slot-store"; -import dayjs from 'dayjs'; - -// Helper method to get formatted slot data by ID -async function getSlotData(slotId: number) { - const slot = await getSlotByIdFromCache(slotId); - - if (!slot) { - return null; - } - - const currentTime = new Date(); - if (dayjs(slot.freezeTime).isBefore(currentTime)) { - return null; - } - - return { - deliveryTime: slot.deliveryTime, - freezeTime: slot.freezeTime, - slotId: slot.id, - products: slot.products.filter((product) => !product.isOutOfStock), - }; -} - -export async function scaffoldSlotsWithProducts() { - const allSlots = await getAllSlotsFromCache(); - const currentTime = new Date(); - const validSlots = allSlots - .filter((slot) => { - return dayjs(slot.freezeTime).isAfter(currentTime) && - dayjs(slot.deliveryTime).isAfter(currentTime) && - !slot.isCapacityFull; - }) - .sort((a, b) => dayjs(a.deliveryTime).valueOf() - dayjs(b.deliveryTime).valueOf()); - - // Fetch all products for availability info - const allProducts = await db - .select({ - id: productInfo.id, - name: productInfo.name, - isOutOfStock: productInfo.isOutOfStock, - isFlashAvailable: productInfo.isFlashAvailable, - }) - .from(productInfo) - .where(eq(productInfo.isSuspended, false)); - - const productAvailability = allProducts.map(product => ({ - id: product.id, - name: product.name, - isOutOfStock: product.isOutOfStock, - isFlashAvailable: product.isFlashAvailable, - })); - - return { - slots: validSlots, - productAvailability, - count: validSlots.length, - }; -} - -export const slotsRouter = router({ - getSlots: publicProcedure.query(async () => { - const slots = await db.query.deliverySlotInfo.findMany({ - where: eq(deliverySlotInfo.isActive, true), - }); - return { - slots, - count: slots.length, - }; - }), - - getSlotsWithProducts: publicProcedure.query(async () => { - const response = await scaffoldSlotsWithProducts(); - return response; - }), - - getSlotById: publicProcedure - .input(z.object({ slotId: z.number() })) - .query(async ({ input }) => { - return await getSlotData(input.slotId); - }), -}); diff --git a/verifier/user-apis/apis/stores.ts b/verifier/user-apis/apis/stores.ts deleted file mode 100644 index 9522b7a..0000000 --- a/verifier/user-apis/apis/stores.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { router, publicProcedure } from '@/src/trpc/trpc-index'; -import { z } from 'zod'; -import { db } from '@/src/db/db_index'; -import { storeInfo, productInfo, units } from '@/src/db/schema'; -import { eq, and, sql } from 'drizzle-orm'; -import { scaffoldAssetUrl } from '@/src/lib/s3-client'; -import { ApiError } from '@/src/lib/api-error'; -import { getTagsByStoreId } from '@/src/stores/product-tag-store'; - -export async function scaffoldStores() { - const storesData = await db - .select({ - id: storeInfo.id, - name: storeInfo.name, - description: storeInfo.description, - imageUrl: storeInfo.imageUrl, - productCount: sql`count(${productInfo.id})`.as('productCount'), - }) - .from(storeInfo) - .leftJoin( - productInfo, - and(eq(productInfo.storeId, storeInfo.id), eq(productInfo.isSuspended, false)) - ) - .groupBy(storeInfo.id); - - // Generate signed URLs for store images and fetch sample products - const storesWithDetails = await Promise.all( - storesData.map(async (store) => { - const signedImageUrl = store.imageUrl ? scaffoldAssetUrl(store.imageUrl) : null; - - // Fetch up to 3 products for this store - const sampleProducts = await db - .select({ - id: productInfo.id, - name: productInfo.name, - images: productInfo.images, - }) - .from(productInfo) - .where(and(eq(productInfo.storeId, store.id), eq(productInfo.isSuspended, false))) - .limit(3); - - // Generate signed URLs for product images - const productsWithSignedUrls = await Promise.all( - sampleProducts.map(async (product) => { - const images = product.images as string[]; - return { - 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 { - stores: storesWithDetails, - }; -} - -export async function scaffoldStoreWithProducts(storeId: number) { - // Fetch store info - const storeData = await db.query.storeInfo.findFirst({ - where: eq(storeInfo.id, storeId), - columns: { - id: true, - name: true, - description: true, - imageUrl: true, - }, - }); - - if (!storeData) { - throw new ApiError('Store not found', 404); - } - - // Generate signed URL for store image - const signedImageUrl = storeData.imageUrl ? scaffoldAssetUrl(storeData.imageUrl) : null; - - // Fetch products for this store - const productsData = await db - .select({ - id: productInfo.id, - name: productInfo.name, - shortDescription: productInfo.shortDescription, - price: productInfo.price, - marketPrice: productInfo.marketPrice, - images: productInfo.images, - isOutOfStock: productInfo.isOutOfStock, - incrementStep: productInfo.incrementStep, - unitShortNotation: units.shortNotation, - unitNotation: units.shortNotation, - productQuantity: productInfo.productQuantity, - }) - .from(productInfo) - .innerJoin(units, eq(productInfo.unitId, units.id)) - .where(and(eq(productInfo.storeId, storeId), eq(productInfo.isSuspended, false))); - - - // Generate signed URLs for product images - const productsWithSignedUrls = await Promise.all( - productsData.map(async (product) => ({ - id: product.id, - name: product.name, - shortDescription: product.shortDescription, - price: product.price, - marketPrice: product.marketPrice, - incrementStep: product.incrementStep, - unit: product.unitShortNotation, - unitNotation: product.unitNotation, - images: scaffoldAssetUrl((product.images as string[]) || []), - isOutOfStock: product.isOutOfStock, - productQuantity: product.productQuantity - })) - ); - - const tags = await getTagsByStoreId(storeId); - - return { - store: { - id: storeData.id, - name: storeData.name, - description: storeData.description, - signedImageUrl, - }, - products: productsWithSignedUrls, - tags: tags.map(tag => ({ - id: tag.id, - tagName: tag.tagName, - tagDescription: tag.tagDescription, - imageUrl: tag.imageUrl, - productIds: tag.productIds, - })), - }; -} - -export const storesRouter = router({ - getStores: publicProcedure - .query(async () => { - const response = await scaffoldStores(); - return response; - }), - - getStoreWithProducts: publicProcedure - .input(z.object({ - storeId: z.number(), - })) - .query(async ({ input }) => { - const { storeId } = input; - const response = await scaffoldStoreWithProducts(storeId); - return response; - }), -}); diff --git a/verifier/user-apis/apis/tags.ts b/verifier/user-apis/apis/tags.ts deleted file mode 100644 index d21b229..0000000 --- a/verifier/user-apis/apis/tags.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { router, publicProcedure } from '@/src/trpc/trpc-index'; -import { z } from 'zod'; -import { getTagsByStoreId } from '@/src/stores/product-tag-store'; -import { ApiError } from '@/src/lib/api-error'; - -export const tagsRouter = router({ - getTagsByStore: publicProcedure - .input(z.object({ - storeId: z.number(), - })) - .query(async ({ input }) => { - const { storeId } = input; - - // Get tags from cache that are related to this store - const tags = await getTagsByStoreId(storeId); - - - return { - tags: tags.map(tag => ({ - id: tag.id, - tagName: tag.tagName, - tagDescription: tag.tagDescription, - imageUrl: tag.imageUrl, - productIds: tag.productIds, - })), - }; - }), -}); diff --git a/verifier/user-apis/apis/user-trpc-index.ts b/verifier/user-apis/apis/user-trpc-index.ts deleted file mode 100644 index 0b48629..0000000 --- a/verifier/user-apis/apis/user-trpc-index.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { router } from '@/src/trpc/trpc-index'; -import { addressRouter } from '@/src/trpc/apis/user-apis/apis/address'; -import { authRouter } from '@/src/trpc/apis/user-apis/apis/auth'; -import { bannerRouter } from '@/src/trpc/apis/user-apis/apis/banners'; -import { cartRouter } from '@/src/trpc/apis/user-apis/apis/cart'; -import { complaintRouter } from '@/src/trpc/apis/user-apis/apis/complaint'; -import { orderRouter } from '@/src/trpc/apis/user-apis/apis/order'; -import { productRouter } from '@/src/trpc/apis/user-apis/apis/product'; -import { slotsRouter } from '@/src/trpc/apis/user-apis/apis/slots'; -import { userRouter as userDataRouter } from '@/src/trpc/apis/user-apis/apis/user'; -import { userCouponRouter } from '@/src/trpc/apis/user-apis/apis/coupon'; -import { storesRouter } from '@/src/trpc/apis/user-apis/apis/stores'; -import { fileUploadRouter } from '@/src/trpc/apis/user-apis/apis/file-upload'; -import { tagsRouter } from '@/src/trpc/apis/user-apis/apis/tags'; - -export const userRouter = router({ - address: addressRouter, - auth: authRouter, - banner: bannerRouter, - cart: cartRouter, - complaint: complaintRouter, - order: orderRouter, - product: productRouter, - slots: slotsRouter, - user: userDataRouter, - coupon: userCouponRouter, - stores: storesRouter, - fileUpload: fileUploadRouter, - tags: tagsRouter, -}); - -export type UserRouter = typeof userRouter; diff --git a/verifier/user-apis/apis/user.ts b/verifier/user-apis/apis/user.ts deleted file mode 100644 index 057697a..0000000 --- a/verifier/user-apis/apis/user.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { router, protectedProcedure, publicProcedure } from '@/src/trpc/trpc-index'; -import { eq, and } from 'drizzle-orm'; -import { z } from 'zod'; -import { db } from '@/src/db/db_index'; -import { users, userDetails, userCreds, notifCreds, unloggedUserTokens } from '@/src/db/schema'; -import { ApiError } from '@/src/lib/api-error'; -import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client'; -import { signToken } from '@/src/lib/jwt-utils'; - -interface AuthResponse { - token: string; - user: { - id: number; - name: string | null; - email: string | null; - mobile: string | null; - profileImage?: string | null; - bio?: string | null; - dateOfBirth?: string | null; - gender?: string | null; - occupation?: string | null; - }; -} - -const generateToken = async (userId: number): Promise => { - return signToken({ userId }); -}; - -export const userRouter = router({ - getSelfData: protectedProcedure - .query(async ({ ctx }) => { - const userId = ctx.user.userId; - - if (!userId) { - throw new ApiError('User not authenticated', 401); - } - - const [user] = await db - .select() - .from(users) - .where(eq(users.id, userId)) - .limit(1); - - if (!user) { - throw new ApiError('User not found', 404); - } - - // Get user details for profile image - const [userDetail] = await db - .select() - .from(userDetails) - .where(eq(userDetails.userId, userId)) - .limit(1); - - // Generate signed URL for profile image if it exists - const profileImageSignedUrl = userDetail?.profileImage - ? await generateSignedUrlFromS3Url(userDetail.profileImage) - : null; - - const response: Omit = { - user: { - id: user.id, - name: user.name, - email: user.email, - mobile: user.mobile, - profileImage: profileImageSignedUrl, - bio: userDetail?.bio || null, - dateOfBirth: userDetail?.dateOfBirth || null, - gender: userDetail?.gender || null, - occupation: userDetail?.occupation || null, - }, - }; - - return { - success: true, - data: response, - }; - }), - - checkProfileComplete: protectedProcedure - .query(async ({ ctx }) => { - const userId = ctx.user.userId; - - if (!userId) { - throw new ApiError('User not authenticated', 401); - } - - const result = await db - .select() - .from(users) - .leftJoin(userCreds, eq(users.id, userCreds.userId)) - .where(eq(users.id, userId)) - .limit(1); - - if (result.length === 0) { - throw new ApiError('User not found', 404); - } - - const { users: user, user_creds: creds } = result[0]; - - return { - isComplete: !!(user.name && user.email && creds), - }; - }), - - savePushToken: publicProcedure - .input(z.object({ token: z.string() })) - .mutation(async ({ input, ctx }) => { - const { token } = input; - const userId = ctx.user?.userId; - - if (userId) { - // AUTHENTICATED USER - // Check if token exists in notif_creds for this user - const existing = await db.query.notifCreds.findFirst({ - where: and( - eq(notifCreds.userId, userId), - eq(notifCreds.token, token) - ), - }); - - if (existing) { - // Update lastVerified timestamp - await db - .update(notifCreds) - .set({ lastVerified: new Date() }) - .where(eq(notifCreds.id, existing.id)); - } else { - // Insert new token into notif_creds - await db.insert(notifCreds).values({ - userId, - token, - lastVerified: new Date(), - }); - } - - // Remove from unlogged_user_tokens if it exists - await db - .delete(unloggedUserTokens) - .where(eq(unloggedUserTokens.token, token)); - - } else { - // UNAUTHENTICATED USER - // Save/update in unlogged_user_tokens - const existing = await db.query.unloggedUserTokens.findFirst({ - where: eq(unloggedUserTokens.token, token), - }); - - if (existing) { - await db - .update(unloggedUserTokens) - .set({ lastVerified: new Date() }) - .where(eq(unloggedUserTokens.id, existing.id)); - } else { - await db.insert(unloggedUserTokens).values({ - token, - lastVerified: new Date(), - }); - } - } - - return { success: true }; - }), -}); diff --git a/verifier/user-apis/dataAccessors/demo.txt b/verifier/user-apis/dataAccessors/demo.txt deleted file mode 100644 index e69de29..0000000