From 038733c14a4ee0fb3c1afde88a8706f6b1a9bc3b Mon Sep 17 00:00:00 2001 From: shafi54 <108669266+shafi-aviz@users.noreply.github.com> Date: Wed, 25 Mar 2026 09:39:53 +0530 Subject: [PATCH] enh --- apps/backend/src/dbService.ts | 95 ++++++ .../trpc/apis/admin-apis/apis/staff-user.ts | 114 ++----- .../src/trpc/apis/admin-apis/apis/store.ts | 290 ++++++++-------- .../src/trpc/apis/admin-apis/apis/user.ts | 315 ++++-------------- packages/db_helper_postgres/index.ts | 7 + .../src/helper_methods/order.ts | 259 ++++++++++++++ .../src/helper_methods/product.ts | 130 ++++++++ .../src/helper_methods/slots.ts | 101 ++++++ .../src/helper_methods/staff-user.ts | 153 +++++++++ .../src/helper_methods/store.ts | 151 +++++++++ .../src/helper_methods/user.ts | 270 +++++++++++++++ .../src/helper_methods/vendor-snippets.ts | 130 ++++++++ packages/shared/types/index.ts | 2 + packages/shared/types/staff-user.types.ts | 17 + packages/shared/types/store.types.ts | 14 + 15 files changed, 1584 insertions(+), 464 deletions(-) create mode 100644 packages/db_helper_postgres/src/helper_methods/order.ts create mode 100644 packages/db_helper_postgres/src/helper_methods/product.ts create mode 100644 packages/db_helper_postgres/src/helper_methods/slots.ts create mode 100644 packages/db_helper_postgres/src/helper_methods/staff-user.ts create mode 100644 packages/db_helper_postgres/src/helper_methods/store.ts create mode 100644 packages/db_helper_postgres/src/helper_methods/user.ts create mode 100644 packages/db_helper_postgres/src/helper_methods/vendor-snippets.ts create mode 100644 packages/shared/types/staff-user.types.ts create mode 100644 packages/shared/types/store.types.ts diff --git a/apps/backend/src/dbService.ts b/apps/backend/src/dbService.ts index 7d744de..caa8552 100644 --- a/apps/backend/src/dbService.ts +++ b/apps/backend/src/dbService.ts @@ -39,6 +39,101 @@ export { checkCouponExists, checkReservedCouponExists, getOrderWithUser, + // Store methods + getAllStores, + getStoreById, + createStore, + updateStore, + deleteStore, + // Staff-user methods + getStaffUserByName, + getAllStaff, + getStaffByName, + getAllUsers, + getUserWithDetails, + updateUserSuspension, + checkStaffUserExists, + checkStaffRoleExists, + createStaffUser, + getAllRoles, + // User methods + createUserByMobile, + getUserByMobile, + getUnresolvedComplaintsCount, + getAllUsersWithFilters, + getOrderCountsByUserIds, + getLastOrdersByUserIds, + getSuspensionStatusesByUserIds, + getUserBasicInfo, + getUserSuspensionStatus, + getUserOrders, + getOrderStatusesByOrderIds, + getItemCountsByOrderIds, + upsertUserSuspension, + searchUsers, + getAllNotifCreds, + getAllUnloggedTokens, + getNotifTokensByUserIds, + getUserIncidentsWithRelations, + createUserIncident, + // Vendor-snippets methods + checkVendorSnippetExists, + getVendorSnippetById, + getVendorSnippetByCode, + getAllVendorSnippets, + createVendorSnippet, + updateVendorSnippet, + deleteVendorSnippet, + getProductsByIds, + getVendorSlotById, + getVendorOrdersBySlotId, + getOrderItemsByOrderIds, + getOrderStatusByOrderIds, + updateVendorOrderItemPackaging, + // Product methods + getAllProducts, + getProductById, + createProduct, + updateProduct, + toggleProductOutOfStock, + getAllUnits, + getAllProductTags, + getProductReviews, + respondToReview, + getAllProductGroups, + createProductGroup, + updateProductGroup, + deleteProductGroup, + addProductToGroup, + removeProductFromGroup, + // Slots methods + getAllSlots, + getSlotById, + createSlot, + updateSlot, + deleteSlot, + getSlotProducts, + addProductToSlot, + removeProductFromSlot, + clearSlotProducts, + updateSlotCapacity, + getSlotDeliverySequence, + updateSlotDeliverySequence, + // Order methods + updateOrderNotes, + getOrderWithDetails, + getFullOrder, + getOrderDetails, + getAllOrders, + getOrdersBySlotId, + updateOrderPackaged, + updateOrderDelivered, + updateOrderItemPackaging, + updateAddressCoords, + getOrderStatus, + cancelOrder, + getTodaysOrders, + removeDeliveryCharge, } from 'postgresService'; // Re-export types from local types file (to avoid circular dependencies) diff --git a/apps/backend/src/trpc/apis/admin-apis/apis/staff-user.ts b/apps/backend/src/trpc/apis/admin-apis/apis/staff-user.ts index 0a62c21..aee10f1 100644 --- a/apps/backend/src/trpc/apis/admin-apis/apis/staff-user.ts +++ b/apps/backend/src/trpc/apis/admin-apis/apis/staff-user.ts @@ -1,11 +1,20 @@ 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 jwt from 'jsonwebtoken'; import { ApiError } from '@/src/lib/api-error' +import { + getStaffUserByName, + getAllStaff, + getAllUsers, + getUserWithDetails, + updateUserSuspension, + checkStaffUserExists, + checkStaffRoleExists, + createStaffUser, + getAllRoles, +} from '@/src/dbService' +import type { StaffUser, StaffRole } from '@packages/shared' export const staffUserRouter = router({ login: publicProcedure @@ -20,9 +29,7 @@ export const staffUserRouter = router({ throw new ApiError('Name and password are required', 400); } - const staff = await db.query.staffUsers.findFirst({ - where: eq(staffUsers.name, name), - }); + const staff = await getStaffUserByName(name); if (!staff) { throw new ApiError('Invalid credentials', 401); @@ -48,23 +55,7 @@ export const staffUserRouter = router({ getStaff: protectedProcedure .query(async ({ ctx }) => { - const staff = await db.query.staffUsers.findMany({ - columns: { - id: true, - name: true, - }, - with: { - role: { - with: { - rolePermissions: { - with: { - permission: true, - }, - }, - }, - }, - }, - }); + const staff = await getAllStaff(); // Transform the data to include role and permissions in a cleaner format const transformedStaff = staff.map((user) => ({ @@ -74,7 +65,7 @@ export const staffUserRouter = router({ id: user.role.id, name: user.role.roleName, } : null, - permissions: user.role?.rolePermissions.map((rp) => ({ + permissions: user.role?.rolePermissions.map((rp: any) => ({ id: rp.permission.id, name: rp.permission.permissionName, })) || [], @@ -94,34 +85,9 @@ export const staffUserRouter = router({ .query(async ({ input }) => { const { cursor, limit, search } = input; - let whereCondition = undefined; + const { users: usersToReturn, hasMore } = await getAllUsers(cursor, limit, search); - 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 => ({ + const formattedUsers = usersToReturn.map((user: any) => ({ id: user.id, name: user.name, email: user.email, @@ -140,16 +106,7 @@ export const staffUserRouter = router({ .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, - }, - }, - }); + const user = await getUserWithDetails(userId); if (!user) { throw new ApiError("User not found", 404); @@ -173,13 +130,7 @@ export const staffUserRouter = router({ .mutation(async ({ input }) => { const { userId, isSuspended } = input; - await db - .insert(userDetails) - .values({ userId, isSuspended }) - .onConflictDoUpdate({ - target: userDetails.userId, - set: { isSuspended }, - }); + await updateUserSuspension(userId, isSuspended); return { success: true }; }), @@ -194,20 +145,16 @@ export const staffUserRouter = router({ const { name, password, roleId } = input; // Check if staff user already exists - const existingUser = await db.query.staffUsers.findFirst({ - where: eq(staffUsers.name, name), - }); + const existingUser = await checkStaffUserExists(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), - }); + const roleExists = await checkStaffRoleExists(roleId); - if (!role) { + if (!roleExists) { throw new ApiError('Invalid role selected', 400); } @@ -215,29 +162,20 @@ export const staffUserRouter = router({ 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(); + const newUser = await createStaffUser(name, hashedPassword, roleId); 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, - }, - }); + const roles = await getAllRoles(); return { - roles: roles.map(role => ({ + roles: roles.map((role: any) => ({ id: role.id, name: role.roleName, })), }; }), -}); \ No newline at end of file +}); diff --git a/apps/backend/src/trpc/apis/admin-apis/apis/store.ts b/apps/backend/src/trpc/apis/admin-apis/apis/store.ts index 3233071..5f5694e 100644 --- a/apps/backend/src/trpc/apis/admin-apis/apis/store.ts +++ b/apps/backend/src/trpc/apis/admin-apis/apis/store.ts @@ -1,30 +1,29 @@ 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, generateSignedUrlFromS3Url } from '@/src/lib/s3-client' -import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; +import { extractKeyFromPresignedUrl, deleteImageUtil, generateSignedUrlFromS3Url } from '@/src/lib/s3-client' import { scheduleStoreInitialization } from '@/src/stores/store-initializer' - +import { + getAllStores as getAllStoresFromDb, + getStoreById as getStoreByIdFromDb, + createStore as createStoreInDb, + updateStore as updateStoreInDb, + deleteStore as deleteStoreFromDb, +} from '@/src/dbService' +import type { Store } from '@packages/shared' export const storeRouter = router({ getStores: protectedProcedure - .query(async ({ ctx }) => { - const stores = await db.query.storeInfo.findMany({ - with: { - owner: true, - }, - }); + .query(async ({ ctx }): Promise<{ stores: any[]; count: number }> => { + const stores = await getAllStoresFromDb(); - Promise.all(stores.map(async store => { + await Promise.all(stores.map(async store => { if(store.imageUrl) store.imageUrl = await generateSignedUrlFromS3Url(store.imageUrl) })).catch((e) => { throw new ApiError("Unable to find store image urls") - } - ) + }) + return { stores, count: stores.length, @@ -35,15 +34,10 @@ export const storeRouter = router({ .input(z.object({ id: z.number(), })) - .query(async ({ input, ctx }) => { + .query(async ({ input, ctx }): Promise<{ store: any }> => { const { id } = input; - const store = await db.query.storeInfo.findFirst({ - where: eq(storeInfo.id, id), - with: { - owner: true, - }, - }); + const store = await getStoreByIdFromDb(id); if (!store) { throw new ApiError("Store not found", 404); @@ -54,19 +48,31 @@ export const storeRouter = router({ }; }), - 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 }) => { + 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 }): Promise<{ store: Store; message: string }> => { const { name, description, imageUrl, owner, products } = input; const imageKey = imageUrl ? extractKeyFromPresignedUrl(imageUrl) : undefined; + const newStore = await createStoreInDb( + { + name, + description, + imageUrl: imageKey, + owner, + }, + products + ); + + /* + // Old implementation - direct DB query: const [newStore] = await db .insert(storeInfo) .values({ @@ -84,6 +90,7 @@ export const storeRouter = router({ .set({ storeId: newStore.id }) .where(inArray(productInfo.id, products)); } + */ // Reinitialize stores to reflect changes scheduleStoreInitialization() @@ -94,117 +101,134 @@ export const storeRouter = router({ }; }), - 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 }) => { + 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 }): Promise<{ store: Store; message: string }> => { const { id, name, description, imageUrl, owner, products } = input; - const existingStore = await db.query.storeInfo.findFirst({ - where: eq(storeInfo.id, id), - }); + const existingStore = await getStoreByIdFromDb(id); - if (!existingStore) { - throw new ApiError("Store not found", 404); - } + if (!existingStore) { + throw new ApiError("Store not found", 404); + } - const oldImageKey = existingStore.imageUrl; - const newImageKey = imageUrl ? extractKeyFromPresignedUrl(imageUrl) : oldImageKey; + 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 - } + // 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)) + const updatedStore = await updateStoreInDb( + id, + { + name, + description, + imageUrl: newImageKey, + owner, + }, + products + ); + + /* + // Old implementation - direct DB query: + 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 }): Promise<{ message: string }> => { + const { storeId } = input; + + const result = await deleteStoreFromDb(storeId); + + /* + // Old implementation - direct DB query with transaction: + 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 (!updatedStore) { - throw new ApiError("Store not found", 404); - } + if (!deletedStore) { + 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)); + return { + message: "Store deleted successfully", + }; + }); + */ - // 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 (outside transaction) + scheduleStoreInitialization() - // 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; - }), - }); + return result; + }), +}); diff --git a/apps/backend/src/trpc/apis/admin-apis/apis/user.ts b/apps/backend/src/trpc/apis/admin-apis/apis/user.ts index 2b176b2..2ae5760 100644 --- a/apps/backend/src/trpc/apis/admin-apis/apis/user.ts +++ b/apps/backend/src/trpc/apis/admin-apis/apis/user.ts @@ -1,44 +1,29 @@ 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; -} +import { + createUserByMobile, + getUserByMobile, + getUnresolvedComplaintsCount, + getAllUsersWithFilters, + getOrderCountsByUserIds, + getLastOrdersByUserIds, + getSuspensionStatusesByUserIds, + getUserBasicInfo, + getUserSuspensionStatus, + getUserOrders, + getOrderStatusesByOrderIds, + getItemCountsByOrderIds, + upsertUserSuspension, + searchUsers, + getAllNotifCreds, + getAllUnloggedTokens, + getNotifTokensByUserIds, + getUserIncidentsWithRelations, + createUserIncident, +} from '@/src/dbService'; export const userRouter = { createUserByMobile: protectedProcedure @@ -46,7 +31,22 @@ export const userRouter = { mobile: z.string().min(1, 'Mobile number is required'), })) .mutation(async ({ input }) => { - const newUser = await createUserByMobile(input.mobile); + // Clean mobile number (remove non-digits) + const cleanMobile = input.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 getUserByMobile(cleanMobile); + + if (existingUser) { + throw new ApiError('User with this mobile number already exists', 409); + } + + const newUser = await createUserByMobile(cleanMobile); return { success: true, @@ -56,10 +56,10 @@ export const userRouter = { getEssentials: protectedProcedure .query(async () => { - const count = await db.$count(complaints, eq(complaints.isResolved, false)); + const count = await getUnresolvedComplaintsCount(); return { - unresolvedComplaints: count || 0, + unresolvedComplaints: count, }; }), @@ -72,79 +72,22 @@ export const userRouter = { .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; + const { users: usersToReturn, hasMore } = await getAllUsersWithFilters(limit, cursor, search); // Get order stats for each user - const userIds = usersToReturn.map(u => u.id); + const userIds = usersToReturn.map((u: any) => u.id); - let orderCounts: { userId: number; totalOrders: number }[] = []; - let lastOrders: { userId: number; lastOrderDate: Date | null }[] = []; - let suspensionStatuses: { userId: number; isSuspended: boolean }[] = []; + const orderCounts = await getOrderCountsByUserIds(userIds); + const lastOrders = await getLastOrdersByUserIds(userIds); + const suspensionStatuses = await getSuspensionStatusesByUserIds(userIds); - 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 => ({ + const usersWithStats = usersToReturn.map((user: any) => ({ ...user, totalOrders: orderCountMap.get(user.id) || 0, lastOrderDate: lastOrderMap.get(user.id) || null, @@ -169,69 +112,24 @@ export const userRouter = { 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); + const user = await getUserBasicInfo(userId); - if (!user || user.length === 0) { + if (!user) { 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); + const isSuspended = await getUserSuspensionStatus(userId); - // 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 all orders for this user + const userOrders = await getUserOrders(userId); // 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`, `)})`); - } + const orderIds = userOrders.map((o: any) => o.id); + const orderStatuses = await getOrderStatusesByOrderIds(orderIds); // 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); + const itemCounts = await getItemCountsByOrderIds(orderIds); // Create lookup maps const statusMap = new Map(orderStatuses.map(s => [s.orderId, s])); @@ -246,7 +144,7 @@ export const userRouter = { }; // Combine data - const ordersWithDetails = userOrders.map(order => { + const ordersWithDetails = userOrders.map((order: any) => { const status = statusMap.get(order.id); return { id: order.id, @@ -261,8 +159,8 @@ export const userRouter = { return { user: { - ...user[0], - isSuspended: userDetail[0]?.isSuspended ?? false, + ...user, + isSuspended, }, orders: ordersWithDetails, }; @@ -276,39 +174,7 @@ export const userRouter = { .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, - }); - } + await upsertUserSuspension(userId, isSuspended); return { success: true, @@ -323,36 +189,15 @@ export const userRouter = { .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); - } + const usersList = await searchUsers(search); // Get eligible users (have notif_creds entry) - const eligibleUsers = await db - .select({ userId: notifCreds.userId }) - .from(notifCreds); + const eligibleUsers = await getAllNotifCreds(); const eligibleSet = new Set(eligibleUsers.map(u => u.userId)); return { - users: usersList.map(user => ({ + users: usersList.map((user: any) => ({ id: user.id, name: user.name, mobile: user.mobile, @@ -375,8 +220,8 @@ export const userRouter = { 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); + const loggedInTokens = await getAllNotifCreds(); + const unloggedTokens = await getAllUnloggedTokens(); tokens = [ ...loggedInTokens.map(t => t.token), @@ -384,11 +229,7 @@ export const userRouter = { ]; } else { // Send to specific users - get their tokens - const userTokens = await db - .select({ token: notifCreds.token }) - .from(notifCreds) - .where(inArray(notifCreds.userId, userIds)); - + const userTokens = await getNotifTokensByUserIds(userIds); tokens = userTokens.map(t => t.token); } @@ -427,21 +268,10 @@ export const userRouter = { .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), - }); + const incidents = await getUserIncidentsWithRelations(userId); return { - incidents: incidents.map(incident => ({ + incidents: incidents.map((incident: any) => ({ id: incident.id, userId: incident.userId, orderId: incident.orderId, @@ -470,14 +300,13 @@ export const userRouter = { 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(); + const incident = await createUserIncident( + userId, + orderId, + adminComment, + adminUserId, + negativityScore + ); recomputeUserNegativityScore(userId); diff --git a/packages/db_helper_postgres/index.ts b/packages/db_helper_postgres/index.ts index a55459b..6069737 100644 --- a/packages/db_helper_postgres/index.ts +++ b/packages/db_helper_postgres/index.ts @@ -12,3 +12,10 @@ export * from './src/helper_methods/banner'; export * from './src/helper_methods/complaint'; export * from './src/helper_methods/const'; export * from './src/helper_methods/coupon'; +export * from './src/helper_methods/store'; +export * from './src/helper_methods/staff-user'; +export * from './src/helper_methods/user'; +export * from './src/helper_methods/vendor-snippets'; +export * from './src/helper_methods/product'; +export * from './src/helper_methods/slots'; +export * from './src/helper_methods/order'; diff --git a/packages/db_helper_postgres/src/helper_methods/order.ts b/packages/db_helper_postgres/src/helper_methods/order.ts new file mode 100644 index 0000000..355a9d2 --- /dev/null +++ b/packages/db_helper_postgres/src/helper_methods/order.ts @@ -0,0 +1,259 @@ +import { db } from '../db/db_index'; +import { orders, orderItems, orderStatus, users, addresses, refunds, complaints, payments } from '../db/schema'; +import { eq, and, gte, lt, desc, inArray, sql } from 'drizzle-orm'; + +export async function updateOrderNotes(orderId: number, adminNotes: string | null): Promise { + const [result] = await db + .update(orders) + .set({ adminNotes }) + .where(eq(orders.id, orderId)) + .returning(); + return result; +} + +export async function getOrderWithDetails(orderId: number): Promise { + return await db.query.orders.findFirst({ + where: eq(orders.id, orderId), + with: { + orderItems: { + with: { + product: { + with: { + unit: true, + }, + }, + }, + }, + user: true, + address: true, + orderStatus: true, + slot: true, + payments: true, + refunds: true, + }, + }); +} + +export async function getFullOrder(orderId: number): Promise { + return await db.query.orders.findFirst({ + where: eq(orders.id, orderId), + with: { + orderItems: { + with: { + product: { + with: { + unit: true, + }, + }, + }, + }, + user: { + with: { + userDetails: true, + }, + }, + address: true, + orderStatus: true, + slot: true, + payments: true, + refunds: true, + complaints: true, + }, + }); +} + +export async function getOrderDetails(orderId: number): Promise { + return await db.query.orders.findFirst({ + where: eq(orders.id, orderId), + with: { + orderItems: { + with: { + product: { + with: { + unit: true, + }, + }, + }, + }, + user: true, + address: true, + orderStatus: true, + slot: true, + payments: true, + refunds: true, + complaints: true, + }, + }); +} + +export async function getAllOrders( + limit: number, + cursor?: number, + slotId?: number | null, + filters?: any +): Promise<{ orders: any[]; hasMore: boolean }> { + let whereConditions = []; + + if (cursor) { + whereConditions.push(lt(orders.id, cursor)); + } + + if (slotId) { + whereConditions.push(eq(orders.slotId, slotId)); + } + + // Add filter conditions + if (filters) { + if (filters.packagedFilter === 'packaged') { + whereConditions.push(eq(orders.isPackaged, true)); + } else if (filters.packagedFilter === 'not_packaged') { + whereConditions.push(eq(orders.isPackaged, false)); + } + + if (filters.deliveredFilter === 'delivered') { + whereConditions.push(eq(orders.isDelivered, true)); + } else if (filters.deliveredFilter === 'not_delivered') { + whereConditions.push(eq(orders.isDelivered, false)); + } + + if (filters.flashDeliveryFilter === 'flash') { + whereConditions.push(eq(orders.isFlashDelivery, true)); + } else if (filters.flashDeliveryFilter === 'regular') { + whereConditions.push(eq(orders.isFlashDelivery, false)); + } + } + + const ordersList = await db.query.orders.findMany({ + where: whereConditions.length > 0 ? and(...whereConditions) : undefined, + with: { + orderItems: { + with: { + product: true, + }, + }, + user: true, + orderStatus: true, + slot: true, + }, + orderBy: desc(orders.id), + limit: limit + 1, + }); + + const hasMore = ordersList.length > limit; + return { orders: hasMore ? ordersList.slice(0, limit) : ordersList, hasMore }; +} + +export async function getOrdersBySlotId(slotId: number): Promise { + return await db.query.orders.findMany({ + where: eq(orders.slotId, slotId), + with: { + orderItems: { + with: { + product: true, + }, + }, + user: true, + orderStatus: true, + address: true, + }, + orderBy: desc(orders.createdAt), + }); +} + +export async function updateOrderPackaged(orderId: number, isPackaged: boolean): Promise { + const [result] = await db + .update(orders) + .set({ isPackaged }) + .where(eq(orders.id, orderId)) + .returning(); + return result; +} + +export async function updateOrderDelivered(orderId: number, isDelivered: boolean): Promise { + const [result] = await db + .update(orders) + .set({ isDelivered }) + .where(eq(orders.id, orderId)) + .returning(); + return result; +} + +export async function updateOrderItemPackaging( + orderItemId: number, + isPackaged: boolean, + isPackageVerified: boolean +): Promise { + await db.update(orderItems) + .set({ is_packaged: isPackaged, is_package_verified: isPackageVerified }) + .where(eq(orderItems.id, orderItemId)); +} + +export async function updateAddressCoords(addressId: number, lat: number, lng: number): Promise { + await db.update(addresses) + .set({ lat, lng }) + .where(eq(addresses.id, addressId)); +} + +export async function getOrderStatus(orderId: number): Promise { + return await db.query.orderStatus.findFirst({ + where: eq(orderStatus.orderId, orderId), + }); +} + +export async function cancelOrder(orderId: number, reason: string): Promise { + return await db.transaction(async (tx) => { + // Update order status + const [order] = await tx.update(orders) + .set({ isCancelled: true, cancellationReason: reason }) + .where(eq(orders.id, orderId)) + .returning(); + + // Create order status entry + await tx.insert(orderStatus).values({ + orderId, + isCancelled: true, + cancelReason: reason, + }); + + return order; + }); +} + +export async function getTodaysOrders(slotId?: number): Promise { + const today = new Date(); + today.setHours(0, 0, 0, 0); + const tomorrow = new Date(today); + tomorrow.setDate(tomorrow.getDate() + 1); + + let whereConditions = [ + gte(orders.createdAt, today), + lt(orders.createdAt, tomorrow), + ]; + + if (slotId) { + whereConditions.push(eq(orders.slotId, slotId)); + } + + return await db.query.orders.findMany({ + where: and(...whereConditions), + with: { + orderItems: { + with: { + product: true, + }, + }, + user: true, + orderStatus: true, + }, + orderBy: desc(orders.createdAt), + }); +} + +export async function removeDeliveryCharge(orderId: number): Promise { + const [result] = await db + .update(orders) + .set({ deliveryCharge: '0' }) + .where(eq(orders.id, orderId)) + .returning(); + return result; +} diff --git a/packages/db_helper_postgres/src/helper_methods/product.ts b/packages/db_helper_postgres/src/helper_methods/product.ts new file mode 100644 index 0000000..13c4b51 --- /dev/null +++ b/packages/db_helper_postgres/src/helper_methods/product.ts @@ -0,0 +1,130 @@ +import { db } from '../db/db_index'; +import { productInfo, units, specialDeals, productSlots, productTags, productReviews, productGroupInfo, productGroupMembership } from '../db/schema'; +import { eq, and, inArray, desc, sql, asc } from 'drizzle-orm'; + +export async function getAllProducts(): Promise { + return await db.query.productInfo.findMany({ + orderBy: productInfo.name, + with: { + unit: true, + store: true, + }, + }); +} + +export async function getProductById(id: number): Promise { + return await db.query.productInfo.findFirst({ + where: eq(productInfo.id, id), + with: { + unit: true, + store: true, + productSlots: { + with: { + slot: true, + }, + }, + specialDeals: true, + productTags: { + with: { + tag: true, + }, + }, + }, + }); +} + +export async function createProduct(input: any): Promise { + const [product] = await db.insert(productInfo).values(input).returning(); + return product; +} + +export async function updateProduct(id: number, updates: any): Promise { + const [product] = await db.update(productInfo) + .set(updates) + .where(eq(productInfo.id, id)) + .returning(); + return product; +} + +export async function toggleProductOutOfStock(id: number, isOutOfStock: boolean): Promise { + const [product] = await db.update(productInfo) + .set({ isOutOfStock }) + .where(eq(productInfo.id, id)) + .returning(); + return product; +} + +export async function getAllUnits(): Promise { + return await db.query.units.findMany({ + orderBy: units.name, + }); +} + +export async function getAllProductTags(): Promise { + return await db.query.productTags.findMany({ + with: { + products: { + with: { + product: true, + }, + }, + }, + }); +} + +export async function getProductReviews(productId: number): Promise { + return await db.query.productReviews.findMany({ + where: eq(productReviews.productId, productId), + with: { + user: true, + }, + orderBy: desc(productReviews.createdAt), + }); +} + +export async function respondToReview(reviewId: number, adminResponse: string): Promise { + await db.update(productReviews) + .set({ adminResponse }) + .where(eq(productReviews.id, reviewId)); +} + +export async function getAllProductGroups(): Promise { + return await db.query.productGroupInfo.findMany({ + with: { + products: { + with: { + product: true, + }, + }, + }, + }); +} + +export async function createProductGroup(name: string): Promise { + const [group] = await db.insert(productGroupInfo).values({ name }).returning(); + return group; +} + +export async function updateProductGroup(id: number, name: string): Promise { + const [group] = await db.update(productGroupInfo) + .set({ name }) + .where(eq(productGroupInfo.id, id)) + .returning(); + return group; +} + +export async function deleteProductGroup(id: number): Promise { + await db.delete(productGroupInfo).where(eq(productGroupInfo.id, id)); +} + +export async function addProductToGroup(groupId: number, productId: number): Promise { + await db.insert(productGroupMembership).values({ groupId, productId }); +} + +export async function removeProductFromGroup(groupId: number, productId: number): Promise { + await db.delete(productGroupMembership) + .where(and( + eq(productGroupMembership.groupId, groupId), + eq(productGroupMembership.productId, productId) + )); +} diff --git a/packages/db_helper_postgres/src/helper_methods/slots.ts b/packages/db_helper_postgres/src/helper_methods/slots.ts new file mode 100644 index 0000000..bcc2f01 --- /dev/null +++ b/packages/db_helper_postgres/src/helper_methods/slots.ts @@ -0,0 +1,101 @@ +import { db } from '../db/db_index'; +import { deliverySlotInfo, productSlots, productInfo, vendorSnippets } from '../db/schema'; +import { eq, and, inArray, desc } from 'drizzle-orm'; + +export async function getAllSlots(): Promise { + return await db.query.deliverySlotInfo.findMany({ + orderBy: desc(deliverySlotInfo.createdAt), + with: { + productSlots: { + with: { + product: true, + }, + }, + vendorSnippets: true, + }, + }); +} + +export async function getSlotById(id: number): Promise { + return await db.query.deliverySlotInfo.findFirst({ + where: eq(deliverySlotInfo.id, id), + with: { + productSlots: { + with: { + product: true, + }, + }, + vendorSnippets: { + with: { + slot: true, + }, + }, + }, + }); +} + +export async function createSlot(input: any): Promise { + const [slot] = await db.insert(deliverySlotInfo).values(input).returning(); + return slot; +} + +export async function updateSlot(id: number, updates: any): Promise { + const [slot] = await db.update(deliverySlotInfo) + .set(updates) + .where(eq(deliverySlotInfo.id, id)) + .returning(); + return slot; +} + +export async function deleteSlot(id: number): Promise { + await db.delete(deliverySlotInfo).where(eq(deliverySlotInfo.id, id)); +} + +export async function getSlotProducts(slotId: number): Promise { + return await db.query.productSlots.findMany({ + where: eq(productSlots.slotId, slotId), + with: { + product: true, + }, + }); +} + +export async function addProductToSlot(slotId: number, productId: number): Promise { + await db.insert(productSlots).values({ slotId, productId }); +} + +export async function removeProductFromSlot(slotId: number, productId: number): Promise { + await db.delete(productSlots) + .where(and( + eq(productSlots.slotId, slotId), + eq(productSlots.productId, productId) + )); +} + +export async function clearSlotProducts(slotId: number): Promise { + await db.delete(productSlots).where(eq(productSlots.slotId, slotId)); +} + +export async function updateSlotCapacity(slotId: number, maxCapacity: number): Promise { + const [slot] = await db.update(deliverySlotInfo) + .set({ maxCapacity }) + .where(eq(deliverySlotInfo.id, slotId)) + .returning(); + return slot; +} + +export async function getSlotDeliverySequence(slotId: number): Promise { + const slot = await db.query.deliverySlotInfo.findFirst({ + where: eq(deliverySlotInfo.id, slotId), + columns: { + deliverySequence: true, + }, + }); + return slot?.deliverySequence || null; +} + +export async function updateSlotDeliverySequence(slotId: number, sequence: any): Promise { + await db.update(deliverySlotInfo) + .set({ deliverySequence: sequence }) + .where(eq(deliverySlotInfo.id, slotId)); +} diff --git a/packages/db_helper_postgres/src/helper_methods/staff-user.ts b/packages/db_helper_postgres/src/helper_methods/staff-user.ts new file mode 100644 index 0000000..f02f8b0 --- /dev/null +++ b/packages/db_helper_postgres/src/helper_methods/staff-user.ts @@ -0,0 +1,153 @@ +import { db } from '../db/db_index'; +import { staffUsers, staffRoles, users, userDetails, orders } from '../db/schema'; +import { eq, or, ilike, and, lt, desc } from 'drizzle-orm'; + +export interface StaffUser { + id: number; + name: string; + password: string; + staffRoleId: number; + createdAt: Date; +} + +export async function getStaffUserByName(name: string): Promise { + const staff = await db.query.staffUsers.findFirst({ + where: eq(staffUsers.name, name), + }); + + return staff || null; +} + +export async function getAllStaff(): Promise { + const staff = await db.query.staffUsers.findMany({ + columns: { + id: true, + name: true, + }, + with: { + role: { + with: { + rolePermissions: { + with: { + permission: true, + }, + }, + }, + }, + }, + }); + + return staff; +} + +export async function getStaffByName(name: string): Promise { + const staff = await db.query.staffUsers.findFirst({ + where: eq(staffUsers.name, name), + }); + return staff || null; +} + +export async function getAllUsers( + cursor?: number, + limit: number = 20, + search?: string +): Promise<{ users: any[]; hasMore: boolean }> { + 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, + }); + + const hasMore = allUsers.length > limit; + const usersToReturn = hasMore ? allUsers.slice(0, limit) : allUsers; + + return { users: usersToReturn, hasMore }; +} + +export async function getUserWithDetails(userId: number): Promise { + const user = await db.query.users.findFirst({ + where: eq(users.id, userId), + with: { + userDetails: true, + orders: { + orderBy: desc(orders.createdAt), + limit: 1, + }, + }, + }); + + return user || null; +} + +export async function updateUserSuspension(userId: number, isSuspended: boolean): Promise { + await db + .insert(userDetails) + .values({ userId, isSuspended }) + .onConflictDoUpdate({ + target: userDetails.userId, + set: { isSuspended }, + }); +} + +export async function checkStaffUserExists(name: string): Promise { + const existingUser = await db.query.staffUsers.findFirst({ + where: eq(staffUsers.name, name), + }); + return !!existingUser; +} + +export async function checkStaffRoleExists(roleId: number): Promise { + const role = await db.query.staffRoles.findFirst({ + where: eq(staffRoles.id, roleId), + }); + return !!role; +} + +export async function createStaffUser( + name: string, + password: string, + roleId: number +): Promise { + const [newUser] = await db.insert(staffUsers).values({ + name: name.trim(), + password, + staffRoleId: roleId, + }).returning(); + + return { + id: newUser.id, + name: newUser.name, + password: newUser.password, + staffRoleId: newUser.staffRoleId, + createdAt: newUser.createdAt, + }; +} + +export async function getAllRoles(): Promise { + const roles = await db.query.staffRoles.findMany({ + columns: { + id: true, + roleName: true, + }, + }); + + return roles; +} diff --git a/packages/db_helper_postgres/src/helper_methods/store.ts b/packages/db_helper_postgres/src/helper_methods/store.ts new file mode 100644 index 0000000..cb1d755 --- /dev/null +++ b/packages/db_helper_postgres/src/helper_methods/store.ts @@ -0,0 +1,151 @@ +import { db } from '../db/db_index'; +import { storeInfo, productInfo } from '../db/schema'; +import { eq, inArray } from 'drizzle-orm'; + +export interface Store { + id: number; + name: string; + description: string | null; + imageUrl: string | null; + owner: number; + createdAt: Date; + updatedAt: Date; +} + +export async function getAllStores(): Promise { + const stores = await db.query.storeInfo.findMany({ + with: { + owner: true, + }, + }); + + return stores; +} + +export async function getStoreById(id: number): Promise { + const store = await db.query.storeInfo.findFirst({ + where: eq(storeInfo.id, id), + with: { + owner: true, + }, + }); + + return store || null; +} + +export interface CreateStoreInput { + name: string; + description?: string; + imageUrl?: string; + owner: number; +} + +export async function createStore( + input: CreateStoreInput, + products?: number[] +): Promise { + const [newStore] = await db + .insert(storeInfo) + .values({ + name: input.name, + description: input.description, + imageUrl: input.imageUrl, + owner: input.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)); + } + + return { + id: newStore.id, + name: newStore.name, + description: newStore.description, + imageUrl: newStore.imageUrl, + owner: newStore.owner, + createdAt: newStore.createdAt, + updatedAt: newStore.updatedAt, + }; +} + +export interface UpdateStoreInput { + name?: string; + description?: string; + imageUrl?: string; + owner?: number; +} + +export async function updateStore( + id: number, + input: UpdateStoreInput, + products?: number[] +): Promise { + const [updatedStore] = await db + .update(storeInfo) + .set({ + ...input, + updatedAt: new Date(), + }) + .where(eq(storeInfo.id, id)) + .returning(); + + if (!updatedStore) { + throw new Error("Store not found"); + } + + // Update products if provided + if (products !== undefined) { + // 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)); + } + } + + return { + id: updatedStore.id, + name: updatedStore.name, + description: updatedStore.description, + imageUrl: updatedStore.imageUrl, + owner: updatedStore.owner, + createdAt: updatedStore.createdAt, + updatedAt: updatedStore.updatedAt, + }; +} + +export async function deleteStore(id: number): Promise<{ message: string }> { + return 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, id)); + + // Then delete the store + const [deletedStore] = await tx + .delete(storeInfo) + .where(eq(storeInfo.id, id)) + .returning(); + + if (!deletedStore) { + throw new Error("Store not found"); + } + + return { + message: "Store deleted successfully", + }; + }); +} diff --git a/packages/db_helper_postgres/src/helper_methods/user.ts b/packages/db_helper_postgres/src/helper_methods/user.ts new file mode 100644 index 0000000..e25fcac --- /dev/null +++ b/packages/db_helper_postgres/src/helper_methods/user.ts @@ -0,0 +1,270 @@ +import { db } from '../db/db_index'; +import { users, userDetails, orders, orderItems, complaints, notifCreds, unloggedUserTokens, userIncidents, orderStatus } from '../db/schema'; +import { eq, sql, desc, asc, count, max, inArray } from 'drizzle-orm'; + +export async function createUserByMobile(mobile: string): Promise { + const [newUser] = await db + .insert(users) + .values({ + name: null, + email: null, + mobile, + }) + .returning(); + + return newUser; +} + +export async function getUserByMobile(mobile: string): Promise { + const [existingUser] = await db + .select() + .from(users) + .where(eq(users.mobile, mobile)) + .limit(1); + + return existingUser || null; +} + +export async function getUnresolvedComplaintsCount(): Promise { + const result = await db + .select({ count: count(complaints.id) }) + .from(complaints) + .where(eq(complaints.isResolved, false)); + + return result[0]?.count || 0; +} + +export async function getAllUsersWithFilters( + limit: number, + cursor?: number, + search?: string +): Promise<{ users: any[]; hasMore: boolean }> { + const whereConditions = []; + + if (search && search.trim()) { + whereConditions.push(sql`${users.mobile} ILIKE ${`%${search.trim()}%`}`); + } + + if (cursor) { + whereConditions.push(sql`${users.id} > ${cursor}`); + } + + 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); + + const hasMore = usersList.length > limit; + const usersToReturn = hasMore ? usersList.slice(0, limit) : usersList; + + return { users: usersToReturn, hasMore }; +} + +export async function getOrderCountsByUserIds(userIds: number[]): Promise<{ userId: number; totalOrders: number }[]> { + if (userIds.length === 0) return []; + + return await db + .select({ + userId: orders.userId, + totalOrders: count(orders.id), + }) + .from(orders) + .where(sql`${orders.userId} IN (${sql.join(userIds, sql`, `)})`) + .groupBy(orders.userId); +} + +export async function getLastOrdersByUserIds(userIds: number[]): Promise<{ userId: number; lastOrderDate: Date | null }[]> { + if (userIds.length === 0) return []; + + return await db + .select({ + userId: orders.userId, + lastOrderDate: max(orders.createdAt), + }) + .from(orders) + .where(sql`${orders.userId} IN (${sql.join(userIds, sql`, `)})`) + .groupBy(orders.userId); +} + +export async function getSuspensionStatusesByUserIds(userIds: number[]): Promise<{ userId: number; isSuspended: boolean }[]> { + if (userIds.length === 0) return []; + + return await db + .select({ + userId: userDetails.userId, + isSuspended: userDetails.isSuspended, + }) + .from(userDetails) + .where(sql`${userDetails.userId} IN (${sql.join(userIds, sql`, `)})`); +} + +export async function getUserBasicInfo(userId: number): Promise { + 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); + + return user[0] || null; +} + +export async function getUserSuspensionStatus(userId: number): Promise { + const userDetail = await db + .select({ + isSuspended: userDetails.isSuspended, + }) + .from(userDetails) + .where(eq(userDetails.userId, userId)) + .limit(1); + + return userDetail[0]?.isSuspended ?? false; +} + +export async function getUserOrders(userId: number): Promise { + return 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)); +} + +export async function getOrderStatusesByOrderIds(orderIds: number[]): Promise<{ orderId: number; isDelivered: boolean; isCancelled: boolean }[]> { + if (orderIds.length === 0) return []; + + return await db + .select({ + orderId: orderStatus.orderId, + isDelivered: orderStatus.isDelivered, + isCancelled: orderStatus.isCancelled, + }) + .from(orderStatus) + .where(sql`${orderStatus.orderId} IN (${sql.join(orderIds, sql`, `)})`); +} + +export async function getItemCountsByOrderIds(orderIds: number[]): Promise<{ orderId: number; itemCount: number }[]> { + if (orderIds.length === 0) return []; + + return await db + .select({ + orderId: orderItems.orderId, + itemCount: count(orderItems.id), + }) + .from(orderItems) + .where(sql`${orderItems.orderId} IN (${sql.join(orderIds, sql`, `)})`) + .groupBy(orderItems.orderId); +} + +export async function upsertUserSuspension(userId: number, isSuspended: boolean): Promise { + const existingDetail = await db + .select({ id: userDetails.id }) + .from(userDetails) + .where(eq(userDetails.userId, userId)) + .limit(1); + + if (existingDetail.length > 0) { + await db + .update(userDetails) + .set({ isSuspended }) + .where(eq(userDetails.userId, userId)); + } else { + await db + .insert(userDetails) + .values({ + userId, + isSuspended, + }); + } +} + +export async function searchUsers(search?: string): Promise { + if (search && search.trim()) { + return 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 { + return await db + .select({ + id: users.id, + name: users.name, + mobile: users.mobile, + }) + .from(users); + } +} + +export async function getAllNotifCreds(): Promise<{ userId: number }[]> { + return await db + .select({ userId: notifCreds.userId }) + .from(notifCreds); +} + +export async function getAllUnloggedTokens(): Promise<{ token: string }[]> { + return await db + .select({ token: unloggedUserTokens.token }) + .from(unloggedUserTokens); +} + +export async function getNotifTokensByUserIds(userIds: number[]): Promise<{ token: string }[]> { + return await db + .select({ token: notifCreds.token }) + .from(notifCreds) + .where(inArray(notifCreds.userId, userIds)); +} + +export async function getUserIncidentsWithRelations(userId: number): Promise { + return await db.query.userIncidents.findMany({ + where: eq(userIncidents.userId, userId), + with: { + order: { + with: { + orderStatus: true, + }, + }, + addedBy: true, + }, + orderBy: desc(userIncidents.dateAdded), + }); +} + +export async function createUserIncident( + userId: number, + orderId: number | undefined, + adminComment: string | undefined, + adminUserId: number, + negativityScore: number | undefined +): Promise { + const [incident] = await db.insert(userIncidents) + .values({ + userId, + orderId, + adminComment, + addedBy: adminUserId, + negativityScore, + }) + .returning(); + + return incident; +} diff --git a/packages/db_helper_postgres/src/helper_methods/vendor-snippets.ts b/packages/db_helper_postgres/src/helper_methods/vendor-snippets.ts new file mode 100644 index 0000000..8cec6a6 --- /dev/null +++ b/packages/db_helper_postgres/src/helper_methods/vendor-snippets.ts @@ -0,0 +1,130 @@ +import { db } from '../db/db_index'; +import { vendorSnippets, deliverySlotInfo, productInfo, orders, orderItems, orderStatus } from '../db/schema'; +import { eq, and, inArray, gt, sql, asc } from 'drizzle-orm'; + +export async function checkVendorSnippetExists(snippetCode: string): Promise { + const existingSnippet = await db.query.vendorSnippets.findFirst({ + where: eq(vendorSnippets.snippetCode, snippetCode), + }); + return !!existingSnippet; +} + +export async function getVendorSnippetById(id: number): Promise { + return await db.query.vendorSnippets.findFirst({ + where: eq(vendorSnippets.id, id), + with: { + slot: true, + }, + }); +} + +export async function getVendorSnippetByCode(snippetCode: string): Promise { + return await db.query.vendorSnippets.findFirst({ + where: eq(vendorSnippets.snippetCode, snippetCode), + }); +} + +export async function getAllVendorSnippets(): Promise { + return await db.query.vendorSnippets.findMany({ + with: { + slot: true, + }, + orderBy: (vendorSnippets, { desc }) => [desc(vendorSnippets.createdAt)], + }); +} + +export interface CreateVendorSnippetInput { + snippetCode: string; + slotId?: number; + productIds: number[]; + isPermanent: boolean; + validTill?: Date; +} + +export async function createVendorSnippet(input: CreateVendorSnippetInput): Promise { + const [result] = await db.insert(vendorSnippets).values({ + snippetCode: input.snippetCode, + slotId: input.slotId, + productIds: input.productIds, + isPermanent: input.isPermanent, + validTill: input.validTill, + }).returning(); + + return result; +} + +export async function updateVendorSnippet(id: number, updates: any): Promise { + const [result] = await db.update(vendorSnippets) + .set(updates) + .where(eq(vendorSnippets.id, id)) + .returning(); + + return result; +} + +export async function deleteVendorSnippet(id: number): Promise { + await db.delete(vendorSnippets) + .where(eq(vendorSnippets.id, id)); +} + +export async function getProductsByIds(productIds: number[]): Promise { + return await db.query.productInfo.findMany({ + where: inArray(productInfo.id, productIds), + columns: { id: true, name: true }, + }); +} + +export async function getVendorSlotById(slotId: number): Promise { + return await db.query.deliverySlotInfo.findFirst({ + where: eq(deliverySlotInfo.id, slotId), + }); +} + +export async function getVendorOrdersBySlotId(slotId: number): Promise { + return 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)], + }); +} + +export async function getOrderItemsByOrderIds(orderIds: number[]): Promise { + return await db.query.orderItems.findMany({ + where: inArray(orderItems.orderId, orderIds), + with: { + product: { + with: { + unit: true, + }, + }, + }, + }); +} + +export async function getOrderStatusByOrderIds(orderIds: number[]): Promise { + return await db.query.orderStatus.findMany({ + where: inArray(orderStatus.orderId, orderIds), + }); +} + +export async function updateVendorOrderItemPackaging(orderItemId: number, isPackaged: boolean, isPackageVerified: boolean): Promise { + await db.update(orderItems) + .set({ + is_packaged: isPackaged, + is_package_verified: isPackageVerified, + }) + .where(eq(orderItems.id, orderItemId)); +} diff --git a/packages/shared/types/index.ts b/packages/shared/types/index.ts index 0188fee..6f8996d 100644 --- a/packages/shared/types/index.ts +++ b/packages/shared/types/index.ts @@ -5,3 +5,5 @@ export type { Banner } from './banner.types'; export type { Complaint, ComplaintWithUser } from './complaint.types'; export type { Constant, ConstantUpdateResult } from './const.types'; export type { Coupon, ReservedCoupon, CouponValidationResult, UserMiniInfo } from './coupon.types'; +export type { Store } from './store.types'; +export type { StaffUser, StaffRole } from './staff-user.types'; diff --git a/packages/shared/types/staff-user.types.ts b/packages/shared/types/staff-user.types.ts new file mode 100644 index 0000000..6b7d20c --- /dev/null +++ b/packages/shared/types/staff-user.types.ts @@ -0,0 +1,17 @@ +/** + * Staff User Types + * Central type definitions for staff user-related data structures + */ + +export interface StaffUser { + id: number; + name: string; + password: string; + staffRoleId: number; + createdAt: Date; +} + +export interface StaffRole { + id: number; + roleName: string; +} diff --git a/packages/shared/types/store.types.ts b/packages/shared/types/store.types.ts new file mode 100644 index 0000000..65c2e0e --- /dev/null +++ b/packages/shared/types/store.types.ts @@ -0,0 +1,14 @@ +/** + * Store Types + * Central type definitions for store-related data structures + */ + +export interface Store { + id: number; + name: string; + description: string | null; + imageUrl: string | null; + owner: number; + createdAt: Date; + updatedAt: Date; +}