Compare commits

..

2 commits

Author SHA1 Message Date
shafi54
038733c14a enh 2026-03-25 09:39:53 +05:30
shafi54
d9652405ca enh 2026-03-25 01:43:02 +05:30
24 changed files with 2800 additions and 926 deletions

View file

@ -9,7 +9,132 @@ export { db } from 'postgresService';
export * from 'postgresService';
// Re-export methods from postgresService (implementation lives there)
export { getBanners, getBannerById, createBanner, updateBanner, deleteBanner } from 'postgresService';
export {
// Banner methods
getBanners,
getBannerById,
createBanner,
updateBanner,
deleteBanner,
// Complaint methods
getComplaints,
resolveComplaint,
// Constants methods
getAllConstants,
upsertConstants,
// Coupon methods (batch 1 - non-transaction)
getAllCoupons,
getCouponById,
invalidateCoupon,
validateCoupon,
getReservedCoupons,
getUsersForCoupon,
// Coupon methods (batch 2 - transactions)
createCouponWithRelations,
updateCouponWithRelations,
generateCancellationCoupon,
createReservedCouponWithProducts,
createCouponForUser,
checkUsersExist,
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)
export type { Banner } from './types/db.types';

View file

@ -1,9 +1,8 @@
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 { generateSignedUrlsFromS3Urls } from '@/src/lib/s3-client'
import { getComplaints as getComplaintsFromDb, resolveComplaint as resolveComplaintInDb } from '@/src/dbService'
import type { ComplaintWithUser } from '@packages/shared'
export const complaintRouter = router({
getAll: protectedProcedure
@ -11,7 +10,27 @@ export const complaintRouter = router({
cursor: z.number().optional(),
limit: z.number().default(20),
}))
.query(async ({ input }) => {
.query(async ({ input }): Promise<{
complaints: Array<{
id: number;
text: string;
userId: number;
userName: string | null;
userMobile: string | null;
orderId: number | null;
status: string;
createdAt: Date;
images: string[];
}>;
nextCursor?: number;
}> => {
const { cursor, limit } = input;
// Using dbService helper (new implementation)
const { complaints: complaintsData, hasMore } = await getComplaintsFromDb(cursor, limit);
/*
// Old implementation - direct DB query:
const { cursor, limit } = input;
let whereCondition = cursor
@ -37,10 +56,13 @@ export const complaintRouter = router({
.limit(limit + 1);
const hasMore = complaintsData.length > limit;
const complaintsToReturn = hasMore ? complaintsData.slice(0, limit) : complaintsData;
*/
const complaintsToReturn = hasMore ? complaintsData.slice(0, limit) : complaintsData;
const complaintsWithSignedImages = await Promise.all(
complaintsToReturn.map(async (c) => {
complaintsToReturn.map(async (c: ComplaintWithUser) => {
const signedImages = c.images
? await generateSignedUrlsFromS3Urls(c.images as string[])
: [];
@ -69,11 +91,17 @@ export const complaintRouter = router({
resolve: protectedProcedure
.input(z.object({ id: z.string(), response: z.string().optional() }))
.mutation(async ({ input }) => {
.mutation(async ({ input }): Promise<{ message: string }> => {
// Using dbService helper (new implementation)
await resolveComplaintInDb(parseInt(input.id), input.response);
/*
// Old implementation - direct DB query:
await db
.update(complaints)
.set({ isResolved: true, response: input.response })
.where(eq(complaints.id, parseInt(input.id)));
*/
return { message: 'Complaint resolved successfully' };
}),

View file

@ -1,22 +1,27 @@
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'
import { getAllConstants as getAllConstantsFromDb, upsertConstants as upsertConstantsInDb } from '@/src/dbService'
import type { Constant, ConstantUpdateResult } from '@packages/shared'
export const constRouter = router({
getConstants: protectedProcedure
.query(async () => {
.query(async (): Promise<Constant[]> => {
// Using dbService helper (new implementation)
const constants = await getAllConstantsFromDb();
/*
// Old implementation - direct DB query:
const constants = await db.select().from(keyValStore);
const resp = constants.map(c => ({
key: c.key,
value: c.value,
}));
*/
return resp;
return constants;
}),
updateConstants: protectedProcedure
@ -26,7 +31,7 @@ export const constRouter = router({
value: z.any(),
})),
}))
.mutation(async ({ input }) => {
.mutation(async ({ input }): Promise<ConstantUpdateResult> => {
const { constants } = input;
const validKeys = Object.values(CONST_KEYS) as string[];
@ -38,6 +43,11 @@ export const constRouter = router({
throw new Error(`Invalid constant keys: ${invalidKeys.join(', ')}`);
}
// Using dbService helper (new implementation)
await upsertConstantsInDb(constants);
/*
// Old implementation - direct DB query:
await db.transaction(async (tx) => {
for (const { key, value } of constants) {
await tx.insert(keyValStore)
@ -48,6 +58,7 @@ export const constRouter = router({
});
}
});
*/
// Refresh all constants in Redis after database update
await computeConstants();

View file

@ -1,9 +1,26 @@
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';
import {
// Batch 1 - Non-transaction methods
getAllCoupons as getAllCouponsFromDb,
getCouponById as getCouponByIdFromDb,
invalidateCoupon as invalidateCouponInDb,
validateCoupon as validateCouponInDb,
getReservedCoupons as getReservedCouponsFromDb,
getUsersForCoupon as getUsersForCouponFromDb,
// Batch 2 - Transaction methods
createCouponWithRelations,
updateCouponWithRelations,
generateCancellationCoupon,
createReservedCouponWithProducts,
createCouponForUser,
checkUsersExist,
checkCouponExists,
checkReservedCouponExists,
getOrderWithUser,
} from '@/src/dbService'
import type { Coupon, CouponValidationResult, UserMiniInfo } from '@packages/shared'
const createCouponBodySchema = z.object({
couponCode: z.string().optional(),
@ -31,7 +48,7 @@ const validateCouponBodySchema = z.object({
export const couponRouter = router({
create: protectedProcedure
.input(createCouponBodySchema)
.mutation(async ({ input, ctx }) => {
.mutation(async ({ input, ctx }): Promise<Coupon> => {
const { couponCode, isUserBased, discountPercent, flatDiscount, minOrder, productIds, applicableUsers, applicableProducts, maxValue, isApplyForAll, validTill, maxLimitForUser, exclusiveApply } = input;
// Validation: ensure at least one discount type is provided
@ -49,17 +66,6 @@ export const couponRouter = router({
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) {
@ -69,22 +75,27 @@ export const couponRouter = router({
// 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) {
// Using dbService helper (new implementation)
const codeExists = await checkCouponExists(finalCouponCode);
if (codeExists) {
throw new Error("Coupon code already exists");
}
const result = await db.insert(coupons).values({
// If applicableUsers is provided, verify users exist
if (applicableUsers && applicableUsers.length > 0) {
const usersExist = await checkUsersExist(applicableUsers);
if (!usersExist) {
throw new Error("Some applicable users not found");
}
}
const coupon = await createCouponWithRelations(
{
couponCode: finalCouponCode,
isUserBased: isUserBased || false,
discountPercent: discountPercent?.toString(),
@ -95,9 +106,29 @@ export const couponRouter = router({
maxValue: maxValue?.toString(),
isApplyForAll: isApplyForAll || false,
validTill: validTill ? dayjs(validTill).toDate() : undefined,
maxLimitForUser: maxLimitForUser,
maxLimitForUser,
exclusiveApply: exclusiveApply || false,
}).returning();
},
applicableUsers,
applicableProducts
);
/*
// Old implementation - direct DB query with transaction:
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,
exclusiveApply: exclusiveApply || false,
}).returning();
const coupon = result[0];
@ -120,6 +151,7 @@ export const couponRouter = router({
}))
);
}
*/
return coupon;
}),
@ -130,71 +162,22 @@ export const couponRouter = router({
limit: z.number().default(50),
search: z.string().optional(),
}))
.query(async ({ input }) => {
.query(async ({ input }): Promise<{ coupons: any[]; nextCursor?: number }> => {
const { cursor, limit, search } = input;
let whereCondition = undefined;
const conditions = [];
const { coupons: couponsList, hasMore } = await getAllCouponsFromDb(cursor, limit, search);
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;
const nextCursor = hasMore ? couponsList[couponsList.length - 1].id : undefined;
return { coupons: couponsList, nextCursor };
}),
getById: protectedProcedure
.input(z.object({ id: z.number() }))
.query(async ({ input }) => {
.query(async ({ input }): Promise<any> => {
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,
},
},
},
});
const result = await getCouponByIdFromDb(couponId);
if (!result) {
throw new Error("Coupon not found");
@ -203,8 +186,8 @@ export const couponRouter = router({
return {
...result,
productIds: (result.productIds as number[]) || undefined,
applicableUsers: result.applicableUsers.map(au => au.user),
applicableProducts: result.applicableProducts.map(ap => ap.product),
applicableUsers: result.applicableUsers.map((au: any) => au.user),
applicableProducts: result.applicableProducts.map((ap: any) => ap.product),
};
}),
@ -215,7 +198,7 @@ export const couponRouter = router({
isInvalidated: z.boolean().optional(),
}),
}))
.mutation(async ({ input }) => {
.mutation(async ({ input }): Promise<Coupon> => {
const { id, updates } = input;
// Validation: ensure discount types are valid
@ -225,43 +208,31 @@ export const couponRouter = router({
}
}
// 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");
}
}
// Prepare update data
const updateData: any = {};
if (updates.couponCode !== undefined) updateData.couponCode = updates.couponCode;
if (updates.isUserBased !== undefined) updateData.isUserBased = updates.isUserBased;
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.isApplyForAll !== undefined) updateData.isApplyForAll = updates.isApplyForAll;
if (updates.validTill !== undefined) updateData.validTill = updates.validTill ? dayjs(updates.validTill).toDate() : null;
if (updates.maxLimitForUser !== undefined) updateData.maxLimitForUser = updates.maxLimitForUser;
if (updates.exclusiveApply !== undefined) updateData.exclusiveApply = updates.exclusiveApply;
if (updates.isInvalidated !== undefined) updateData.isInvalidated = updates.isInvalidated;
if (updates.productIds !== undefined) updateData.productIds = updates.productIds;
// 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;
}
// Using dbService helper (new implementation)
const coupon = await updateCouponWithRelations(
id,
updateData,
updates.applicableUsers,
updates.applicableProducts
);
/*
// Old implementation - direct DB query:
const result = await db.update(coupons)
.set(updateData)
.where(eq(coupons.id, id))
@ -271,8 +242,6 @@ export const couponRouter = router({
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));
@ -298,246 +267,43 @@ export const couponRouter = router({
);
}
}
*/
return result[0];
return coupon;
}),
delete: protectedProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input }) => {
.mutation(async ({ input }): Promise<{ message: string }> => {
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");
}
await invalidateCouponInDb(id);
return { message: "Coupon invalidated successfully" };
}),
validate: protectedProcedure
.input(validateCouponBodySchema)
.query(async ({ input }) => {
.query(async ({ input }): Promise<CouponValidationResult> => {
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)
),
});
const result = await validateCouponInDb(code, userId, orderAmount);
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,
};
return result;
}),
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
generateCancellationCoupon: protectedProcedure
.input(
z.object({
orderId: z.number(),
})
)
.mutation(async ({ input, ctx }): Promise<Coupon> => {
const { orderId } = input;
// Get staff user ID from auth middleware
const staffUserId = ctx.staffUser?.id;
@ -545,21 +311,144 @@ export const couponRouter = router({
throw new Error("Unauthorized");
}
// Generate secret code if not provided (use couponCode as base)
// Using dbService helper (new implementation)
const order = await getOrderWithUser(orderId);
if (!order) {
throw new Error("Order not found");
}
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 codeExists = await checkCouponExists(couponCode);
if (codeExists) {
throw new Error("Coupon code already exists");
}
// Get order total amount
const orderAmount = parseFloat(order.totalAmount);
const coupon = await generateCancellationCoupon(
orderId,
staffUserId,
order.userId,
orderAmount,
couponCode
);
/*
// Old implementation - direct DB query with transaction:
const coupon = await db.transaction(async (tx) => {
// Calculate expiry date (30 days from now)
const expiryDate = new Date();
expiryDate.setDate(expiryDate.getDate() + 30);
// 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 }): Promise<{ coupons: any[]; nextCursor?: number }> => {
const { cursor, limit, search } = input;
const { coupons: result, hasMore } = await getReservedCouponsFromDb(cursor, limit, search);
const nextCursor = hasMore ? result[result.length - 1].id : undefined;
return {
coupons: result,
nextCursor,
};
}),
createReservedCoupon: protectedProcedure
.input(createCouponBodySchema)
.mutation(async ({ input, ctx }): Promise<any> => {
const { couponCode, isUserBased, discountPercent, flatDiscount, minOrder, productIds, applicableProducts, maxValue, 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)");
}
// Get staff user ID from auth middleware
const staffUserId = ctx.staffUser?.id;
if (!staffUserId) {
throw new Error("Unauthorized");
}
// Generate secret code if not provided
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) {
// Using dbService helper (new implementation)
const codeExists = await checkReservedCouponExists(secretCode);
if (codeExists) {
throw new Error("Secret code already exists");
}
const coupon = await createReservedCouponWithProducts(
{
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,
},
applicableProducts
);
/*
// Old implementation - direct DB query:
const result = await db.insert(reservedCoupons).values({
secretCode,
couponCode: couponCode || `RESERVED${Date.now().toString().slice(-6)}`,
couponCode: couponCode || RESERVED${Date.now().toString().slice(-6)},
discountPercent: discountPercent?.toString(),
flatDiscount: flatDiscount?.toString(),
minOrder: minOrder?.toString(),
@ -582,6 +471,7 @@ export const couponRouter = router({
}))
);
}
*/
return coupon;
}),
@ -592,120 +482,97 @@ export const couponRouter = router({
limit: z.number().min(1).max(50).default(20),
offset: z.number().min(0).default(0),
}))
.query(async ({ input }) => {
const { search, limit } = input;
.query(async ({ input }): Promise<{ users: UserMiniInfo[] }> => {
const { search, limit, offset } = input;
let whereCondition = undefined;
if (search && search.trim()) {
whereCondition = or(
like(users.name, `%${search}%`),
like(users.mobile, `%${search}%`)
);
}
const result = await getUsersForCouponFromDb(search, limit, offset);
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,
}))
};
return result;
}),
createCoupon: protectedProcedure
.input(z.object({
mobile: z.string().min(1, 'Mobile number is required'),
}))
.mutation(async ({ input, ctx }) => {
const { mobile } = input;
createCoupon: protectedProcedure
.input(z.object({
mobile: z.string().min(1, 'Mobile number is required'),
}))
.mutation(async ({ input, ctx }): Promise<{ success: boolean; coupon: any }> => {
const { mobile } = input;
// Get staff user ID from auth middleware
const staffUserId = ctx.staffUser?.id;
if (!staffUserId) {
throw new Error("Unauthorized");
}
// 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, '');
// 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");
}
// 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),
});
// 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}`;
if (!user) {
// Create new user
const [newUser] = await db.insert(users).values({
name: null,
email: null,
mobile: cleanMobile,
}).returning();
user = newUser;
}
// Using dbService helper (new implementation)
const codeExists = await checkCouponExists(couponCode);
if (codeExists) {
throw new Error("Generated coupon code already exists - please try again");
}
// 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}`;
const { coupon, user } = await createCouponForUser(cleanMobile, couponCode, staffUserId);
// Check if coupon code already exists (very unlikely but safe)
const existingCode = await db.query.coupons.findFirst({
where: eq(coupons.couponCode, couponCode),
});
/*
// Old implementation - direct DB query with transaction:
// Check if user exists, create if not
let user = await db.query.users.findFirst({
where: eq(users.mobile, cleanMobile),
});
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
if (!user) {
const [newUser] = await db.insert(users).values({
name: null,
email: null,
mobile: cleanMobile,
}).returning();
user = newUser;
}
// Associate coupon with user
await db.insert(couponApplicableUsers).values({
couponId: coupon.id,
// Create the coupon
const [coupon] = await db.insert(coupons).values({
couponCode,
isUserBased: true,
discountPercent: "20",
minOrder: "1000",
maxValue: "500",
maxLimitForUser: 1,
isApplyForAll: false,
exclusiveApply: false,
createdBy: staffUserId,
validTill: dayjs().add(90, 'days').toDate(),
}).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,
});
return {
success: true,
coupon: {
id: coupon.id,
couponCode: coupon.couponCode,
userId: user.id,
userMobile: user.mobile,
discountPercent: 20,
minOrder: 1000,
maxValue: 500,
maxLimitForUser: 1,
},
};
}),
userMobile: user.mobile,
discountPercent: 20,
minOrder: 1000,
maxValue: 500,
maxLimitForUser: 1,
},
};
}),
});

View file

@ -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,26 +162,17 @@ 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,
})),

View file

@ -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;
}),
});

View file

@ -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<typeof users.$inferSelect> {
// 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,71 +72,14 @@ 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 }[] = [];
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`, `)})`);
}
const orderCounts = await getOrderCountsByUserIds(userIds);
const lastOrders = await getLastOrdersByUserIds(userIds);
const suspensionStatuses = await getSuspensionStatusesByUserIds(userIds);
// Create lookup maps
const orderCountMap = new Map(orderCounts.map(o => [o.userId, o.totalOrders]));
@ -144,7 +87,7 @@ export const userRouter = {
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);

View file

@ -9,3 +9,13 @@ export * from './src/db/schema';
// Re-export helper methods
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';

View file

@ -0,0 +1,74 @@
import { db } from '../db/db_index';
import { complaints, users } from '../db/schema';
import { eq, desc, lt } from 'drizzle-orm';
export interface Complaint {
id: number;
complaintBody: string;
userId: number;
orderId: number | null;
isResolved: boolean;
response: string | null;
createdAt: Date;
images: string[] | null;
}
export interface ComplaintWithUser extends Complaint {
userName: string | null;
userMobile: string | null;
}
export async function getComplaints(
cursor?: number,
limit: number = 20
): Promise<{ complaints: ComplaintWithUser[]; hasMore: boolean }> {
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,
response: complaints.response,
createdAt: complaints.createdAt,
images: complaints.images,
userName: users.name,
userMobile: users.mobile,
})
.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;
return {
complaints: complaintsToReturn.map((c) => ({
id: c.id,
complaintBody: c.complaintBody,
userId: c.userId,
orderId: c.orderId,
isResolved: c.isResolved,
response: c.response,
createdAt: c.createdAt,
images: c.images,
userName: c.userName,
userMobile: c.userMobile,
})),
hasMore,
};
}
export async function resolveComplaint(
id: number,
response?: string
): Promise<void> {
await db
.update(complaints)
.set({ isResolved: true, response })
.where(eq(complaints.id, id));
}

View file

@ -0,0 +1,29 @@
import { db } from '../db/db_index';
import { keyValStore } from '../db/schema';
export interface Constant {
key: string;
value: any;
}
export async function getAllConstants(): Promise<Constant[]> {
const constants = await db.select().from(keyValStore);
return constants.map(c => ({
key: c.key,
value: c.value,
}));
}
export async function upsertConstants(constants: Constant[]): Promise<void> {
await db.transaction(async (tx) => {
for (const { key, value } of constants) {
await tx.insert(keyValStore)
.values({ key, value })
.onConflictDoUpdate({
target: keyValStore.key,
set: { value },
});
}
});
}

View file

@ -0,0 +1,633 @@
import { db } from '../db/db_index';
import { coupons, reservedCoupons, users } from '../db/schema';
import { eq, and, like, or, inArray, lt, desc } from 'drizzle-orm';
export interface Coupon {
id: number;
couponCode: string;
isUserBased: boolean;
discountPercent: string | null;
flatDiscount: string | null;
minOrder: string | null;
productIds: number[] | null;
maxValue: string | null;
isApplyForAll: boolean;
validTill: Date | null;
maxLimitForUser: number | null;
exclusiveApply: boolean;
isInvalidated: boolean;
createdAt: Date;
createdBy: number;
}
export async function getAllCoupons(
cursor?: number,
limit: number = 50,
search?: string
): Promise<{ coupons: any[]; hasMore: boolean }> {
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;
return { coupons: couponsList, hasMore };
}
export async function getCouponById(id: number): Promise<any | null> {
const result = await db.query.coupons.findFirst({
where: eq(coupons.id, id),
with: {
creator: true,
applicableUsers: {
with: {
user: true,
},
},
applicableProducts: {
with: {
product: true,
},
},
},
});
return result || null;
}
export async function invalidateCoupon(id: number): Promise<Coupon> {
const result = await db.update(coupons)
.set({ isInvalidated: true })
.where(eq(coupons.id, id))
.returning();
return result[0];
}
export interface CouponValidationResult {
valid: boolean;
message?: string;
discountAmount?: number;
coupon?: Partial<Coupon>;
}
export async function validateCoupon(
code: string,
userId: number,
orderAmount: number
): Promise<CouponValidationResult> {
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,
}
};
}
export async function getReservedCoupons(
cursor?: number,
limit: number = 50,
search?: string
): Promise<{ coupons: any[]; hasMore: boolean }> {
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,
});
const hasMore = result.length > limit;
const couponsList = hasMore ? result.slice(0, limit) : result;
return { coupons: couponsList, hasMore };
}
export interface UserMiniInfo {
id: number;
name: string;
mobile: string | null;
}
export async function getUsersForCoupon(
search?: string,
limit: number = 20,
offset: number = 0
): Promise<{ users: UserMiniInfo[] }> {
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: offset,
orderBy: (users, { asc }) => [asc(users.name)],
});
return {
users: userList.map(user => ({
id: user.id,
name: user.name || 'Unknown',
mobile: user.mobile,
}))
};
}
// ============================================================================
// BATCH 2: Transaction Methods
// ============================================================================
import { couponApplicableUsers, couponApplicableProducts, orders, orderStatus } from '../db/schema';
export interface CreateCouponInput {
couponCode: string;
isUserBased: boolean;
discountPercent?: string;
flatDiscount?: string;
minOrder?: string;
productIds?: number[] | null;
maxValue?: string;
isApplyForAll: boolean;
validTill?: Date;
maxLimitForUser?: number;
exclusiveApply: boolean;
createdBy: number;
}
export async function createCouponWithRelations(
input: CreateCouponInput,
applicableUsers?: number[],
applicableProducts?: number[]
): Promise<Coupon> {
return await db.transaction(async (tx) => {
// Create the coupon
const [coupon] = await tx.insert(coupons).values({
couponCode: input.couponCode,
isUserBased: input.isUserBased,
discountPercent: input.discountPercent,
flatDiscount: input.flatDiscount,
minOrder: input.minOrder,
productIds: input.productIds,
createdBy: input.createdBy,
maxValue: input.maxValue,
isApplyForAll: input.isApplyForAll,
validTill: input.validTill,
maxLimitForUser: input.maxLimitForUser,
exclusiveApply: input.exclusiveApply,
}).returning();
// Insert applicable users
if (applicableUsers && applicableUsers.length > 0) {
await tx.insert(couponApplicableUsers).values(
applicableUsers.map(userId => ({
couponId: coupon.id,
userId,
}))
);
}
// Insert applicable products
if (applicableProducts && applicableProducts.length > 0) {
await tx.insert(couponApplicableProducts).values(
applicableProducts.map(productId => ({
couponId: coupon.id,
productId,
}))
);
}
return {
id: coupon.id,
couponCode: coupon.couponCode,
isUserBased: coupon.isUserBased,
discountPercent: coupon.discountPercent,
flatDiscount: coupon.flatDiscount,
minOrder: coupon.minOrder,
productIds: coupon.productIds,
maxValue: coupon.maxValue,
isApplyForAll: coupon.isApplyForAll,
validTill: coupon.validTill,
maxLimitForUser: coupon.maxLimitForUser,
exclusiveApply: coupon.exclusiveApply,
isInvalidated: coupon.isInvalidated,
createdAt: coupon.createdAt,
createdBy: coupon.createdBy,
};
});
}
export interface UpdateCouponInput {
couponCode?: string;
isUserBased?: boolean;
discountPercent?: string;
flatDiscount?: string;
minOrder?: string;
productIds?: number[] | null;
maxValue?: string;
isApplyForAll?: boolean;
validTill?: Date | null;
maxLimitForUser?: number;
exclusiveApply?: boolean;
isInvalidated?: boolean;
}
export async function updateCouponWithRelations(
id: number,
input: UpdateCouponInput,
applicableUsers?: number[],
applicableProducts?: number[]
): Promise<Coupon> {
return await db.transaction(async (tx) => {
// Update the coupon
const [coupon] = await tx.update(coupons)
.set({
...input,
lastUpdated: new Date(),
})
.where(eq(coupons.id, id))
.returning();
// Update applicable users: delete existing and insert new
if (applicableUsers !== undefined) {
await tx.delete(couponApplicableUsers).where(eq(couponApplicableUsers.couponId, id));
if (applicableUsers.length > 0) {
await tx.insert(couponApplicableUsers).values(
applicableUsers.map(userId => ({
couponId: id,
userId,
}))
);
}
}
// Update applicable products: delete existing and insert new
if (applicableProducts !== undefined) {
await tx.delete(couponApplicableProducts).where(eq(couponApplicableProducts.couponId, id));
if (applicableProducts.length > 0) {
await tx.insert(couponApplicableProducts).values(
applicableProducts.map(productId => ({
couponId: id,
productId,
}))
);
}
}
return {
id: coupon.id,
couponCode: coupon.couponCode,
isUserBased: coupon.isUserBased,
discountPercent: coupon.discountPercent,
flatDiscount: coupon.flatDiscount,
minOrder: coupon.minOrder,
productIds: coupon.productIds,
maxValue: coupon.maxValue,
isApplyForAll: coupon.isApplyForAll,
validTill: coupon.validTill,
maxLimitForUser: coupon.maxLimitForUser,
exclusiveApply: coupon.exclusiveApply,
isInvalidated: coupon.isInvalidated,
createdAt: coupon.createdAt,
createdBy: coupon.createdBy,
};
});
}
export async function generateCancellationCoupon(
orderId: number,
staffUserId: number,
userId: number,
orderAmount: number,
couponCode: string
): Promise<Coupon> {
return await db.transaction(async (tx) => {
// Calculate expiry date (30 days from now)
const expiryDate = new Date();
expiryDate.setDate(expiryDate.getDate() + 30);
// Create the coupon
const [coupon] = 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();
// Insert applicable users
await tx.insert(couponApplicableUsers).values({
couponId: coupon.id,
userId,
});
// Update order_status with refund coupon ID
await tx.update(orderStatus)
.set({ refundCouponId: coupon.id })
.where(eq(orderStatus.orderId, orderId));
return {
id: coupon.id,
couponCode: coupon.couponCode,
isUserBased: coupon.isUserBased,
discountPercent: coupon.discountPercent,
flatDiscount: coupon.flatDiscount,
minOrder: coupon.minOrder,
productIds: coupon.productIds,
maxValue: coupon.maxValue,
isApplyForAll: coupon.isApplyForAll,
validTill: coupon.validTill,
maxLimitForUser: coupon.maxLimitForUser,
exclusiveApply: coupon.exclusiveApply,
isInvalidated: coupon.isInvalidated,
createdAt: coupon.createdAt,
createdBy: coupon.createdBy,
};
});
}
export interface CreateReservedCouponInput {
secretCode: string;
couponCode: string;
discountPercent?: string;
flatDiscount?: string;
minOrder?: string;
productIds?: number[] | null;
maxValue?: string;
validTill?: Date;
maxLimitForUser?: number;
exclusiveApply: boolean;
createdBy: number;
}
export async function createReservedCouponWithProducts(
input: CreateReservedCouponInput,
applicableProducts?: number[]
): Promise<any> {
return await db.transaction(async (tx) => {
const [coupon] = await tx.insert(reservedCoupons).values({
secretCode: input.secretCode,
couponCode: input.couponCode,
discountPercent: input.discountPercent,
flatDiscount: input.flatDiscount,
minOrder: input.minOrder,
productIds: input.productIds,
maxValue: input.maxValue,
validTill: input.validTill,
maxLimitForUser: input.maxLimitForUser,
exclusiveApply: input.exclusiveApply,
createdBy: input.createdBy,
}).returning();
// Insert applicable products if provided
if (applicableProducts && applicableProducts.length > 0) {
await tx.insert(couponApplicableProducts).values(
applicableProducts.map(productId => ({
couponId: coupon.id,
productId,
}))
);
}
return coupon;
});
}
export async function getOrCreateUserByMobile(
mobile: string
): Promise<{ id: number; mobile: string; name: string | null }> {
return await db.transaction(async (tx) => {
// Check if user exists
let user = await tx.query.users.findFirst({
where: eq(users.mobile, mobile),
});
if (!user) {
// Create new user
const [newUser] = await tx.insert(users).values({
name: null,
email: null,
mobile,
}).returning();
user = newUser;
}
return {
id: user.id,
mobile: user.mobile,
name: user.name,
};
});
}
export async function createCouponForUser(
mobile: string,
couponCode: string,
staffUserId: number
): Promise<{ coupon: Coupon; user: { id: number; mobile: string; name: string | null } }> {
return await db.transaction(async (tx) => {
// Get or create user
let user = await tx.query.users.findFirst({
where: eq(users.mobile, mobile),
});
if (!user) {
const [newUser] = await tx.insert(users).values({
name: null,
email: null,
mobile,
}).returning();
user = newUser;
}
// Create the coupon
const [coupon] = await tx.insert(coupons).values({
couponCode,
isUserBased: true,
discountPercent: "20",
minOrder: "1000",
maxValue: "500",
maxLimitForUser: 1,
isApplyForAll: false,
exclusiveApply: false,
createdBy: staffUserId,
validTill: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000), // 90 days from now
}).returning();
// Associate coupon with user
await tx.insert(couponApplicableUsers).values({
couponId: coupon.id,
userId: user.id,
});
return {
coupon: {
id: coupon.id,
couponCode: coupon.couponCode,
isUserBased: coupon.isUserBased,
discountPercent: coupon.discountPercent,
flatDiscount: coupon.flatDiscount,
minOrder: coupon.minOrder,
productIds: coupon.productIds,
maxValue: coupon.maxValue,
isApplyForAll: coupon.isApplyForAll,
validTill: coupon.validTill,
maxLimitForUser: coupon.maxLimitForUser,
exclusiveApply: coupon.exclusiveApply,
isInvalidated: coupon.isInvalidated,
createdAt: coupon.createdAt,
createdBy: coupon.createdBy,
},
user: {
id: user.id,
mobile: user.mobile,
name: user.name,
},
};
});
}
// ============================================================================
// Utility Functions
// ============================================================================
export async function checkUsersExist(userIds: number[]): Promise<boolean> {
const existingUsers = await db.query.users.findMany({
where: inArray(users.id, userIds),
columns: { id: true },
});
return existingUsers.length === userIds.length;
}
export async function checkCouponExists(couponCode: string): Promise<boolean> {
const existing = await db.query.coupons.findFirst({
where: eq(coupons.couponCode, couponCode),
});
return !!existing;
}
export async function checkReservedCouponExists(secretCode: string): Promise<boolean> {
const existing = await db.query.reservedCoupons.findFirst({
where: eq(reservedCoupons.secretCode, secretCode),
});
return !!existing;
}
export async function getOrderWithUser(orderId: number): Promise<any | null> {
return await db.query.orders.findFirst({
where: eq(orders.id, orderId),
with: {
user: true,
},
});
}

View file

@ -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<any> {
const [result] = await db
.update(orders)
.set({ adminNotes })
.where(eq(orders.id, orderId))
.returning();
return result;
}
export async function getOrderWithDetails(orderId: number): Promise<any | null> {
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<any | null> {
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<any | null> {
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<any[]> {
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<any> {
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<any> {
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<void> {
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<void> {
await db.update(addresses)
.set({ lat, lng })
.where(eq(addresses.id, addressId));
}
export async function getOrderStatus(orderId: number): Promise<any | null> {
return await db.query.orderStatus.findFirst({
where: eq(orderStatus.orderId, orderId),
});
}
export async function cancelOrder(orderId: number, reason: string): Promise<any> {
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<any[]> {
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<any> {
const [result] = await db
.update(orders)
.set({ deliveryCharge: '0' })
.where(eq(orders.id, orderId))
.returning();
return result;
}

View file

@ -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<any[]> {
return await db.query.productInfo.findMany({
orderBy: productInfo.name,
with: {
unit: true,
store: true,
},
});
}
export async function getProductById(id: number): Promise<any | null> {
return await db.query.productInfo.findFirst({
where: eq(productInfo.id, id),
with: {
unit: true,
store: true,
productSlots: {
with: {
slot: true,
},
},
specialDeals: true,
productTags: {
with: {
tag: true,
},
},
},
});
}
export async function createProduct(input: any): Promise<any> {
const [product] = await db.insert(productInfo).values(input).returning();
return product;
}
export async function updateProduct(id: number, updates: any): Promise<any> {
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<any> {
const [product] = await db.update(productInfo)
.set({ isOutOfStock })
.where(eq(productInfo.id, id))
.returning();
return product;
}
export async function getAllUnits(): Promise<any[]> {
return await db.query.units.findMany({
orderBy: units.name,
});
}
export async function getAllProductTags(): Promise<any[]> {
return await db.query.productTags.findMany({
with: {
products: {
with: {
product: true,
},
},
},
});
}
export async function getProductReviews(productId: number): Promise<any[]> {
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<void> {
await db.update(productReviews)
.set({ adminResponse })
.where(eq(productReviews.id, reviewId));
}
export async function getAllProductGroups(): Promise<any[]> {
return await db.query.productGroupInfo.findMany({
with: {
products: {
with: {
product: true,
},
},
},
});
}
export async function createProductGroup(name: string): Promise<any> {
const [group] = await db.insert(productGroupInfo).values({ name }).returning();
return group;
}
export async function updateProductGroup(id: number, name: string): Promise<any> {
const [group] = await db.update(productGroupInfo)
.set({ name })
.where(eq(productGroupInfo.id, id))
.returning();
return group;
}
export async function deleteProductGroup(id: number): Promise<void> {
await db.delete(productGroupInfo).where(eq(productGroupInfo.id, id));
}
export async function addProductToGroup(groupId: number, productId: number): Promise<void> {
await db.insert(productGroupMembership).values({ groupId, productId });
}
export async function removeProductFromGroup(groupId: number, productId: number): Promise<void> {
await db.delete(productGroupMembership)
.where(and(
eq(productGroupMembership.groupId, groupId),
eq(productGroupMembership.productId, productId)
));
}

View file

@ -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<any[]> {
return await db.query.deliverySlotInfo.findMany({
orderBy: desc(deliverySlotInfo.createdAt),
with: {
productSlots: {
with: {
product: true,
},
},
vendorSnippets: true,
},
});
}
export async function getSlotById(id: number): Promise<any | null> {
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<any> {
const [slot] = await db.insert(deliverySlotInfo).values(input).returning();
return slot;
}
export async function updateSlot(id: number, updates: any): Promise<any> {
const [slot] = await db.update(deliverySlotInfo)
.set(updates)
.where(eq(deliverySlotInfo.id, id))
.returning();
return slot;
}
export async function deleteSlot(id: number): Promise<void> {
await db.delete(deliverySlotInfo).where(eq(deliverySlotInfo.id, id));
}
export async function getSlotProducts(slotId: number): Promise<any[]> {
return await db.query.productSlots.findMany({
where: eq(productSlots.slotId, slotId),
with: {
product: true,
},
});
}
export async function addProductToSlot(slotId: number, productId: number): Promise<void> {
await db.insert(productSlots).values({ slotId, productId });
}
export async function removeProductFromSlot(slotId: number, productId: number): Promise<void> {
await db.delete(productSlots)
.where(and(
eq(productSlots.slotId, slotId),
eq(productSlots.productId, productId)
));
}
export async function clearSlotProducts(slotId: number): Promise<void> {
await db.delete(productSlots).where(eq(productSlots.slotId, slotId));
}
export async function updateSlotCapacity(slotId: number, maxCapacity: number): Promise<any> {
const [slot] = await db.update(deliverySlotInfo)
.set({ maxCapacity })
.where(eq(deliverySlotInfo.id, slotId))
.returning();
return slot;
}
export async function getSlotDeliverySequence(slotId: number): Promise<any | null> {
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<void> {
await db.update(deliverySlotInfo)
.set({ deliverySequence: sequence })
.where(eq(deliverySlotInfo.id, slotId));
}

View file

@ -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<StaffUser | null> {
const staff = await db.query.staffUsers.findFirst({
where: eq(staffUsers.name, name),
});
return staff || null;
}
export async function getAllStaff(): Promise<any[]> {
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<StaffUser | null> {
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<any | null> {
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<void> {
await db
.insert(userDetails)
.values({ userId, isSuspended })
.onConflictDoUpdate({
target: userDetails.userId,
set: { isSuspended },
});
}
export async function checkStaffUserExists(name: string): Promise<boolean> {
const existingUser = await db.query.staffUsers.findFirst({
where: eq(staffUsers.name, name),
});
return !!existingUser;
}
export async function checkStaffRoleExists(roleId: number): Promise<boolean> {
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<StaffUser> {
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<any[]> {
const roles = await db.query.staffRoles.findMany({
columns: {
id: true,
roleName: true,
},
});
return roles;
}

View file

@ -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<any[]> {
const stores = await db.query.storeInfo.findMany({
with: {
owner: true,
},
});
return stores;
}
export async function getStoreById(id: number): Promise<any | null> {
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<Store> {
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<Store> {
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",
};
});
}

View file

@ -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<any> {
const [newUser] = await db
.insert(users)
.values({
name: null,
email: null,
mobile,
})
.returning();
return newUser;
}
export async function getUserByMobile(mobile: string): Promise<any | null> {
const [existingUser] = await db
.select()
.from(users)
.where(eq(users.mobile, mobile))
.limit(1);
return existingUser || null;
}
export async function getUnresolvedComplaintsCount(): Promise<number> {
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<any | null> {
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<boolean> {
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<any[]> {
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<void> {
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<any[]> {
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<any[]> {
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<any> {
const [incident] = await db.insert(userIncidents)
.values({
userId,
orderId,
adminComment,
addedBy: adminUserId,
negativityScore,
})
.returning();
return incident;
}

View file

@ -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<boolean> {
const existingSnippet = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.snippetCode, snippetCode),
});
return !!existingSnippet;
}
export async function getVendorSnippetById(id: number): Promise<any | null> {
return await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.id, id),
with: {
slot: true,
},
});
}
export async function getVendorSnippetByCode(snippetCode: string): Promise<any | null> {
return await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.snippetCode, snippetCode),
});
}
export async function getAllVendorSnippets(): Promise<any[]> {
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<any> {
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<any> {
const [result] = await db.update(vendorSnippets)
.set(updates)
.where(eq(vendorSnippets.id, id))
.returning();
return result;
}
export async function deleteVendorSnippet(id: number): Promise<void> {
await db.delete(vendorSnippets)
.where(eq(vendorSnippets.id, id));
}
export async function getProductsByIds(productIds: number[]): Promise<any[]> {
return await db.query.productInfo.findMany({
where: inArray(productInfo.id, productIds),
columns: { id: true, name: true },
});
}
export async function getVendorSlotById(slotId: number): Promise<any | null> {
return await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, slotId),
});
}
export async function getVendorOrdersBySlotId(slotId: number): Promise<any[]> {
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<any[]> {
return await db.query.orderItems.findMany({
where: inArray(orderItems.orderId, orderIds),
with: {
product: {
with: {
unit: true,
},
},
},
});
}
export async function getOrderStatusByOrderIds(orderIds: number[]): Promise<any[]> {
return await db.query.orderStatus.findMany({
where: inArray(orderStatus.orderId, orderIds),
});
}
export async function updateVendorOrderItemPackaging(orderItemId: number, isPackaged: boolean, isPackageVerified: boolean): Promise<void> {
await db.update(orderItems)
.set({
is_packaged: isPackaged,
is_package_verified: isPackageVerified,
})
.where(eq(orderItems.id, orderItemId));
}

View file

@ -0,0 +1,20 @@
/**
* Complaint Types
* Central type definitions for complaint-related data structures
*/
export interface Complaint {
id: number;
complaintBody: string;
userId: number;
orderId: number | null;
isResolved: boolean;
response: string | null;
createdAt: Date;
images: string[] | null;
}
export interface ComplaintWithUser extends Complaint {
userName: string | null;
userMobile: string | null;
}

View file

@ -0,0 +1,15 @@
/**
* Constants Types
* Central type definitions for key-value store constants
*/
export interface Constant {
key: string;
value: any;
}
export interface ConstantUpdateResult {
success: boolean;
updatedCount: number;
keys: string[];
}

View file

@ -0,0 +1,41 @@
/**
* Coupon Types
* Central type definitions for coupon-related data structures
*/
export interface Coupon {
id: number;
couponCode: string;
isUserBased: boolean;
discountPercent: string | null;
flatDiscount: string | null;
minOrder: string | null;
productIds: number[] | null;
maxValue: string | null;
isApplyForAll: boolean;
validTill: Date | null;
maxLimitForUser: number | null;
exclusiveApply: boolean;
isInvalidated: boolean;
createdAt: Date;
createdBy: number;
}
export interface ReservedCoupon extends Coupon {
secretCode: string;
redeemedUserId: number | null;
redeemedAt: Date | null;
}
export interface CouponValidationResult {
valid: boolean;
message?: string;
discountAmount?: number;
coupon?: Partial<Coupon>;
}
export interface UserMiniInfo {
id: number;
name: string;
mobile: string | null;
}

View file

@ -2,3 +2,8 @@
// Re-export all types from the types folder
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';

View file

@ -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;
}

View file

@ -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;
}