This commit is contained in:
shafi54 2026-03-26 12:07:49 +05:30
parent 9137b5e1e6
commit 89de986764
39 changed files with 1947 additions and 1221 deletions

View file

@ -1,12 +1,67 @@
import { eq, gt, and, sql, inArray } from "drizzle-orm";
import { Request, Response } from "express"; import { Request, Response } from "express";
import { db } from "@/src/db/db_index"
import { productInfo, units, productSlots, deliverySlotInfo, productTags } from "@/src/db/schema"
import { scaffoldAssetUrl } from "@/src/lib/s3-client" import { scaffoldAssetUrl } from "@/src/lib/s3-client"
import { getNextDeliveryDate } from "@/src/trpc/apis/common-apis/common"
import {
getAllProductsWithUnits,
type ProductSummaryData,
} from "@/src/dbService"
/** /**
* Get next delivery date for a product * Get all products summary for dropdown
*/ */
export const getAllProductsSummary = async (req: Request, res: Response) => {
try {
const { tagId } = req.query;
const tagIdNum = tagId ? parseInt(tagId as string) : undefined;
// If tagId is provided but no products found, return empty array
if (tagIdNum) {
const products = await getAllProductsWithUnits(tagIdNum);
if (products.length === 0) {
return res.status(200).json({
products: [],
count: 0,
});
}
}
const productsWithUnits = await getAllProductsWithUnits(tagIdNum);
// Generate signed URLs for product images
const formattedProducts = await Promise.all(
productsWithUnits.map(async (product: ProductSummaryData) => {
const nextDeliveryDate = await getNextDeliveryDate(product.id);
return {
id: product.id,
name: product.name,
shortDescription: product.shortDescription,
price: product.price,
marketPrice: product.marketPrice,
unit: product.unitShortNotation,
productQuantity: product.productQuantity,
isOutOfStock: product.isOutOfStock,
nextDeliveryDate: nextDeliveryDate ? nextDeliveryDate.toISOString() : null,
images: scaffoldAssetUrl((product.images as string[]) || []),
};
})
);
return res.status(200).json({
products: formattedProducts,
count: formattedProducts.length,
});
} catch (error) {
console.error("Get products summary error:", error);
return res.status(500).json({ error: "Failed to fetch products summary" });
}
};
/*
// Old implementation - direct DB queries:
import { eq, gt, and, sql, inArray } from "drizzle-orm";
import { db } from "@/src/db/db_index"
import { productInfo, units, productSlots, deliverySlotInfo, productTags } from "@/src/db/schema"
const getNextDeliveryDate = async (productId: number): Promise<Date | null> => { const getNextDeliveryDate = async (productId: number): Promise<Date | null> => {
const result = await db const result = await db
.select({ deliveryTime: deliverySlotInfo.deliveryTime }) .select({ deliveryTime: deliverySlotInfo.deliveryTime })
@ -22,13 +77,9 @@ const getNextDeliveryDate = async (productId: number): Promise<Date | null> => {
.orderBy(deliverySlotInfo.deliveryTime) .orderBy(deliverySlotInfo.deliveryTime)
.limit(1); .limit(1);
return result[0]?.deliveryTime || null; return result[0]?.deliveryTime || null;
}; };
/**
* Get all products summary for dropdown
*/
export const getAllProductsSummary = async (req: Request, res: Response) => { export const getAllProductsSummary = async (req: Request, res: Response) => {
try { try {
const { tagId } = req.query; const { tagId } = req.query;
@ -103,3 +154,4 @@ export const getAllProductsSummary = async (req: Request, res: Response) => {
return res.status(500).json({ error: "Failed to fetch products summary" }); return res.status(500).json({ error: "Failed to fetch products summary" });
} }
}; };
*/

View file

@ -1,125 +0,0 @@
/*
* This was a one time script to change the composition of the signed urls
*/
import { db } from '@/src/db/db_index'
import {
userDetails,
productInfo,
productTagInfo,
complaints
} from '@/src/db/schema';
import { eq, not, isNull } from 'drizzle-orm';
const S3_DOMAIN = 'https://s3.sgp.io.cloud.ovh.net';
const cleanImageUrl = (url: string): string => {
if (url.startsWith(S3_DOMAIN)) {
return url.replace(S3_DOMAIN + '/', '');
}
return url;
};
const cleanImageUrls = (urls: string[]): string[] => {
return urls.map(cleanImageUrl);
};
async function migrateUserDetails() {
console.log('Migrating userDetails...');
const users = await db.select().from(userDetails).where(not(isNull(userDetails.profileImage)));
console.log(`Found ${users.length} user records with profile images`);
for (const user of users) {
if (user.profileImage) {
const cleanedUrl = cleanImageUrl(user.profileImage);
await db.update(userDetails)
.set({ profileImage: cleanedUrl })
.where(eq(userDetails.id, user.id));
}
}
console.log('userDetails migration completed');
}
async function migrateProductInfo() {
console.log('Migrating productInfo...');
const products = await db.select().from(productInfo).where(not(isNull(productInfo.images)));
console.log(`Found ${products.length} product records with images`);
for (const product of products) {
if (product.images && Array.isArray(product.images)) {
const cleanedUrls = cleanImageUrls(product.images);
await db.update(productInfo)
.set({ images: cleanedUrls })
.where(eq(productInfo.id, product.id));
}
}
console.log('productInfo migration completed');
}
async function migrateProductTagInfo() {
console.log('Migrating productTagInfo...');
const tags = await db.select().from(productTagInfo).where(not(isNull(productTagInfo.imageUrl)));
console.log(`Found ${tags.length} tag records with images`);
for (const tag of tags) {
if (tag.imageUrl) {
const cleanedUrl = cleanImageUrl(tag.imageUrl);
await db.update(productTagInfo)
.set({ imageUrl: cleanedUrl })
.where(eq(productTagInfo.id, tag.id));
}
}
console.log('productTagInfo migration completed');
}
async function migrateComplaints() {
console.log('Migrating complaints...');
const complaintRecords = await db.select().from(complaints).where(not(isNull(complaints.images)));
console.log(`Found ${complaintRecords.length} complaint records with images`);
for (const complaint of complaintRecords) {
if (complaint.images && Array.isArray(complaint.images)) {
const cleanedUrls = cleanImageUrls(complaint.images);
await db.update(complaints)
.set({ images: cleanedUrls })
.where(eq(complaints.id, complaint.id));
}
}
console.log('complaints migration completed');
}
async function runMigration() {
console.log('Starting image URL migration...');
console.log(`Removing S3 domain: ${S3_DOMAIN}`);
try {
await migrateUserDetails();
await migrateProductInfo();
await migrateProductTagInfo();
await migrateComplaints();
console.log('Migration completed successfully!');
} catch (error) {
console.error('Migration failed:', error);
throw error;
}
}
// Run the migration
runMigration()
.then(() => {
console.log('Process completed successfully');
process.exit(0);
})
.catch((error) => {
console.error('Process failed:', error);
process.exit(1);
});

View file

@ -1,8 +1,79 @@
import {
seedUnits,
seedStaffRoles,
seedStaffPermissions,
seedRolePermissions,
seedKeyValStore,
type UnitSeedData,
type RolePermissionAssignment,
type KeyValSeedData,
type StaffRoleName,
type StaffPermissionName,
} from '@/src/dbService'
import { minOrderValue, deliveryCharge } from '@/src/lib/env-exporter'
import { CONST_KEYS } from '@/src/lib/const-keys'
export async function seed() {
console.log("Seeding database...");
// Seed units
const unitsToSeed: UnitSeedData[] = [
{ shortNotation: "Kg", fullName: "Kilogram" },
{ shortNotation: "L", fullName: "Litre" },
{ shortNotation: "Dz", fullName: "Dozen" },
{ shortNotation: "Pc", fullName: "Unit Piece" },
];
await seedUnits(unitsToSeed);
// Seed staff roles
const rolesToSeed: StaffRoleName[] = ['super_admin', 'admin', 'marketer', 'delivery_staff'];
await seedStaffRoles(rolesToSeed);
// Seed staff permissions
const permissionsToSeed: StaffPermissionName[] = ['crud_product', 'make_coupon', 'crud_staff_users'];
await seedStaffPermissions(permissionsToSeed);
// Seed role-permission assignments
const rolePermissionAssignments: RolePermissionAssignment[] = [
// super_admin gets all permissions
{ roleName: 'super_admin', permissionName: 'crud_product' },
{ roleName: 'super_admin', permissionName: 'make_coupon' },
{ roleName: 'super_admin', permissionName: 'crud_staff_users' },
// admin gets product and coupon permissions
{ roleName: 'admin', permissionName: 'crud_product' },
{ roleName: 'admin', permissionName: 'make_coupon' },
// marketer gets coupon permission
{ roleName: 'marketer', permissionName: 'make_coupon' },
];
await seedRolePermissions(rolePermissionAssignments);
// Seed key-val store constants
const constantsToSeed: KeyValSeedData[] = [
{ key: CONST_KEYS.readableOrderId, value: 0 },
{ key: CONST_KEYS.minRegularOrderValue, value: minOrderValue },
{ key: CONST_KEYS.freeDeliveryThreshold, value: minOrderValue },
{ key: CONST_KEYS.deliveryCharge, value: deliveryCharge },
{ key: CONST_KEYS.flashFreeDeliveryThreshold, value: 500 },
{ key: CONST_KEYS.flashDeliveryCharge, value: 69 },
{ key: CONST_KEYS.popularItems, value: [] },
{ key: CONST_KEYS.allItemsOrder, value: [] },
{ key: CONST_KEYS.versionNum, value: '1.1.0' },
{ key: CONST_KEYS.playStoreUrl, value: 'https://play.google.com/store/apps/details?id=in.freshyo.app' },
{ key: CONST_KEYS.appStoreUrl, value: 'https://apps.apple.com/in/app/freshyo/id6756889077' },
{ key: CONST_KEYS.isFlashDeliveryEnabled, value: false },
{ key: CONST_KEYS.supportMobile, value: '8688182552' },
{ key: CONST_KEYS.supportEmail, value: 'qushammohd@gmail.com' },
];
await seedKeyValStore(constantsToSeed);
console.log("Seeding completed.");
}
/*
// Old implementation - direct DB queries:
import { db } from "@/src/db/db_index" import { db } from "@/src/db/db_index"
import { units, productInfo, deliverySlotInfo, productSlots, keyValStore, staffRoles, staffPermissions, staffRolePermissions } from "@/src/db/schema" import { units, productInfo, deliverySlotInfo, productSlots, keyValStore, staffRoles, staffPermissions, staffRolePermissions } from "@/src/db/schema"
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { minOrderValue, deliveryCharge } from '@/src/lib/env-exporter'
import { CONST_KEYS } from '@/src/lib/const-keys'
export async function seed() { export async function seed() {
console.log("Seeding database..."); console.log("Seeding database...");
@ -136,3 +207,4 @@ export async function seed() {
console.log("Seeding completed."); console.log("Seeding completed.");
} }
*/

View file

@ -155,6 +155,9 @@ export type {
UserUpdateNotesResponse, UserUpdateNotesResponse,
UserRecentProduct, UserRecentProduct,
UserRecentProductsResponse, UserRecentProductsResponse,
// Store types
StoreSummary,
StoresSummaryResponse,
} from '@packages/shared'; } from '@packages/shared';
export type { export type {

View file

@ -1,12 +1,9 @@
import * as cron from 'node-cron'; import * as cron from 'node-cron';
import { db } from '@/src/db/db_index'
import { payments, orders, deliverySlotInfo, refunds } from '@/src/db/schema'
import { eq, and, gt, isNotNull } from 'drizzle-orm';
interface PendingPaymentRecord { interface PendingPaymentRecord {
payment: typeof payments.$inferSelect; payment: any;
order: typeof orders.$inferSelect; order: any;
slot: typeof deliverySlotInfo.$inferSelect; slot: any;
} }
export const createPaymentNotification = (record: PendingPaymentRecord) => { export const createPaymentNotification = (record: PendingPaymentRecord) => {
@ -19,34 +16,60 @@ export const createPaymentNotification = (record: PendingPaymentRecord) => {
export const checkRefundStatuses = async () => { export const checkRefundStatuses = async () => {
try { try {
// const initiatedRefunds = await db // TODO: Reimplement with helpers from @/src/dbService
// .select() // This function checks Razorpay refund status and updates database
// .from(refunds) // Requires: getPendingRefunds(), updateRefundStatus()
// .where(and( } catch (error) {
// eq(refunds.refundStatus, 'initiated'), console.error('Error in checkRefundStatuses:', error);
// isNotNull(refunds.merchantRefundId) }
// )); };
//
// // Process refunds concurrently using Promise.allSettled export const checkPendingPayments = async () => {
// const promises = initiatedRefunds.map(async (refund) => { try {
// if (!refund.merchantRefundId) return; // TODO: Reimplement with helpers from @/src/dbService
// // This function finds pending payments and sends notifications
// try { // Requires: getPendingPaymentsWithOrders()
// const razorpayRefund = await RazorpayPaymentService.fetchRefund(refund.merchantRefundId); } catch (error) {
// console.error('Error checking pending payments:', error);
// if (razorpayRefund.status === 'processed') { }
// await db };
// .update(refunds)
// .set({ refundStatus: 'success', refundProcessedAt: new Date() }) /*
// .where(eq(refunds.id, refund.id)); // Old implementation - direct DB queries:
// } import { db } from '@/src/db/db_index'
// } catch (error) { import { payments, orders, deliverySlotInfo, refunds } from '@/src/db/schema'
// console.error(`Error checking refund ${refund.id}:`, error); import { eq, and, gt, isNotNull } from 'drizzle-orm';
// }
// }); export const checkRefundStatuses = async () => {
// try {
// // Wait for all promises to complete const initiatedRefunds = await db
// await Promise.allSettled(promises); .select()
.from(refunds)
.where(and(
eq(refunds.refundStatus, 'initiated'),
isNotNull(refunds.merchantRefundId)
));
// Process refunds concurrently using Promise.allSettled
const promises = initiatedRefunds.map(async (refund) => {
if (!refund.merchantRefundId) return;
try {
const razorpayRefund = await RazorpayPaymentService.fetchRefund(refund.merchantRefundId);
if (razorpayRefund.status === 'processed') {
await db
.update(refunds)
.set({ refundStatus: 'success', refundProcessedAt: new Date() })
.where(eq(refunds.id, refund.id));
}
} catch (error) {
console.error(`Error checking refund ${refund.id}:`, error);
}
});
// Wait for all promises to complete
await Promise.allSettled(promises);
} catch (error) { } catch (error) {
console.error('Error in checkRefundStatuses:', error); console.error('Error in checkRefundStatuses:', error);
} }
@ -75,4 +98,4 @@ export const checkPendingPayments = async () => {
console.error('Error checking pending payments:', error); console.error('Error checking pending payments:', error);
} }
}; };
*/

View file

@ -1,7 +1,5 @@
import * as cron from 'node-cron'; import * as cron from 'node-cron';
import { db } from '@/src/db/db_index' import { toggleFlashDeliveryForItems, toggleKeyVal } from '@/src/dbService';
import { productInfo, keyValStore } from '@/src/db/schema'
import { inArray, eq } from 'drizzle-orm';
import { CONST_KEYS } from '@/src/lib/const-keys' import { CONST_KEYS } from '@/src/lib/const-keys'
import { computeConstants } from '@/src/lib/const-store' import { computeConstants } from '@/src/lib/const-store'
@ -20,6 +18,61 @@ const MUTTON_ITEMS = [
export const startAutomatedJobs = () => { export const startAutomatedJobs = () => {
// Job to disable flash delivery for mutton at 12 PM daily
cron.schedule('0 12 * * *', async () => {
try {
console.log('Disabling flash delivery for products at 12 PM');
await toggleFlashDeliveryForItems(false, MUTTON_ITEMS);
console.log('Flash delivery disabled successfully');
} catch (error) {
console.error('Error disabling flash delivery:', error);
}
});
// Job to enable flash delivery for mutton at 6 AM daily
cron.schedule('0 6 * * *', async () => {
try {
console.log('Enabling flash delivery for products at 5 AM');
await toggleFlashDeliveryForItems(true, MUTTON_ITEMS);
console.log('Flash delivery enabled successfully');
} catch (error) {
console.error('Error enabling flash delivery:', error);
}
});
// Job to disable flash delivery feature at 9 PM daily
cron.schedule('0 21 * * *', async () => {
try {
console.log('Disabling flash delivery feature at 9 PM');
await toggleKeyVal(CONST_KEYS.isFlashDeliveryEnabled, false);
await computeConstants(); // Refresh Redis cache
console.log('Flash delivery feature disabled successfully');
} catch (error) {
console.error('Error disabling flash delivery feature:', error);
}
});
// Job to enable flash delivery feature at 6 AM daily
cron.schedule('0 6 * * *', async () => {
try {
console.log('Enabling flash delivery feature at 6 AM');
await toggleKeyVal(CONST_KEYS.isFlashDeliveryEnabled, true);
await computeConstants(); // Refresh Redis cache
console.log('Flash delivery feature enabled successfully');
} catch (error) {
console.error('Error enabling flash delivery feature:', error);
}
});
console.log('Automated jobs scheduled');
};
/*
// Old implementation - direct DB queries:
import { db } from '@/src/db/db_index'
import { productInfo, keyValStore } from '@/src/db/schema'
import { inArray, eq } from 'drizzle-orm';
// Job to disable flash delivery for mutton at 12 PM daily // Job to disable flash delivery for mutton at 12 PM daily
cron.schedule('0 12 * * *', async () => { cron.schedule('0 12 * * *', async () => {
try { try {
@ -77,9 +130,7 @@ export const startAutomatedJobs = () => {
console.error('Error enabling flash delivery feature:', error); console.error('Error enabling flash delivery feature:', error);
} }
}); });
*/
console.log('Automated jobs scheduled');
};
// Optional: Call on import if desired, or export and call in main app // Optional: Call on import if desired, or export and call in main app
// startAutomatedJobs(); // startAutomatedJobs();

View file

@ -5,8 +5,7 @@ import { scaffoldStores } from '@/src/trpc/apis/user-apis/apis/stores'
import { scaffoldSlotsWithProducts } from '@/src/trpc/apis/user-apis/apis/slots' import { scaffoldSlotsWithProducts } from '@/src/trpc/apis/user-apis/apis/slots'
import { scaffoldBanners } from '@/src/trpc/apis/user-apis/apis/banners' import { scaffoldBanners } from '@/src/trpc/apis/user-apis/apis/banners'
import { scaffoldStoreWithProducts } from '@/src/trpc/apis/user-apis/apis/stores' import { scaffoldStoreWithProducts } from '@/src/trpc/apis/user-apis/apis/stores'
import { storeInfo } from '@/src/db/schema' import { getStoresSummary } from '@/src/dbService'
import { db } from '@/src/db/db_index'
import { imageUploadS3 } from '@/src/lib/s3-client' import { imageUploadS3 } from '@/src/lib/s3-client'
import { apiCacheKey, cloudflareApiToken, cloudflareZoneId, assetsDomain } from '@/src/lib/env-exporter' import { apiCacheKey, cloudflareApiToken, cloudflareZoneId, assetsDomain } from '@/src/lib/env-exporter'
import { CACHE_FILENAMES } from '@packages/shared' import { CACHE_FILENAMES } from '@packages/shared'
@ -167,8 +166,16 @@ export async function createStoreFile(storeId: number): Promise<string> {
} }
export async function createAllStoresFiles(): Promise<string[]> { export async function createAllStoresFiles(): Promise<string[]> {
// Fetch all store IDs from database /*
// Old implementation - direct DB queries:
import { db } from '@/src/db/db_index'
import { storeInfo } from '@/src/db/schema'
const stores = await db.select({ id: storeInfo.id }).from(storeInfo) const stores = await db.select({ id: storeInfo.id }).from(storeInfo)
*/
// Fetch all store IDs from database using helper
const stores = await getStoresSummary()
// Create cache files for all stores and collect URLs // Create cache files for all stores and collect URLs
const results: string[] = [] const results: string[] = []
@ -289,7 +296,15 @@ async function createBannersFileInternal(): Promise<string> {
} }
async function createAllStoresFilesInternal(): Promise<string[]> { async function createAllStoresFilesInternal(): Promise<string[]> {
/*
// Old implementation - direct DB queries:
import { db } from '@/src/db/db_index'
import { storeInfo } from '@/src/db/schema'
const stores = await db.select({ id: storeInfo.id }).from(storeInfo) const stores = await db.select({ id: storeInfo.id }).from(storeInfo)
*/
const stores = await getStoresSummary()
const results: string[] = [] const results: string[] = []
for (const store of stores) { for (const store of stores) {

View file

@ -1,5 +1,4 @@
import { db } from '@/src/db/db_index' import { getAllKeyValStore } from '@/src/dbService'
import { keyValStore } from '@/src/db/schema'
import redisClient from '@/src/lib/redis-client' import redisClient from '@/src/lib/redis-client'
import { CONST_KEYS, CONST_KEYS_ARRAY, type ConstKey } from '@/src/lib/const-keys' import { CONST_KEYS, CONST_KEYS_ARRAY, type ConstKey } from '@/src/lib/const-keys'
@ -9,7 +8,15 @@ export const computeConstants = async (): Promise<void> => {
try { try {
console.log('Computing constants from database...'); console.log('Computing constants from database...');
/*
// Old implementation - direct DB queries:
import { db } from '@/src/db/db_index'
import { keyValStore } from '@/src/db/schema'
const constants = await db.select().from(keyValStore); const constants = await db.select().from(keyValStore);
*/
const constants = await getAllKeyValStore();
for (const constant of constants) { for (const constant of constants) {
const redisKey = `${CONST_REDIS_PREFIX}${constant.key}`; const redisKey = `${CONST_REDIS_PREFIX}${constant.key}`;

View file

@ -1,6 +1,4 @@
import { db } from '@/src/db/db_index' import { deleteOrdersWithRelations } from '@/src/dbService'
import { orders, orderItems, orderStatus, payments, refunds, couponUsage, complaints } from '@/src/db/schema'
import { eq, inArray } from 'drizzle-orm';
/** /**
* Delete orders and all their related records * Delete orders and all their related records
@ -8,6 +6,26 @@ import { eq, inArray } from 'drizzle-orm';
* @returns Promise<void> * @returns Promise<void>
* @throws Error if deletion fails * @throws Error if deletion fails
*/ */
export const deleteOrders = async (orderIds: number[]): Promise<void> => {
if (orderIds.length === 0) {
return;
}
try {
await deleteOrdersWithRelations(orderIds);
console.log(`Successfully deleted ${orderIds.length} orders and all related records`);
} catch (error) {
console.error(`Failed to delete orders ${orderIds.join(', ')}:`, error);
throw error;
}
};
/*
// Old implementation - direct DB queries:
import { db } from '@/src/db/db_index'
import { orders, orderItems, orderStatus, payments, refunds, couponUsage, complaints } from '@/src/db/schema'
import { eq, inArray } from 'drizzle-orm';
export const deleteOrders = async (orderIds: number[]): Promise<void> => { export const deleteOrders = async (orderIds: number[]): Promise<void> => {
if (orderIds.length === 0) { if (orderIds.length === 0) {
return; return;
@ -43,3 +61,4 @@ export const deleteOrders = async (orderIds: number[]): Promise<void> => {
throw error; throw error;
} }
}; };
*/

View file

@ -1,7 +1,7 @@
import { Queue, Worker } from 'bullmq'; import { Queue, Worker } from 'bullmq';
import { Expo } from 'expo-server-sdk'; import { Expo } from 'expo-server-sdk';
import { redisUrl } from '@/src/lib/env-exporter' import { redisUrl } from '@/src/lib/env-exporter'
import { db } from '@/src/db/db_index' // import { db } from '@/src/db/db_index'
import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client' import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client'
import { import {
NOTIFS_QUEUE, NOTIFS_QUEUE,

View file

@ -1,8 +1,9 @@
import { db } from '@/src/db/db_index' import {
import { orders, orderStatus } from '@/src/db/schema' getOrdersByIdsWithFullData,
getOrderByIdWithFullData,
} from '@/src/dbService'
import redisClient from '@/src/lib/redis-client' import redisClient from '@/src/lib/redis-client'
import { sendTelegramMessage } from '@/src/lib/telegram-service' import { sendTelegramMessage } from '@/src/lib/telegram-service'
import { inArray, eq } from 'drizzle-orm';
const ORDER_CHANNEL = 'orders:placed'; const ORDER_CHANNEL = 'orders:placed';
const CANCELLED_CHANNEL = 'orders:cancelled'; const CANCELLED_CHANNEL = 'orders:cancelled';
@ -98,6 +99,12 @@ export const startOrderHandler = async (): Promise<void> => {
const { orderIds }: OrderIdMessage = JSON.parse(message); const { orderIds }: OrderIdMessage = JSON.parse(message);
console.log('New order received, sending to Telegram...'); console.log('New order received, sending to Telegram...');
/*
// Old implementation - direct DB queries:
import { db } from '@/src/db/db_index'
import { orders } from '@/src/db/schema'
import { inArray } from 'drizzle-orm';
const ordersData = await db.query.orders.findMany({ const ordersData = await db.query.orders.findMany({
where: inArray(orders.id, orderIds), where: inArray(orders.id, orderIds),
with: { with: {
@ -106,6 +113,9 @@ export const startOrderHandler = async (): Promise<void> => {
slot: true, slot: true,
}, },
}); });
*/
const ordersData = await getOrdersByIdsWithFullData(orderIds);
const telegramMessage = formatOrderMessageWithFullData(ordersData); const telegramMessage = formatOrderMessageWithFullData(ordersData);
await sendTelegramMessage(telegramMessage); await sendTelegramMessage(telegramMessage);
@ -143,6 +153,12 @@ export const startCancellationHandler = async (): Promise<void> => {
const cancellationData: CancellationMessage = JSON.parse(message); const cancellationData: CancellationMessage = JSON.parse(message);
console.log('Order cancellation received, sending to Telegram...'); console.log('Order cancellation received, sending to Telegram...');
/*
// Old implementation - direct DB queries:
import { db } from '@/src/db/db_index'
import { orders } from '@/src/db/schema'
import { eq } from 'drizzle-orm';
const orderData = await db.query.orders.findFirst({ const orderData = await db.query.orders.findFirst({
where: eq(orders.id, cancellationData.orderId), where: eq(orders.id, cancellationData.orderId),
with: { with: {
@ -151,6 +167,9 @@ export const startCancellationHandler = async (): Promise<void> => {
refunds: true, refunds: true,
}, },
}); });
*/
const orderData = await getOrderByIdWithFullData(cancellationData.orderId);
if (!orderData) { if (!orderData) {
console.error('Order not found for cancellation:', cancellationData.orderId); console.error('Order not found for cancellation:', cancellationData.orderId);

View file

@ -1,9 +1,7 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { db } from '@/src/db/db_index' import { getStaffUserById, isUserSuspended } from '@/src/dbService';
import { staffUsers, userDetails } from '@/src/db/schema' import { ApiError } from '@/src/lib/api-error';
import { eq } from 'drizzle-orm';
import { ApiError } from '@/src/lib/api-error'
interface AuthenticatedRequest extends Request { interface AuthenticatedRequest extends Request {
user?: { user?: {
@ -33,10 +31,19 @@ export const authenticateUser = async (req: AuthenticatedRequest, res: Response,
// Check if this is a staff token (has staffId) // Check if this is a staff token (has staffId)
if (decoded.staffId) { if (decoded.staffId) {
// This is a staff token, verify staff exists /*
// Old implementation - direct DB queries:
import { db } from '@/src/db/db_index'
import { staffUsers } from '@/src/db/schema'
import { eq } from 'drizzle-orm';
const staff = await db.query.staffUsers.findFirst({ const staff = await db.query.staffUsers.findFirst({
where: eq(staffUsers.id, decoded.staffId), where: eq(staffUsers.id, decoded.staffId),
}); });
*/
// This is a staff token, verify staff exists
const staff = await getStaffUserById(decoded.staffId);
if (!staff) { if (!staff) {
throw new ApiError('Invalid staff token', 401); throw new ApiError('Invalid staff token', 401);
@ -50,7 +57,12 @@ export const authenticateUser = async (req: AuthenticatedRequest, res: Response,
// This is a regular user token // This is a regular user token
req.user = decoded; req.user = decoded;
// Check if user is suspended /*
// Old implementation - direct DB queries:
import { db } from '@/src/db/db_index'
import { userDetails } from '@/src/db/schema'
import { eq } from 'drizzle-orm';
const details = await db.query.userDetails.findFirst({ const details = await db.query.userDetails.findFirst({
where: eq(userDetails.userId, decoded.userId), where: eq(userDetails.userId, decoded.userId),
}); });
@ -58,6 +70,14 @@ export const authenticateUser = async (req: AuthenticatedRequest, res: Response,
if (details?.isSuspended) { if (details?.isSuspended) {
throw new ApiError('Account suspended', 403); throw new ApiError('Account suspended', 403);
} }
*/
// Check if user is suspended
const suspended = await isUserSuspended(decoded.userId);
if (suspended) {
throw new ApiError('Account suspended', 403);
}
} }
next(); next();

View file

@ -1,9 +1,7 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { db } from '@/src/db/db_index' import { getStaffUserById } from '@/src/dbService';
import { staffUsers } from '@/src/db/schema' import { ApiError } from '@/src/lib/api-error';
import { eq } from 'drizzle-orm';
import { ApiError } from '@/src/lib/api-error'
// Extend Request interface to include staffUser // Extend Request interface to include staffUser
declare global { declare global {
@ -54,10 +52,19 @@ export const authenticateStaff = async (req: Request, res: Response, next: NextF
throw new ApiError('Invalid staff token format', 401); throw new ApiError('Invalid staff token format', 401);
} }
// Fetch staff user from database /*
// Old implementation - direct DB queries:
import { db } from '@/src/db/db_index'
import { staffUsers } from '@/src/db/schema'
import { eq } from 'drizzle-orm';
const staff = await db.query.staffUsers.findFirst({ const staff = await db.query.staffUsers.findFirst({
where: eq(staffUsers.id, decoded.staffId), where: eq(staffUsers.id, decoded.staffId),
}); });
*/
// Fetch staff user from database
const staff = await getStaffUserById(decoded.staffId);
if (!staff) { if (!staff) {
throw new ApiError('Staff user not found', 401); throw new ApiError('Staff user not found', 401);

View file

@ -84,6 +84,7 @@ export {
updateSlotDeliverySequence, updateSlotDeliverySequence,
// Admin - Staff User // Admin - Staff User
getStaffUserByName, getStaffUserByName,
getStaffUserById,
getAllStaff, getAllStaff,
getAllUsers, getAllUsers,
getUserWithDetails, getUserWithDetails,
@ -164,6 +165,8 @@ export {
getUserProductReviews, getUserProductReviews,
getUserProductByIdBasic, getUserProductByIdBasic,
createUserProductReview, createUserProductReview,
getAllProductsWithUnits,
type ProductSummaryData,
// User - Slots // User - Slots
getUserActiveSlotsList, getUserActiveSlotsList,
getUserProductAvailability, getUserProductAvailability,
@ -180,10 +183,15 @@ export {
getUserAuthById, getUserAuthById,
getUserAuthCreds, getUserAuthCreds,
getUserAuthDetails, getUserAuthDetails,
isUserSuspended,
createUserAuthWithCreds, createUserAuthWithCreds,
createUserAuthWithMobile, createUserAuthWithMobile,
upsertUserAuthPassword, upsertUserAuthPassword,
deleteUserAuthAccount, deleteUserAuthAccount,
// UV API helpers
createUserWithProfile,
getUserDetailsByUserId,
updateUserProfile,
// User - Coupon // User - Coupon
getUserActiveCouponsWithRelations, getUserActiveCouponsWithRelations,
getUserAllCouponsWithRelations, getUserAllCouponsWithRelations,
@ -218,4 +226,53 @@ export {
getUserRecentlyDeliveredOrderIds, getUserRecentlyDeliveredOrderIds,
getUserProductIdsFromOrders, getUserProductIdsFromOrders,
getUserProductsForRecentOrders, getUserProductsForRecentOrders,
// Store Helpers
getAllBannersForCache,
getAllProductsForCache,
getAllStoresForCache,
getAllDeliverySlotsForCache,
getAllSpecialDealsForCache,
getAllProductTagsForCache,
getAllTagsForCache,
getAllTagProductMappings,
getAllSlotsWithProductsForCache,
getAllUserNegativityScores,
getUserNegativityScore,
type BannerData,
type ProductBasicData,
type StoreBasicData,
type DeliverySlotData,
type SpecialDealData,
type ProductTagData,
type TagBasicData,
type TagProductMapping,
type SlotWithProductsData,
type UserNegativityData,
// Automated Jobs
toggleFlashDeliveryForItems,
toggleKeyVal,
getAllKeyValStore,
// Post-order handler helpers
getOrdersByIdsWithFullData,
getOrderByIdWithFullData,
type OrderWithFullData,
type OrderWithCancellationData,
// Common API helpers
getSuspendedProductIds,
getNextDeliveryDateWithCapacity,
getStoresSummary,
healthCheck,
// Delete orders helper
deleteOrdersWithRelations,
// Seed helpers
seedUnits,
seedStaffRoles,
seedStaffPermissions,
seedRolePermissions,
seedKeyValStore,
type UnitSeedData,
type RolePermissionAssignment,
type KeyValSeedData,
type StaffRoleName,
type StaffPermissionName,
} from 'postgresService' } from 'postgresService'

View file

@ -1,405 +0,0 @@
import { db } from '@/src/db/db_index'
import {
orders,
orderItems,
orderStatus,
addresses,
productInfo,
paymentInfoTable,
coupons,
couponUsage,
payments,
cartItems,
refunds,
units,
userDetails,
} from '@/src/db/schema'
import { eq, and, inArray, desc, gte } from 'drizzle-orm'
// ============ User/Auth Queries ============
/**
* Get user details by user ID
*/
export async function getUserDetails(userId: number) {
return db.query.userDetails.findFirst({
where: eq(userDetails.userId, userId),
})
}
// ============ Address Queries ============
/**
* Get user address by ID
*/
export async function getUserAddress(userId: number, addressId: number) {
return db.query.addresses.findFirst({
where: and(eq(addresses.userId, userId), eq(addresses.id, addressId)),
})
}
// ============ Product Queries ============
/**
* Get product by ID
*/
export async function getProductById(productId: number) {
return db.query.productInfo.findFirst({
where: eq(productInfo.id, productId),
})
}
/**
* Get multiple products by IDs with unit info
*/
export async function getProductsByIdsWithUnits(productIds: number[]) {
return db
.select({
id: productInfo.id,
name: productInfo.name,
shortDescription: productInfo.shortDescription,
price: productInfo.price,
images: productInfo.images,
isOutOfStock: productInfo.isOutOfStock,
unitShortNotation: units.shortNotation,
incrementStep: productInfo.incrementStep,
})
.from(productInfo)
.innerJoin(units, eq(productInfo.unitId, units.id))
.where(and(inArray(productInfo.id, productIds), eq(productInfo.isSuspended, false)))
.orderBy(desc(productInfo.createdAt))
}
// ============ Coupon Queries ============
/**
* Get coupon with usages for user
*/
export async function getCouponWithUsages(couponId: number, userId: number) {
return db.query.coupons.findFirst({
where: eq(coupons.id, couponId),
with: {
usages: { where: eq(couponUsage.userId, userId) },
},
})
}
/**
* Insert coupon usage
*/
export async function insertCouponUsage(data: {
userId: number
couponId: number
orderId: number
orderItemId: number | null
usedAt: Date
}) {
return db.insert(couponUsage).values(data)
}
/**
* Get coupon usages for order
*/
export async function getCouponUsagesForOrder(orderId: number) {
return db.query.couponUsage.findMany({
where: eq(couponUsage.orderId, orderId),
with: {
coupon: true,
},
})
}
// ============ Cart Queries ============
/**
* Delete cart items for user by product IDs
*/
export async function deleteCartItems(userId: number, productIds: number[]) {
return db.delete(cartItems).where(
and(
eq(cartItems.userId, userId),
inArray(cartItems.productId, productIds)
)
)
}
// ============ Payment Info Queries ============
/**
* Create payment info
*/
export async function createPaymentInfo(data: {
status: string
gateway: string
merchantOrderId: string
}) {
return db.insert(paymentInfoTable).values(data).returning()
}
// ============ Order Queries ============
/**
* Insert multiple orders
*/
export async function insertOrders(ordersData: any[]) {
return db.insert(orders).values(ordersData).returning()
}
/**
* Insert multiple order items
*/
export async function insertOrderItems(itemsData: any[]) {
return db.insert(orderItems).values(itemsData)
}
/**
* Insert multiple order statuses
*/
export async function insertOrderStatuses(statusesData: any[]) {
return db.insert(orderStatus).values(statusesData)
}
/**
* Get user orders with all relations
*/
export async function getUserOrdersWithRelations(userId: number, limit: number, offset: number) {
return db.query.orders.findMany({
where: eq(orders.userId, userId),
with: {
orderItems: {
with: {
product: true,
},
},
slot: true,
paymentInfo: true,
orderStatus: true,
refunds: true,
},
orderBy: (orders, { desc }) => [desc(orders.createdAt)],
limit: limit,
offset: offset,
})
}
/**
* Count user orders
*/
export async function countUserOrders(userId: number) {
return db.$count(orders, eq(orders.userId, userId))
}
/**
* Get order by ID with all relations
*/
export async function getOrderByIdWithRelations(orderId: number) {
return db.query.orders.findFirst({
where: eq(orders.id, orderId),
with: {
orderItems: {
with: {
product: true,
},
},
slot: true,
paymentInfo: true,
orderStatus: {
with: {
refundCoupon: true,
},
},
refunds: true,
},
})
}
/**
* Get order by ID with order status
*/
export async function getOrderWithStatus(orderId: number) {
return db.query.orders.findFirst({
where: eq(orders.id, orderId),
with: {
orderStatus: true,
},
})
}
/**
* Update order status to cancelled
*/
export async function updateOrderStatusToCancelled(
statusId: number,
data: {
isCancelled: boolean
cancelReason: string
cancellationUserNotes: string
cancellationReviewed: boolean
}
) {
return db
.update(orderStatus)
.set(data)
.where(eq(orderStatus.id, statusId))
}
/**
* Insert refund record
*/
export async function insertRefund(data: { orderId: number; refundStatus: string }) {
return db.insert(refunds).values(data)
}
/**
* Update order notes
*/
export async function updateOrderNotes(orderId: number, userNotes: string | null) {
return db
.update(orders)
.set({ userNotes })
.where(eq(orders.id, orderId))
}
/**
* Get recent delivered orders for user
*/
export async function getRecentDeliveredOrders(
userId: number,
since: Date,
limit: number
) {
return db
.select({ id: orders.id })
.from(orders)
.innerJoin(orderStatus, eq(orders.id, orderStatus.orderId))
.where(
and(
eq(orders.userId, userId),
eq(orderStatus.isDelivered, true),
gte(orders.createdAt, since)
)
)
.orderBy(desc(orders.createdAt))
.limit(limit)
}
/**
* Get order items by order IDs
*/
export async function getOrderItemsByOrderIds(orderIds: number[]) {
return db
.select({ productId: orderItems.productId })
.from(orderItems)
.where(inArray(orderItems.orderId, orderIds))
}
// ============ Transaction Helper ============
/**
* Execute function within a database transaction
*/
export async function withTransaction<T>(fn: (tx: any) => Promise<T>): Promise<T> {
return db.transaction(fn)
}
/**
* Cancel order with refund record in a transaction
*/
export async function cancelOrderWithRefund(
statusId: number,
orderId: number,
isCod: boolean,
reason: string
): Promise<{ orderId: number }> {
return db.transaction(async (tx) => {
// Update order status
await tx
.update(orderStatus)
.set({
isCancelled: true,
cancelReason: reason,
cancellationUserNotes: reason,
cancellationReviewed: false,
})
.where(eq(orderStatus.id, statusId))
// Insert refund record
const refundStatus = isCod ? "na" : "pending"
await tx.insert(refunds).values({
orderId,
refundStatus,
})
return { orderId }
})
}
type Tx = Parameters<Parameters<typeof db.transaction>[0]>[0]
/**
* Create orders with payment info in a transaction
*/
export async function createOrdersWithPayment(
ordersData: any[],
paymentMethod: "online" | "cod",
totalWithDelivery: number,
razorpayOrderCreator?: (paymentInfoId: number, amount: string) => Promise<any>,
paymentRecordInserter?: (paymentInfoId: number, razorpayOrder: any, tx: Tx) => Promise<any>
): Promise<typeof orders.$inferSelect[]> {
return db.transaction(async (tx) => {
let sharedPaymentInfoId: number | null = null
if (paymentMethod === "online") {
const [paymentInfo] = await tx
.insert(paymentInfoTable)
.values({
status: "pending",
gateway: "razorpay",
merchantOrderId: `multi_order_${Date.now()}`,
})
.returning()
sharedPaymentInfoId = paymentInfo.id
}
const ordersToInsert: Omit<typeof orders.$inferInsert, "id">[] = ordersData.map(
(od) => ({
...od.order,
paymentInfoId: sharedPaymentInfoId,
})
)
const insertedOrders = await tx.insert(orders).values(ordersToInsert).returning()
const allOrderItems: Omit<typeof orderItems.$inferInsert, "id">[] = []
const allOrderStatuses: Omit<typeof orderStatus.$inferInsert, "id">[] = []
insertedOrders.forEach((order: typeof orders.$inferSelect, index: number) => {
const od = ordersData[index]
od.orderItems.forEach((item: any) => {
allOrderItems.push({ ...item, orderId: order.id as number })
})
allOrderStatuses.push({
...od.orderStatus,
orderId: order.id as number,
})
})
await tx.insert(orderItems).values(allOrderItems)
await tx.insert(orderStatus).values(allOrderStatuses)
if (paymentMethod === "online" && sharedPaymentInfoId && razorpayOrderCreator && paymentRecordInserter) {
const razorpayOrder = await razorpayOrderCreator(
sharedPaymentInfoId,
totalWithDelivery.toString()
)
await paymentRecordInserter(
sharedPaymentInfoId,
razorpayOrder,
tx
)
}
return insertedOrders
})
}

View file

@ -1,138 +0,0 @@
import { db } from '@/src/db/db_index'
import { productInfo, units, productSlots, deliverySlotInfo, specialDeals, storeInfo, productReviews, users } from '@/src/db/schema'
import { eq, and, gt, sql, desc } from 'drizzle-orm'
/**
* Get product basic info with unit
*/
export async function getProductWithUnit(productId: number) {
return db
.select({
id: productInfo.id,
name: productInfo.name,
shortDescription: productInfo.shortDescription,
longDescription: productInfo.longDescription,
price: productInfo.price,
marketPrice: productInfo.marketPrice,
images: productInfo.images,
isOutOfStock: productInfo.isOutOfStock,
storeId: productInfo.storeId,
unitShortNotation: units.shortNotation,
incrementStep: productInfo.incrementStep,
productQuantity: productInfo.productQuantity,
isFlashAvailable: productInfo.isFlashAvailable,
flashPrice: productInfo.flashPrice,
})
.from(productInfo)
.innerJoin(units, eq(productInfo.unitId, units.id))
.where(eq(productInfo.id, productId))
.limit(1)
}
/**
* Get store info by ID
*/
export async function getStoreById(storeId: number) {
return db.query.storeInfo.findFirst({
where: eq(storeInfo.id, storeId),
columns: { id: true, name: true, description: true },
})
}
/**
* Get delivery slots for product
*/
export async function getProductDeliverySlots(productId: number) {
return db
.select({
id: deliverySlotInfo.id,
deliveryTime: deliverySlotInfo.deliveryTime,
freezeTime: deliverySlotInfo.freezeTime,
})
.from(productSlots)
.innerJoin(deliverySlotInfo, eq(productSlots.slotId, deliverySlotInfo.id))
.where(
and(
eq(productSlots.productId, productId),
eq(deliverySlotInfo.isActive, true),
gt(deliverySlotInfo.deliveryTime, sql`NOW()`),
gt(deliverySlotInfo.freezeTime, sql`NOW()`)
)
)
.orderBy(deliverySlotInfo.deliveryTime)
}
/**
* Get special deals for product
*/
export async function getProductSpecialDeals(productId: number) {
return db
.select({
quantity: specialDeals.quantity,
price: specialDeals.price,
validTill: specialDeals.validTill,
})
.from(specialDeals)
.where(
and(
eq(specialDeals.productId, productId),
gt(specialDeals.validTill, sql`NOW()`)
)
)
.orderBy(specialDeals.quantity)
}
/**
* Get product reviews with user info
*/
export async function getProductReviews(productId: number, limit: number, offset: number) {
return db
.select({
id: productReviews.id,
reviewBody: productReviews.reviewBody,
ratings: productReviews.ratings,
imageUrls: productReviews.imageUrls,
reviewTime: productReviews.reviewTime,
userName: users.name,
})
.from(productReviews)
.innerJoin(users, eq(productReviews.userId, users.id))
.where(eq(productReviews.productId, productId))
.orderBy(desc(productReviews.reviewTime))
.limit(limit)
.offset(offset)
}
/**
* Count reviews for product
*/
export async function countProductReviews(productId: number) {
const result = await db
.select({ count: sql`count(*)` })
.from(productReviews)
.where(eq(productReviews.productId, productId))
return Number(result[0].count)
}
/**
* Check if product exists
*/
export async function checkProductExists(productId: number) {
return db.query.productInfo.findFirst({
where: eq(productInfo.id, productId),
})
}
/**
* Insert new review
*/
export async function insertReview(data: {
userId: number
productId: number
reviewBody: string
ratings: number
imageUrls: string[]
}) {
return db.insert(productReviews).values(data).returning()
}

View file

@ -1,37 +1,43 @@
// import redisClient from '@/src/stores/redis-client'; import redisClient from '@/src/lib/redis-client'
import redisClient from '@/src/lib/redis-client'; import {
import { db } from '@/src/db/db_index' getAllBannersForCache,
import { homeBanners } from '@/src/db/schema' type BannerData,
import { isNotNull, asc } from 'drizzle-orm'; } from '@/src/dbService'
import { scaffoldAssetUrl } from '@/src/lib/s3-client'; import { scaffoldAssetUrl } from '@/src/lib/s3-client'
// Banner Type (matches getBanners return) // Banner Type (matches getBanners return)
interface Banner { interface Banner {
id: number; id: number
name: string; name: string
imageUrl: string | null; imageUrl: string | null
serialNum: number | null; serialNum: number | null
productIds: number[] | null; productIds: number[] | null
createdAt: Date; createdAt: Date
// updatedAt: Date;
} }
export async function initializeBannerStore(): Promise<void> { export async function initializeBannerStore(): Promise<void> {
try { try {
console.log('Initializing banner store in Redis...'); console.log('Initializing banner store in Redis...')
const banners = await getAllBannersForCache()
/*
// Old implementation - direct DB queries:
import { db } from '@/src/db/db_index'
import { homeBanners } from '@/src/db/schema'
import { isNotNull, asc } from 'drizzle-orm'
const banners = await db.query.homeBanners.findMany({ const banners = await db.query.homeBanners.findMany({
where: isNotNull(homeBanners.serialNum), // Only show assigned banners where: isNotNull(homeBanners.serialNum),
orderBy: asc(homeBanners.serialNum), // Order by slot number 1-4 orderBy: asc(homeBanners.serialNum),
}); });
*/
// Store each banner in Redis // Store each banner in Redis
for (const banner of banners) { for (const banner of banners) {
const signedImageUrl = banner.imageUrl ? scaffoldAssetUrl(banner.imageUrl) : banner.imageUrl; const signedImageUrl = banner.imageUrl
? scaffoldAssetUrl(banner.imageUrl)
: banner.imageUrl
const bannerObj: Banner = { const bannerObj: Banner = {
id: banner.id, id: banner.id,
@ -40,53 +46,52 @@ export async function initializeBannerStore(): Promise<void> {
serialNum: banner.serialNum, serialNum: banner.serialNum,
productIds: banner.productIds, productIds: banner.productIds,
createdAt: banner.createdAt, createdAt: banner.createdAt,
// updatedAt: banner.updatedAt,
};
await redisClient.set(`banner:${banner.id}`, JSON.stringify(bannerObj));
} }
console.log('Banner store initialized successfully'); await redisClient.set(`banner:${banner.id}`, JSON.stringify(bannerObj))
}
console.log('Banner store initialized successfully')
} catch (error) { } catch (error) {
console.error('Error initializing banner store:', error); console.error('Error initializing banner store:', error)
} }
} }
export async function getBannerById(id: number): Promise<Banner | null> { export async function getBannerById(id: number): Promise<Banner | null> {
try { try {
const key = `banner:${id}`; const key = `banner:${id}`
const data = await redisClient.get(key); const data = await redisClient.get(key)
if (!data) return null; if (!data) return null
return JSON.parse(data) as Banner; return JSON.parse(data) as Banner
} catch (error) { } catch (error) {
console.error(`Error getting banner ${id}:`, error); console.error(`Error getting banner ${id}:`, error)
return null; return null
} }
} }
export async function getAllBanners(): Promise<Banner[]> { export async function getAllBanners(): Promise<Banner[]> {
try { try {
// Get all keys matching the pattern "banner:*" // Get all keys matching the pattern "banner:*"
const keys = await redisClient.KEYS('banner:*'); const keys = await redisClient.KEYS('banner:*')
if (keys.length === 0) return []; if (keys.length === 0) return []
// Get all banners using MGET for better performance // Get all banners using MGET for better performance
const bannersData = await redisClient.MGET(keys); const bannersData = await redisClient.MGET(keys)
const banners: Banner[] = []; const banners: Banner[] = []
for (const bannerData of bannersData) { for (const bannerData of bannersData) {
if (bannerData) { if (bannerData) {
banners.push(JSON.parse(bannerData) as Banner); banners.push(JSON.parse(bannerData) as Banner)
} }
} }
// Sort by serialNum to maintain the same order as the original query // Sort by serialNum to maintain the same order as the original query
banners.sort((a, b) => (a.serialNum || 0) - (b.serialNum || 0)); banners.sort((a, b) => (a.serialNum || 0) - (b.serialNum || 0))
return banners; return banners
} catch (error) { } catch (error) {
console.error('Error getting all banners:', error); console.error('Error getting all banners:', error)
return []; return []
} }
} }

View file

@ -1,36 +1,51 @@
// import redisClient from '@/src/stores/redis-client'; import redisClient from '@/src/lib/redis-client'
import redisClient from '@/src/lib/redis-client'; import {
import { db } from '@/src/db/db_index' getAllProductsForCache,
import { productInfo, units, productSlots, deliverySlotInfo, specialDeals, storeInfo, productTags, productTagInfo } from '@/src/db/schema' getAllStoresForCache,
import { eq, and, gt, sql } from 'drizzle-orm'; getAllDeliverySlotsForCache,
import { generateSignedUrlsFromS3Urls, scaffoldAssetUrl } from '@/src/lib/s3-client'; getAllSpecialDealsForCache,
getAllProductTagsForCache,
type ProductBasicData,
type StoreBasicData,
type DeliverySlotData,
type SpecialDealData,
type ProductTagData,
} from '@/src/dbService'
import { scaffoldAssetUrl } from '@/src/lib/s3-client'
// Uniform Product Type (matches getProductDetails return) // Uniform Product Type (matches getProductDetails return)
interface Product { interface Product {
id: number; id: number
name: string; name: string
shortDescription: string | null; shortDescription: string | null
longDescription: string | null; longDescription: string | null
price: string; price: string
marketPrice: string | null; marketPrice: string | null
unitNotation: string; unitNotation: string
images: string[]; images: string[]
isOutOfStock: boolean; isOutOfStock: boolean
store: { id: number; name: string; description: string | null } | null; store: { id: number; name: string; description: string | null } | null
incrementStep: number; incrementStep: number
productQuantity: number; productQuantity: number
isFlashAvailable: boolean; isFlashAvailable: boolean
flashPrice: string | null; flashPrice: string | null
deliverySlots: Array<{ id: number; deliveryTime: Date; freezeTime: Date; isCapacityFull: boolean }>; deliverySlots: Array<{ id: number; deliveryTime: Date; freezeTime: Date; isCapacityFull: boolean }>
specialDeals: Array<{ quantity: string; price: string; validTill: Date }>; specialDeals: Array<{ quantity: string; price: string; validTill: Date }>
productTags: string[]; productTags: string[]
} }
export async function initializeProducts(): Promise<void> { export async function initializeProducts(): Promise<void> {
try { try {
console.log('Initializing product store in Redis...'); console.log('Initializing product store in Redis...')
// Fetch all products with full details (similar to productMega logic) // Fetch all products with full details (similar to productMega logic)
const productsData = await getAllProductsForCache()
/*
// Old implementation - direct DB queries:
import { db } from '@/src/db/db_index'
import { productInfo, units } from '@/src/db/schema'
const productsData = await db const productsData = await db
.select({ .select({
id: productInfo.id, id: productInfo.id,
@ -50,74 +65,50 @@ export async function initializeProducts(): Promise<void> {
}) })
.from(productInfo) .from(productInfo)
.innerJoin(units, eq(productInfo.unitId, units.id)); .innerJoin(units, eq(productInfo.unitId, units.id));
*/
// Fetch all stores // Fetch all stores
const allStores = await db.query.storeInfo.findMany({ const allStores = await getAllStoresForCache()
columns: { id: true, name: true, description: true }, const storeMap = new Map(allStores.map((s) => [s.id, s]))
});
const storeMap = new Map(allStores.map(s => [s.id, s]));
// Fetch all delivery slots (excluding full capacity slots) // Fetch all delivery slots (excluding full capacity slots)
const allDeliverySlots = await db const allDeliverySlots = await getAllDeliverySlotsForCache()
.select({ const deliverySlotsMap = new Map<number, DeliverySlotData[]>()
productId: productSlots.productId,
id: deliverySlotInfo.id,
deliveryTime: deliverySlotInfo.deliveryTime,
freezeTime: deliverySlotInfo.freezeTime,
isCapacityFull: deliverySlotInfo.isCapacityFull,
})
.from(productSlots)
.innerJoin(deliverySlotInfo, eq(productSlots.slotId, deliverySlotInfo.id))
.where(
and(
eq(deliverySlotInfo.isActive, true),
eq(deliverySlotInfo.isCapacityFull, false),
gt(deliverySlotInfo.deliveryTime, sql`NOW()`)
)
);
const deliverySlotsMap = new Map<number, typeof allDeliverySlots>();
for (const slot of allDeliverySlots) { for (const slot of allDeliverySlots) {
if (!deliverySlotsMap.has(slot.productId)) deliverySlotsMap.set(slot.productId, []); if (!deliverySlotsMap.has(slot.productId))
deliverySlotsMap.get(slot.productId)!.push(slot); deliverySlotsMap.set(slot.productId, [])
deliverySlotsMap.get(slot.productId)!.push(slot)
} }
// Fetch all special deals // Fetch all special deals
const allSpecialDeals = await db const allSpecialDeals = await getAllSpecialDealsForCache()
.select({ const specialDealsMap = new Map<number, SpecialDealData[]>()
productId: specialDeals.productId,
quantity: specialDeals.quantity,
price: specialDeals.price,
validTill: specialDeals.validTill,
})
.from(specialDeals)
.where(gt(specialDeals.validTill, sql`NOW()`));
const specialDealsMap = new Map<number, typeof allSpecialDeals>();
for (const deal of allSpecialDeals) { for (const deal of allSpecialDeals) {
if (!specialDealsMap.has(deal.productId)) specialDealsMap.set(deal.productId, []); if (!specialDealsMap.has(deal.productId))
specialDealsMap.get(deal.productId)!.push(deal); specialDealsMap.set(deal.productId, [])
specialDealsMap.get(deal.productId)!.push(deal)
} }
// Fetch all product tags // Fetch all product tags
const allProductTags = await db const allProductTags = await getAllProductTagsForCache()
.select({ const productTagsMap = new Map<number, string[]>()
productId: productTags.productId,
tagName: productTagInfo.tagName,
})
.from(productTags)
.innerJoin(productTagInfo, eq(productTags.tagId, productTagInfo.id));
const productTagsMap = new Map<number, string[]>();
for (const tag of allProductTags) { for (const tag of allProductTags) {
if (!productTagsMap.has(tag.productId)) productTagsMap.set(tag.productId, []); if (!productTagsMap.has(tag.productId))
productTagsMap.get(tag.productId)!.push(tag.tagName); productTagsMap.set(tag.productId, [])
productTagsMap.get(tag.productId)!.push(tag.tagName)
} }
// Store each product in Redis // Store each product in Redis
for (const product of productsData) { for (const product of productsData) {
const signedImages = scaffoldAssetUrl((product.images as string[]) || []); const signedImages = scaffoldAssetUrl(
const store = product.storeId ? storeMap.get(product.storeId) || null : null; (product.images as string[]) || []
const deliverySlots = deliverySlotsMap.get(product.id) || []; )
const specialDeals = specialDealsMap.get(product.id) || []; const store = product.storeId
const productTags = productTagsMap.get(product.id) || []; ? storeMap.get(product.storeId) || null
: null
const deliverySlots = deliverySlotsMap.get(product.id) || []
const specialDeals = specialDealsMap.get(product.id) || []
const productTags = productTagsMap.get(product.id) || []
const productObj: Product = { const productObj: Product = {
id: product.id, id: product.id,
@ -129,60 +120,70 @@ export async function initializeProducts(): Promise<void> {
unitNotation: product.unitShortNotation, unitNotation: product.unitShortNotation,
images: signedImages, images: signedImages,
isOutOfStock: product.isOutOfStock, isOutOfStock: product.isOutOfStock,
store: store ? { id: store.id, name: store.name, description: store.description } : null, store: store
? { id: store.id, name: store.name, description: store.description }
: null,
incrementStep: product.incrementStep, incrementStep: product.incrementStep,
productQuantity: product.productQuantity, productQuantity: product.productQuantity,
isFlashAvailable: product.isFlashAvailable, isFlashAvailable: product.isFlashAvailable,
flashPrice: product.flashPrice?.toString() || null, flashPrice: product.flashPrice?.toString() || null,
deliverySlots: deliverySlots.map(s => ({ id: s.id, deliveryTime: s.deliveryTime, freezeTime: s.freezeTime, isCapacityFull: s.isCapacityFull })), deliverySlots: deliverySlots.map((s) => ({
specialDeals: specialDeals.map(d => ({ quantity: d.quantity.toString(), price: d.price.toString(), validTill: d.validTill })), id: s.id,
deliveryTime: s.deliveryTime,
freezeTime: s.freezeTime,
isCapacityFull: s.isCapacityFull,
})),
specialDeals: specialDeals.map((d) => ({
quantity: d.quantity.toString(),
price: d.price.toString(),
validTill: d.validTill,
})),
productTags: productTags, productTags: productTags,
};
await redisClient.set(`product:${product.id}`, JSON.stringify(productObj));
} }
console.log('Product store initialized successfully'); await redisClient.set(`product:${product.id}`, JSON.stringify(productObj))
}
console.log('Product store initialized successfully')
} catch (error) { } catch (error) {
console.error('Error initializing product store:', error); console.error('Error initializing product store:', error)
} }
} }
export async function getProductById(id: number): Promise<Product | null> { export async function getProductById(id: number): Promise<Product | null> {
try { try {
const key = `product:${id}`; const key = `product:${id}`
const data = await redisClient.get(key); const data = await redisClient.get(key)
if (!data) return null; if (!data) return null
return JSON.parse(data) as Product; return JSON.parse(data) as Product
} catch (error) { } catch (error) {
console.error(`Error getting product ${id}:`, error); console.error(`Error getting product ${id}:`, error)
return null; return null
} }
} }
export async function getAllProducts(): Promise<Product[]> { export async function getAllProducts(): Promise<Product[]> {
try { try {
// Get all keys matching the pattern "product:*" // Get all keys matching the pattern "product:*"
const keys = await redisClient.KEYS('product:*'); const keys = await redisClient.KEYS('product:*')
if (keys.length === 0) { if (keys.length === 0) {
return []; return []
} }
// Get all products using MGET for better performance // Get all products using MGET for better performance
const productsData = await redisClient.MGET(keys); const productsData = await redisClient.MGET(keys)
const products: Product[] = []; const products: Product[] = []
for (const productData of productsData) { for (const productData of productsData) {
if (productData) { if (productData) {
products.push(JSON.parse(productData) as Product); products.push(JSON.parse(productData) as Product)
} }
} }
return products; return products
} catch (error) { } catch (error) {
console.error('Error getting all products:', error); console.error('Error getting all products:', error)
return []; return []
} }
} }

View file

@ -1,26 +1,35 @@
// import redisClient from '@/src/stores/redis-client'; import redisClient from '@/src/lib/redis-client'
import redisClient from '@/src/lib/redis-client'; import {
import { db } from '@/src/db/db_index' getAllTagsForCache,
import { productTagInfo, productTags } from '@/src/db/schema' getAllTagProductMappings,
import { eq, inArray } from 'drizzle-orm'; type TagBasicData,
import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client'; type TagProductMapping,
} from '@/src/dbService'
import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client'
// Tag Type (matches getDashboardTags return) // Tag Type (matches getDashboardTags return)
interface Tag { interface Tag {
id: number; id: number
tagName: string; tagName: string
tagDescription: string | null; tagDescription: string | null
imageUrl: string | null; imageUrl: string | null
isDashboardTag: boolean; isDashboardTag: boolean
relatedStores: number[]; relatedStores: number[]
productIds: number[]; productIds: number[]
} }
export async function initializeProductTagStore(): Promise<void> { export async function initializeProductTagStore(): Promise<void> {
try { try {
console.log('Initializing product tag store in Redis...'); console.log('Initializing product tag store in Redis...')
// Fetch all tags // Fetch all tags
const tagsData = await getAllTagsForCache()
/*
// Old implementation - direct DB queries:
import { db } from '@/src/db/db_index'
import { productTagInfo } from '@/src/db/schema'
const tagsData = await db const tagsData = await db
.select({ .select({
id: productTagInfo.id, id: productTagInfo.id,
@ -31,8 +40,16 @@ export async function initializeProductTagStore(): Promise<void> {
relatedStores: productTagInfo.relatedStores, relatedStores: productTagInfo.relatedStores,
}) })
.from(productTagInfo); .from(productTagInfo);
*/
// Fetch product IDs for each tag // Fetch product IDs for each tag
const productTagsData = await getAllTagProductMappings()
/*
// Old implementation - direct DB queries:
import { productTags } from '@/src/db/schema'
import { inArray } from 'drizzle-orm'
const tagIds = tagsData.map(t => t.id); const tagIds = tagsData.map(t => t.id);
const productTagsData = await db const productTagsData = await db
.select({ .select({
@ -41,19 +58,22 @@ export async function initializeProductTagStore(): Promise<void> {
}) })
.from(productTags) .from(productTags)
.where(inArray(productTags.tagId, tagIds)); .where(inArray(productTags.tagId, tagIds));
*/
// Group product IDs by tag // Group product IDs by tag
const productIdsByTag = new Map<number, number[]>(); const productIdsByTag = new Map<number, number[]>()
for (const pt of productTagsData) { for (const pt of productTagsData) {
if (!productIdsByTag.has(pt.tagId)) { if (!productIdsByTag.has(pt.tagId)) {
productIdsByTag.set(pt.tagId, []); productIdsByTag.set(pt.tagId, [])
} }
productIdsByTag.get(pt.tagId)!.push(pt.productId); productIdsByTag.get(pt.tagId)!.push(pt.productId)
} }
// Store each tag in Redis // Store each tag in Redis
for (const tag of tagsData) { for (const tag of tagsData) {
const signedImageUrl = tag.imageUrl ? await generateSignedUrlFromS3Url(tag.imageUrl) : null; const signedImageUrl = tag.imageUrl
? await generateSignedUrlFromS3Url(tag.imageUrl)
: null
const tagObj: Tag = { const tagObj: Tag = {
id: tag.id, id: tag.id,
@ -63,109 +83,109 @@ export async function initializeProductTagStore(): Promise<void> {
isDashboardTag: tag.isDashboardTag, isDashboardTag: tag.isDashboardTag,
relatedStores: (tag.relatedStores as number[]) || [], relatedStores: (tag.relatedStores as number[]) || [],
productIds: productIdsByTag.get(tag.id) || [], productIds: productIdsByTag.get(tag.id) || [],
};
await redisClient.set(`tag:${tag.id}`, JSON.stringify(tagObj));
} }
console.log('Product tag store initialized successfully'); await redisClient.set(`tag:${tag.id}`, JSON.stringify(tagObj))
}
console.log('Product tag store initialized successfully')
} catch (error) { } catch (error) {
console.error('Error initializing product tag store:', error); console.error('Error initializing product tag store:', error)
} }
} }
export async function getTagById(id: number): Promise<Tag | null> { export async function getTagById(id: number): Promise<Tag | null> {
try { try {
const key = `tag:${id}`; const key = `tag:${id}`
const data = await redisClient.get(key); const data = await redisClient.get(key)
if (!data) return null; if (!data) return null
return JSON.parse(data) as Tag; return JSON.parse(data) as Tag
} catch (error) { } catch (error) {
console.error(`Error getting tag ${id}:`, error); console.error(`Error getting tag ${id}:`, error)
return null; return null
} }
} }
export async function getAllTags(): Promise<Tag[]> { export async function getAllTags(): Promise<Tag[]> {
try { try {
// Get all keys matching the pattern "tag:*" // Get all keys matching the pattern "tag:*"
const keys = await redisClient.KEYS('tag:*'); const keys = await redisClient.KEYS('tag:*')
if (keys.length === 0) { if (keys.length === 0) {
return []; return []
} }
// Get all tags using MGET for better performance // Get all tags using MGET for better performance
const tagsData = await redisClient.MGET(keys); const tagsData = await redisClient.MGET(keys)
const tags: Tag[] = []; const tags: Tag[] = []
for (const tagData of tagsData) { for (const tagData of tagsData) {
if (tagData) { if (tagData) {
tags.push(JSON.parse(tagData) as Tag); tags.push(JSON.parse(tagData) as Tag)
} }
} }
return tags; return tags
} catch (error) { } catch (error) {
console.error('Error getting all tags:', error); console.error('Error getting all tags:', error)
return []; return []
} }
} }
export async function getDashboardTags(): Promise<Tag[]> { export async function getDashboardTags(): Promise<Tag[]> {
try { try {
// Get all keys matching the pattern "tag:*" // Get all keys matching the pattern "tag:*"
const keys = await redisClient.KEYS('tag:*'); const keys = await redisClient.KEYS('tag:*')
if (keys.length === 0) { if (keys.length === 0) {
return []; return []
} }
// Get all tags using MGET for better performance // Get all tags using MGET for better performance
const tagsData = await redisClient.MGET(keys); const tagsData = await redisClient.MGET(keys)
const dashboardTags: Tag[] = []; const dashboardTags: Tag[] = []
for (const tagData of tagsData) { for (const tagData of tagsData) {
if (tagData) { if (tagData) {
const tag = JSON.parse(tagData) as Tag; const tag = JSON.parse(tagData) as Tag
if (tag.isDashboardTag) { if (tag.isDashboardTag) {
dashboardTags.push(tag); dashboardTags.push(tag)
} }
} }
} }
return dashboardTags; return dashboardTags
} catch (error) { } catch (error) {
console.error('Error getting dashboard tags:', error); console.error('Error getting dashboard tags:', error)
return []; return []
} }
} }
export async function getTagsByStoreId(storeId: number): Promise<Tag[]> { export async function getTagsByStoreId(storeId: number): Promise<Tag[]> {
try { try {
// Get all keys matching the pattern "tag:*" // Get all keys matching the pattern "tag:*"
const keys = await redisClient.KEYS('tag:*'); const keys = await redisClient.KEYS('tag:*')
if (keys.length === 0) { if (keys.length === 0) {
return []; return []
} }
// Get all tags using MGET for better performance // Get all tags using MGET for better performance
const tagsData = await redisClient.MGET(keys); const tagsData = await redisClient.MGET(keys)
const storeTags: Tag[] = []; const storeTags: Tag[] = []
for (const tagData of tagsData) { for (const tagData of tagsData) {
if (tagData) { if (tagData) {
const tag = JSON.parse(tagData) as Tag; const tag = JSON.parse(tagData) as Tag
if (tag.relatedStores.includes(storeId)) { if (tag.relatedStores.includes(storeId)) {
storeTags.push(tag); storeTags.push(tag)
} }
} }
} }
return storeTags; return storeTags
} catch (error) { } catch (error) {
console.error(`Error getting tags for store ${storeId}:`, error); console.error(`Error getting tags for store ${storeId}:`, error)
return []; return []
} }
} }

View file

@ -1,50 +1,58 @@
import redisClient from '@/src/lib/redis-client'; import redisClient from '@/src/lib/redis-client'
import { db } from '@/src/db/db_index' import {
import { deliverySlotInfo, productSlots, productInfo, units } from '@/src/db/schema' getAllSlotsWithProductsForCache,
import { eq, and, gt, asc } from 'drizzle-orm'; type SlotWithProductsData,
import { generateSignedUrlsFromS3Urls, scaffoldAssetUrl } from '@/src/lib/s3-client'; } from '@/src/dbService'
import dayjs from 'dayjs'; import { scaffoldAssetUrl } from '@/src/lib/s3-client'
import dayjs from 'dayjs'
// Define the structure for slot with products // Define the structure for slot with products
interface SlotWithProducts { interface SlotWithProducts {
id: number; id: number
deliveryTime: Date; deliveryTime: Date
freezeTime: Date; freezeTime: Date
isActive: boolean; isActive: boolean
isCapacityFull: boolean; isCapacityFull: boolean
products: Array<{ products: Array<{
id: number; id: number
name: string; name: string
shortDescription: string | null; shortDescription: string | null
productQuantity: number; productQuantity: number
price: string; price: string
marketPrice: string | null; marketPrice: string | null
unit: string | null; unit: string | null
images: string[]; images: string[]
isOutOfStock: boolean; isOutOfStock: boolean
storeId: number | null; storeId: number | null
nextDeliveryDate: Date; nextDeliveryDate: Date
}>; }>
} }
interface SlotInfo { interface SlotInfo {
id: number; id: number
deliveryTime: Date; deliveryTime: Date
freezeTime: Date; freezeTime: Date
isCapacityFull: boolean; isCapacityFull: boolean
} }
export async function initializeSlotStore(): Promise<void> { export async function initializeSlotStore(): Promise<void> {
try { try {
console.log('Initializing slot store in Redis...'); console.log('Initializing slot store in Redis...')
const now = new Date();
// Fetch active delivery slots with future delivery times // Fetch active delivery slots with future delivery times
const slots = await getAllSlotsWithProductsForCache()
/*
// Old implementation - direct DB queries:
import { db } from '@/src/db/db_index'
import { deliverySlotInfo } from '@/src/db/schema'
import { eq, gt, and, asc } from 'drizzle-orm'
const now = new Date();
const slots = await db.query.deliverySlotInfo.findMany({ const slots = await db.query.deliverySlotInfo.findMany({
where: and( where: and(
eq(deliverySlotInfo.isActive, true), eq(deliverySlotInfo.isActive, true),
gt(deliverySlotInfo.deliveryTime, now), // Only future slots gt(deliverySlotInfo.deliveryTime, now),
), ),
with: { with: {
productSlots: { productSlots: {
@ -60,6 +68,7 @@ export async function initializeSlotStore(): Promise<void> {
}, },
orderBy: asc(deliverySlotInfo.deliveryTime), orderBy: asc(deliverySlotInfo.deliveryTime),
}); });
*/
// Transform data for storage // Transform data for storage
const slotsWithProducts = await Promise.all( const slotsWithProducts = await Promise.all(
@ -79,151 +88,156 @@ export async function initializeSlotStore(): Promise<void> {
marketPrice: productSlot.product.marketPrice?.toString() || null, marketPrice: productSlot.product.marketPrice?.toString() || null,
unit: productSlot.product.unit?.shortNotation || null, unit: productSlot.product.unit?.shortNotation || null,
images: scaffoldAssetUrl( images: scaffoldAssetUrl(
(productSlot.product.images as string[]) || [], (productSlot.product.images as string[]) || []
), ),
isOutOfStock: productSlot.product.isOutOfStock, isOutOfStock: productSlot.product.isOutOfStock,
storeId: productSlot.product.storeId, storeId: productSlot.product.storeId,
nextDeliveryDate: slot.deliveryTime, nextDeliveryDate: slot.deliveryTime,
})), }))
), ),
})), }))
); )
// Store each slot in Redis with key pattern "slot:{id}" // Store each slot in Redis with key pattern "slot:{id}"
for (const slot of slotsWithProducts) { for (const slot of slotsWithProducts) {
await redisClient.set(`slot:${slot.id}`, JSON.stringify(slot)); await redisClient.set(`slot:${slot.id}`, JSON.stringify(slot))
} }
// Build and store product-slots map // Build and store product-slots map
// Group slots by productId // Group slots by productId
const productSlotsMap: Record<number, SlotInfo[]> = {}; const productSlotsMap: Record<number, SlotInfo[]> = {}
for (const slot of slotsWithProducts) { for (const slot of slotsWithProducts) {
for (const product of slot.products) { for (const product of slot.products) {
if (!productSlotsMap[product.id]) { if (!productSlotsMap[product.id]) {
productSlotsMap[product.id] = []; productSlotsMap[product.id] = []
} }
productSlotsMap[product.id].push({ productSlotsMap[product.id].push({
id: slot.id, id: slot.id,
deliveryTime: slot.deliveryTime, deliveryTime: slot.deliveryTime,
freezeTime: slot.freezeTime, freezeTime: slot.freezeTime,
isCapacityFull: slot.isCapacityFull, isCapacityFull: slot.isCapacityFull,
}); })
} }
} }
// Store each product's slots in Redis with key pattern "product:{id}:slots" // Store each product's slots in Redis with key pattern "product:{id}:slots"
for (const [productId, slotInfos] of Object.entries(productSlotsMap)) { for (const [productId, slotInfos] of Object.entries(productSlotsMap)) {
await redisClient.set(`product:${productId}:slots`, JSON.stringify(slotInfos)); await redisClient.set(
`product:${productId}:slots`,
JSON.stringify(slotInfos)
)
} }
console.log('Slot store initialized successfully'); console.log('Slot store initialized successfully')
} catch (error) { } catch (error) {
console.error('Error initializing slot store:', error); console.error('Error initializing slot store:', error)
} }
} }
export async function getSlotById(slotId: number): Promise<SlotWithProducts | null> { export async function getSlotById(slotId: number): Promise<SlotWithProducts | null> {
try { try {
const key = `slot:${slotId}`; const key = `slot:${slotId}`
const data = await redisClient.get(key); const data = await redisClient.get(key)
if (!data) return null; if (!data) return null
return JSON.parse(data) as SlotWithProducts; return JSON.parse(data) as SlotWithProducts
} catch (error) { } catch (error) {
console.error(`Error getting slot ${slotId}:`, error); console.error(`Error getting slot ${slotId}:`, error)
return null; return null
} }
} }
export async function getAllSlots(): Promise<SlotWithProducts[]> { export async function getAllSlots(): Promise<SlotWithProducts[]> {
try { try {
// Get all keys matching the pattern "slot:*" // Get all keys matching the pattern "slot:*"
const keys = await redisClient.KEYS('slot:*'); const keys = await redisClient.KEYS('slot:*')
if (keys.length === 0) return []; if (keys.length === 0) return []
// Get all slots using MGET for better performance // Get all slots using MGET for better performance
const slotsData = await redisClient.MGET(keys); const slotsData = await redisClient.MGET(keys)
const slots: SlotWithProducts[] = []; const slots: SlotWithProducts[] = []
for (const slotData of slotsData) { for (const slotData of slotsData) {
if (slotData) { if (slotData) {
slots.push(JSON.parse(slotData) as SlotWithProducts); slots.push(JSON.parse(slotData) as SlotWithProducts)
} }
} }
return slots; return slots
} catch (error) { } catch (error) {
console.error('Error getting all slots:', error); console.error('Error getting all slots:', error)
return []; return []
} }
} }
export async function getProductSlots(productId: number): Promise<SlotInfo[]> { export async function getProductSlots(productId: number): Promise<SlotInfo[]> {
try { try {
const key = `product:${productId}:slots`; const key = `product:${productId}:slots`
const data = await redisClient.get(key); const data = await redisClient.get(key)
if (!data) return []; if (!data) return []
return JSON.parse(data) as SlotInfo[]; return JSON.parse(data) as SlotInfo[]
} catch (error) { } catch (error) {
console.error(`Error getting slots for product ${productId}:`, error); console.error(`Error getting slots for product ${productId}:`, error)
return []; return []
} }
} }
export async function getAllProductsSlots(): Promise<Record<number, SlotInfo[]>> { export async function getAllProductsSlots(): Promise<Record<number, SlotInfo[]>> {
try { try {
// Get all keys matching the pattern "product:*:slots" // Get all keys matching the pattern "product:*:slots"
const keys = await redisClient.KEYS('product:*:slots'); const keys = await redisClient.KEYS('product:*:slots')
if (keys.length === 0) return {}; if (keys.length === 0) return {}
// Get all product slots using MGET for better performance // Get all product slots using MGET for better performance
const productsData = await redisClient.MGET(keys); const productsData = await redisClient.MGET(keys)
const result: Record<number, SlotInfo[]> = {}; const result: Record<number, SlotInfo[]> = {}
for (const key of keys) { for (const key of keys) {
// Extract productId from key "product:{id}:slots" // Extract productId from key "product:{id}:slots"
const match = key.match(/product:(\d+):slots/); const match = key.match(/product:(\d+):slots/)
if (match) { if (match) {
const productId = parseInt(match[1], 10); const productId = parseInt(match[1], 10)
const dataIndex = keys.indexOf(key); const dataIndex = keys.indexOf(key)
if (productsData[dataIndex]) { if (productsData[dataIndex]) {
result[productId] = JSON.parse(productsData[dataIndex]) as SlotInfo[]; result[productId] = JSON.parse(productsData[dataIndex]) as SlotInfo[]
} }
} }
} }
return result; return result
} catch (error) { } catch (error) {
console.error('Error getting all products slots:', error); console.error('Error getting all products slots:', error)
return {}; return {}
} }
} }
export async function getMultipleProductsSlots(productIds: number[]): Promise<Record<number, SlotInfo[]>> { export async function getMultipleProductsSlots(
productIds: number[]
): Promise<Record<number, SlotInfo[]>> {
try { try {
if (productIds.length === 0) return {}; if (productIds.length === 0) return {}
// Build keys for all productIds // Build keys for all productIds
const keys = productIds.map(id => `product:${id}:slots`); const keys = productIds.map((id) => `product:${id}:slots`)
// Use MGET for batch retrieval // Use MGET for batch retrieval
const productsData = await redisClient.MGET(keys); const productsData = await redisClient.MGET(keys)
const result: Record<number, SlotInfo[]> = {}; const result: Record<number, SlotInfo[]> = {}
for (let i = 0; i < productIds.length; i++) { for (let i = 0; i < productIds.length; i++) {
const data = productsData[i]; const data = productsData[i]
if (data) { if (data) {
const slots = JSON.parse(data) as SlotInfo[]; const slots = JSON.parse(data) as SlotInfo[]
// Filter out slots that are at full capacity // Filter out slots that are at full capacity
result[productIds[i]] = slots.filter(slot => !slot.isCapacityFull); result[productIds[i]] = slots.filter((slot) => !slot.isCapacityFull)
} }
} }
return result; return result
} catch (error) { } catch (error) {
console.error('Error getting products slots:', error); console.error('Error getting products slots:', error)
return {}; return {}
} }
} }

View file

@ -1,11 +1,21 @@
import redisClient from '@/src/lib/redis-client'; import redisClient from '@/src/lib/redis-client'
import { db } from '@/src/db/db_index' import {
import { userIncidents } from '@/src/db/schema' getAllUserNegativityScores as getAllUserNegativityScoresFromDb,
import { eq, sum } from 'drizzle-orm'; getUserNegativityScore as getUserNegativityScoreFromDb,
type UserNegativityData,
} from '@/src/dbService'
export async function initializeUserNegativityStore(): Promise<void> { export async function initializeUserNegativityStore(): Promise<void> {
try { try {
console.log('Initializing user negativity store in Redis...'); console.log('Initializing user negativity store in Redis...')
const results = await getAllUserNegativityScoresFromDb()
/*
// Old implementation - direct DB queries:
import { db } from '@/src/db/db_index'
import { userIncidents } from '@/src/db/schema'
import { sum } from 'drizzle-orm'
const results = await db const results = await db
.select({ .select({
@ -14,91 +24,102 @@ export async function initializeUserNegativityStore(): Promise<void> {
}) })
.from(userIncidents) .from(userIncidents)
.groupBy(userIncidents.userId); .groupBy(userIncidents.userId);
*/
for (const { userId, totalNegativityScore } of results) { for (const { userId, totalNegativityScore } of results) {
await redisClient.set( await redisClient.set(
`user:negativity:${userId}`, `user:negativity:${userId}`,
totalNegativityScore.toString(), totalNegativityScore.toString()
); )
} }
console.log(`User negativity store initialized for ${results.length} users`); console.log(`User negativity store initialized for ${results.length} users`)
} catch (error) { } catch (error) {
console.error('Error initializing user negativity store:', error); console.error('Error initializing user negativity store:', error)
throw error; throw error
} }
} }
export async function getUserNegativity(userId: number): Promise<number> { export async function getUserNegativity(userId: number): Promise<number> {
try { try {
const key = `user:negativity:${userId}`; const key = `user:negativity:${userId}`
const data = await redisClient.get(key); const data = await redisClient.get(key)
if (!data) { if (!data) {
return 0; return 0
} }
return parseInt(data, 10); return parseInt(data, 10)
} catch (error) { } catch (error) {
console.error(`Error getting negativity score for user ${userId}:`, error); console.error(`Error getting negativity score for user ${userId}:`, error)
return 0; return 0
} }
} }
export async function getAllUserNegativityScores(): Promise<Record<number, number>> { export async function getAllUserNegativityScores(): Promise<Record<number, number>> {
try { try {
const keys = await redisClient.KEYS('user:negativity:*'); const keys = await redisClient.KEYS('user:negativity:*')
if (keys.length === 0) return {}; if (keys.length === 0) return {}
const values = await redisClient.MGET(keys); const values = await redisClient.MGET(keys)
const result: Record<number, number> = {}; const result: Record<number, number> = {}
for (let i = 0; i < keys.length; i++) { for (let i = 0; i < keys.length; i++) {
const key = keys[i]; const key = keys[i]
const value = values[i]; const value = values[i]
const match = key.match(/user:negativity:(\d+)/); const match = key.match(/user:negativity:(\d+)/)
if (match && value) { if (match && value) {
const userId = parseInt(match[1], 10); const userId = parseInt(match[1], 10)
result[userId] = parseInt(value, 10); result[userId] = parseInt(value, 10)
} }
} }
return result; return result
} catch (error) { } catch (error) {
console.error('Error getting all user negativity scores:', error); console.error('Error getting all user negativity scores:', error)
return {}; return {}
} }
} }
export async function getMultipleUserNegativityScores(userIds: number[]): Promise<Record<number, number>> { export async function getMultipleUserNegativityScores(
userIds: number[]
): Promise<Record<number, number>> {
try { try {
if (userIds.length === 0) return {}; if (userIds.length === 0) return {}
const keys = userIds.map(id => `user:negativity:${id}`); const keys = userIds.map((id) => `user:negativity:${id}`)
const values = await redisClient.MGET(keys); const values = await redisClient.MGET(keys)
const result: Record<number, number> = {}; const result: Record<number, number> = {}
for (let i = 0; i < userIds.length; i++) { for (let i = 0; i < userIds.length; i++) {
const value = values[i]; const value = values[i]
if (value) { if (value) {
result[userIds[i]] = parseInt(value, 10); result[userIds[i]] = parseInt(value, 10)
} else { } else {
result[userIds[i]] = 0; result[userIds[i]] = 0
} }
} }
return result; return result
} catch (error) { } catch (error) {
console.error('Error getting multiple user negativity scores:', error); console.error('Error getting multiple user negativity scores:', error)
return {}; return {}
} }
} }
export async function recomputeUserNegativityScore(userId: number): Promise<void> { export async function recomputeUserNegativityScore(userId: number): Promise<void> {
try { try {
const totalScore = await getUserNegativityScoreFromDb(userId)
/*
// Old implementation - direct DB queries:
import { db } from '@/src/db/db_index'
import { userIncidents } from '@/src/db/schema'
import { eq, sum } from 'drizzle-orm'
const [result] = await db const [result] = await db
.select({ .select({
totalNegativityScore: sum(userIncidents.negativityScore).mapWith(Number), totalNegativityScore: sum(userIncidents.negativityScore).mapWith(Number),
@ -108,11 +129,12 @@ export async function recomputeUserNegativityScore(userId: number): Promise<void
.limit(1); .limit(1);
const totalScore = result?.totalNegativityScore || 0; const totalScore = result?.totalNegativityScore || 0;
*/
const key = `user:negativity:${userId}`; const key = `user:negativity:${userId}`
await redisClient.set(key, totalScore.toString()); await redisClient.set(key, totalScore.toString())
} catch (error) { } catch (error) {
console.error(`Error recomputing negativity score for user ${userId}:`, error); console.error(`Error recomputing negativity score for user ${userId}:`, error)
throw error; throw error
} }
} }

View file

@ -1,7 +1,10 @@
import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index' import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index'
import { commonRouter } from '@/src/trpc/apis/common-apis/common' import { commonRouter } from '@/src/trpc/apis/common-apis/common'
import { db } from '@/src/db/db_index' import {
import { keyValStore, productInfo, storeInfo } from '@/src/db/schema' getStoresSummary,
healthCheck,
} from '@/src/dbService'
import type { StoresSummaryResponse } from '@packages/shared'
import * as turf from '@turf/turf'; import * as turf from '@turf/turf';
import { z } from 'zod'; import { z } from 'zod';
import { mbnrGeoJson } from '@/src/lib/mbnr-geojson' import { mbnrGeoJson } from '@/src/lib/mbnr-geojson'
@ -37,8 +40,14 @@ export async function scaffoldEssentialConsts() {
export const commonApiRouter = router({ export const commonApiRouter = router({
product: commonRouter, product: commonRouter,
getStoresSummary: publicProcedure getStoresSummary: publicProcedure
.query(async () => { .query(async (): Promise<StoresSummaryResponse> => {
/*
// Old implementation - direct DB queries:
import { db } from '@/src/db/db_index'
import { storeInfo } from '@/src/db/schema'
const stores = await db.query.storeInfo.findMany({ const stores = await db.query.storeInfo.findMany({
columns: { columns: {
id: true, id: true,
@ -46,11 +55,15 @@ export const commonApiRouter = router({
description: true, description: true,
}, },
}); });
*/
const stores = await getStoresSummary();
return { return {
stores, stores,
}; };
}), }),
checkLocationInPolygon: publicProcedure checkLocationInPolygon: publicProcedure
.input(z.object({ .input(z.object({
lat: z.number().min(-90).max(90), lat: z.number().min(-90).max(90),
@ -110,16 +123,23 @@ export const commonApiRouter = router({
} }
return { uploadUrls }; return { uploadUrls };
}), }),
healthCheck: publicProcedure healthCheck: publicProcedure
.query(async () => { .query(async () => {
/*
// Old implementation - direct DB queries:
import { db } from '@/src/db/db_index'
import { keyValStore, productInfo } from '@/src/db/schema'
// Test DB connection by selecting product names // Test DB connection by selecting product names
// await db.select({ name: productInfo.name }).from(productInfo).limit(1); // await db.select({ name: productInfo.name }).from(productInfo).limit(1);
await db.select({ key: keyValStore.key }).from(keyValStore).limit(1); await db.select({ key: keyValStore.key }).from(keyValStore).limit(1);
*/
return { const result = await healthCheck();
status: "ok", return result;
};
}), }),
essentialConsts: publicProcedure essentialConsts: publicProcedure
.query(async () => { .query(async () => {
const response = await scaffoldEssentialConsts(); const response = await scaffoldEssentialConsts();

View file

@ -1,43 +1,35 @@
import { router, publicProcedure } from '@/src/trpc/trpc-index' import { router, publicProcedure } from '@/src/trpc/trpc-index'
import { db } from '@/src/db/db_index' import {
import { productInfo, units, productSlots, deliverySlotInfo, storeInfo } from '@/src/db/schema' getSuspendedProductIds,
import { eq, gt, and, sql, inArray } from 'drizzle-orm'; getNextDeliveryDateWithCapacity,
getStoresSummary,
} from '@/src/dbService'
import { generateSignedUrlsFromS3Urls, generateSignedUrlFromS3Url } from '@/src/lib/s3-client' import { generateSignedUrlsFromS3Urls, generateSignedUrlFromS3Url } from '@/src/lib/s3-client'
import { getAllProducts as getAllProductsFromCache } from '@/src/stores/product-store' import { getAllProducts as getAllProductsFromCache } from '@/src/stores/product-store'
import { getDashboardTags as getDashboardTagsFromCache } from '@/src/stores/product-tag-store' import { getDashboardTags as getDashboardTagsFromCache } from '@/src/stores/product-tag-store'
export const getNextDeliveryDate = async (productId: number): Promise<Date | null> => { // Re-export with original name for backwards compatibility
const result = await db export const getNextDeliveryDate = getNextDeliveryDateWithCapacity
.select({ deliveryTime: deliverySlotInfo.deliveryTime })
.from(productSlots)
.innerJoin(deliverySlotInfo, eq(productSlots.slotId, deliverySlotInfo.id))
.where(
and(
eq(productSlots.productId, productId),
eq(deliverySlotInfo.isActive, true),
eq(deliverySlotInfo.isCapacityFull, false),
gt(deliverySlotInfo.deliveryTime, sql`NOW()`)
)
)
.orderBy(deliverySlotInfo.deliveryTime)
.limit(1);
return result[0]?.deliveryTime || null;
};
export async function scaffoldProducts() { export async function scaffoldProducts() {
// Get all products from cache // Get all products from cache
let products = await getAllProductsFromCache(); let products = await getAllProductsFromCache();
products = products.filter(item => Boolean(item.id)) products = products.filter(item => Boolean(item.id))
/*
// Old implementation - direct DB queries:
import { db } from '@/src/db/db_index'
import { productInfo } from '@/src/db/schema'
import { eq } from 'drizzle-orm';
// Get suspended product IDs to filter them out // Get suspended product IDs to filter them out
const suspendedProducts = await db const suspendedProducts = await db
.select({ id: productInfo.id }) .select({ id: productInfo.id })
.from(productInfo) .from(productInfo)
.where(eq(productInfo.isSuspended, true)); .where(eq(productInfo.isSuspended, true));
*/
const suspendedProductIds = new Set(suspendedProducts.map(sp => sp.id)); const suspendedProductIds = new Set(await getSuspendedProductIds());
// Filter out suspended products // Filter out suspended products
products = products.filter(product => !suspendedProductIds.has(product.id)); products = products.filter(product => !suspendedProductIds.has(product.id));
@ -45,7 +37,7 @@ export async function scaffoldProducts() {
// Format products to match the expected response structure // Format products to match the expected response structure
const formattedProducts = await Promise.all( const formattedProducts = await Promise.all(
products.map(async (product) => { products.map(async (product) => {
const nextDeliveryDate = await getNextDeliveryDate(product.id); const nextDeliveryDate = await getNextDeliveryDateWithCapacity(product.id);
return { return {
id: product.id, id: product.id,
name: product.name, name: product.name,
@ -89,28 +81,18 @@ export const commonRouter = router({
return response; return response;
}), }),
/*
// Old implementation - moved to common-trpc-index.ts:
getStoresSummary: publicProcedure getStoresSummary: publicProcedure
.query(async () => { .query(async () => {
const stores = await db.query.storeInfo.findMany({ const stores = await getStoresSummary();
columns: { return { stores };
id: true,
name: true,
description: true,
},
});
return {
stores,
};
}), }),
healthCheck: publicProcedure healthCheck: publicProcedure
.query(async () => { .query(async () => {
// Test DB connection by selecting product names const result = await healthCheck();
await db.select({ name: productInfo.name }).from(productInfo).limit(1); return result;
return {
status: "ok",
};
}), }),
*/
}); });

View file

@ -1,9 +1,14 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { eq } from 'drizzle-orm'; import {
import { db } from '@/src/db/db_index' getUserAuthByEmail,
import { users, userCreds, userDetails } from '@/src/db/schema' getUserAuthByMobile,
createUserWithProfile,
getUserAuthById,
getUserDetailsByUserId,
updateUserProfile,
} from '@/src/dbService';
import { ApiError } from '@/src/lib/api-error' import { ApiError } from '@/src/lib/api-error'
import catchAsync from '@/src/lib/catch-async' import catchAsync from '@/src/lib/catch-async'
import { jwtSecret } from '@/src/lib/env-exporter'; import { jwtSecret } from '@/src/lib/env-exporter';
@ -79,24 +84,36 @@ export const register = catchAsync(async (req: Request, res: Response, next: Nex
throw new ApiError('Invalid mobile number', 400); throw new ApiError('Invalid mobile number', 400);
} }
// Check if email already exists /*
// Old implementation - direct DB queries:
import { db } from '@/src/db/db_index'
import { users } from '@/src/db/schema'
import { eq } from 'drizzle-orm';
const [existingEmail] = await db const [existingEmail] = await db
.select() .select()
.from(users) .from(users)
.where(eq(users.email, email.toLowerCase())) .where(eq(users.email, email.toLowerCase()))
.limit(1); .limit(1);
*/
// Check if email already exists
const existingEmail = await getUserAuthByEmail(email.toLowerCase());
if (existingEmail) { if (existingEmail) {
throw new ApiError('Email already registered', 409); throw new ApiError('Email already registered', 409);
} }
// Check if mobile already exists /*
// Old implementation - direct DB queries:
const [existingMobile] = await db const [existingMobile] = await db
.select() .select()
.from(users) .from(users)
.where(eq(users.mobile, cleanMobile)) .where(eq(users.mobile, cleanMobile))
.limit(1); .limit(1);
*/
// Check if mobile already exists
const existingMobile = await getUserAuthByMobile(cleanMobile);
if (existingMobile) { if (existingMobile) {
throw new ApiError('Mobile number already registered', 409); throw new ApiError('Mobile number already registered', 409);
} }
@ -104,9 +121,11 @@ export const register = catchAsync(async (req: Request, res: Response, next: Nex
// Hash password // Hash password
const hashedPassword = await bcrypt.hash(password, 12); const hashedPassword = await bcrypt.hash(password, 12);
// Create user and credentials in a transaction /*
// Old implementation - direct DB queries:
import { userCreds, userDetails } from '@/src/db/schema'
const newUser = await db.transaction(async (tx) => { const newUser = await db.transaction(async (tx) => {
// Create user
const [user] = await tx const [user] = await tx
.insert(users) .insert(users)
.values({ .values({
@ -116,24 +135,28 @@ export const register = catchAsync(async (req: Request, res: Response, next: Nex
}) })
.returning(); .returning();
// Create user credentials await tx.insert(userCreds).values({
await tx
.insert(userCreds)
.values({
userId: user.id, userId: user.id,
userPassword: hashedPassword, userPassword: hashedPassword,
}); });
// Create user details with profile image await tx.insert(userDetails).values({
await tx
.insert(userDetails)
.values({
userId: user.id, userId: user.id,
profileImage: profileImageUrl, profileImage: profileImageUrl,
}); });
return user; return user;
}); });
*/
// Create user with profile in transaction
const newUser = await createUserWithProfile({
name: name.trim(),
email: email.toLowerCase().trim(),
mobile: cleanMobile,
hashedPassword,
profileImage: profileImageUrl,
});
const token = generateToken(newUser.id); const token = generateToken(newUser.id);
@ -195,7 +218,12 @@ export const updateProfile = catchAsync(async (req: Request, res: Response, next
} }
} }
// Check if email already exists (if changing email) /*
// Old implementation - direct DB queries:
import { db } from '@/src/db/db_index'
import { users, userCreds, userDetails } from '@/src/db/schema'
import { eq } from 'drizzle-orm';
if (email) { if (email) {
const [existingEmail] = await db const [existingEmail] = await db
.select() .select()
@ -207,8 +235,18 @@ export const updateProfile = catchAsync(async (req: Request, res: Response, next
throw new ApiError('Email already registered', 409); throw new ApiError('Email already registered', 409);
} }
} }
*/
// Check if mobile already exists (if changing mobile) // Check if email already exists (if changing email)
if (email) {
const existingEmail = await getUserAuthByEmail(email.toLowerCase());
if (existingEmail && existingEmail.id !== userId) {
throw new ApiError('Email already registered', 409);
}
}
/*
// Old implementation - direct DB queries:
if (mobile) { if (mobile) {
const cleanMobile = mobile.replace(/\D/g, ''); const cleanMobile = mobile.replace(/\D/g, '');
const [existingMobile] = await db const [existingMobile] = await db
@ -221,8 +259,25 @@ export const updateProfile = catchAsync(async (req: Request, res: Response, next
throw new ApiError('Mobile number already registered', 409); throw new ApiError('Mobile number already registered', 409);
} }
} }
*/
// Update user and user details in a transaction // Check if mobile already exists (if changing mobile)
if (mobile) {
const cleanMobile = mobile.replace(/\D/g, '');
const existingMobile = await getUserAuthByMobile(cleanMobile);
if (existingMobile && existingMobile.id !== userId) {
throw new ApiError('Mobile number already registered', 409);
}
}
// Hash password if provided
let hashedPassword: string | undefined;
if (password) {
hashedPassword = await bcrypt.hash(password, 12);
}
/*
// Old implementation - direct DB queries:
const updatedUser = await db.transaction(async (tx) => { const updatedUser = await db.transaction(async (tx) => {
// Update user table // Update user table
const updateData: any = {}; const updateData: any = {};
@ -231,19 +286,13 @@ export const updateProfile = catchAsync(async (req: Request, res: Response, next
if (mobile) updateData.mobile = mobile.replace(/\D/g, ''); if (mobile) updateData.mobile = mobile.replace(/\D/g, '');
if (Object.keys(updateData).length > 0) { if (Object.keys(updateData).length > 0) {
await tx await tx.update(users).set(updateData).where(eq(users.id, userId));
.update(users)
.set(updateData)
.where(eq(users.id, userId));
} }
// Update password if provided // Update password if provided
if (password) { if (password) {
const hashedPassword = await bcrypt.hash(password, 12); const hashedPassword = await bcrypt.hash(password, 12);
await tx await tx.update(userCreds).set({ userPassword: hashedPassword }).where(eq(userCreds.userId, userId));
.update(userCreds)
.set({ userPassword: hashedPassword })
.where(eq(userCreds.userId, userId));
} }
// Update or insert user details // Update or insert user details
@ -255,44 +304,41 @@ export const updateProfile = catchAsync(async (req: Request, res: Response, next
if (profileImageUrl) userDetailsUpdate.profileImage = profileImageUrl; if (profileImageUrl) userDetailsUpdate.profileImage = profileImageUrl;
userDetailsUpdate.updatedAt = new Date(); userDetailsUpdate.updatedAt = new Date();
// Check if user details record exists const [existingDetails] = await tx.select().from(userDetails).where(eq(userDetails.userId, userId)).limit(1);
const [existingDetails] = await tx
.select()
.from(userDetails)
.where(eq(userDetails.userId, userId))
.limit(1);
if (existingDetails) { if (existingDetails) {
// Update existing record await tx.update(userDetails).set(userDetailsUpdate).where(eq(userDetails.userId, userId));
await tx
.update(userDetails)
.set(userDetailsUpdate)
.where(eq(userDetails.userId, userId));
} else { } else {
// Create new record
userDetailsUpdate.userId = userId; userDetailsUpdate.userId = userId;
userDetailsUpdate.createdAt = new Date(); userDetailsUpdate.createdAt = new Date();
await tx await tx.insert(userDetails).values(userDetailsUpdate);
.insert(userDetails)
.values(userDetailsUpdate);
} }
// Return updated user data const [user] = await tx.select().from(users).where(eq(users.id, userId)).limit(1);
const [user] = await tx
.select()
.from(users)
.where(eq(users.id, userId))
.limit(1);
return user; return user;
}); });
*/
// Update user profile in transaction
const updatedUser = await updateUserProfile(userId, {
name: name?.trim(),
email: email?.toLowerCase().trim(),
mobile: mobile?.replace(/\D/g, ''),
hashedPassword,
profileImage: profileImageUrl,
bio,
dateOfBirth: dateOfBirth ? new Date(dateOfBirth) : undefined,
gender,
occupation,
});
/*
// Old implementation - direct DB queries:
const [userDetail] = await db.select().from(userDetails).where(eq(userDetails.userId, userId)).limit(1);
*/
// Get updated user details for response // Get updated user details for response
const [userDetail] = await db const userDetail = await getUserDetailsByUserId(userId);
.select()
.from(userDetails)
.where(eq(userDetails.userId, userId))
.limit(1);
// Generate signed URL for profile image if it exists // Generate signed URL for profile image if it exists
const profileImageSignedUrl = userDetail?.profileImage const profileImageSignedUrl = userDetail?.profileImage
@ -319,3 +365,10 @@ export const updateProfile = catchAsync(async (req: Request, res: Response, next
data: response, data: response,
}); });
}); });
/*
// Old implementation - direct DB queries:
import { db } from '@/src/db/db_index'
import { users, userCreds, userDetails } from '@/src/db/schema'
import { eq } from 'drizzle-orm';
*/

View file

@ -1,6 +1,5 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { db } from '@/src/db/db_index' import { createUserComplaint } from '@/src/dbService';
import { complaints } from '@/src/db/schema'
import { ApiError } from '@/src/lib/api-error' import { ApiError } from '@/src/lib/api-error'
import catchAsync from '@/src/lib/catch-async' import catchAsync from '@/src/lib/catch-async'
import { imageUploadS3 } from '@/src/lib/s3-client' import { imageUploadS3 } from '@/src/lib/s3-client'
@ -43,15 +42,34 @@ export const raiseComplaint = catchAsync(async (req: Request, res: Response, nex
uploadedImageUrls = await Promise.all(imageUploadPromises); uploadedImageUrls = await Promise.all(imageUploadPromises);
} }
/*
// Old implementation - direct DB queries:
import { db } from '@/src/db/db_index'
import { complaints } from '@/src/db/schema'
await db.insert(complaints).values({ await db.insert(complaints).values({
userId, userId,
orderId: orderIdNum, orderId: orderIdNum,
complaintBody: complaintBody.trim(), complaintBody: complaintBody.trim(),
images: uploadedImageUrls.length > 0 ? uploadedImageUrls : null, images: uploadedImageUrls.length > 0 ? uploadedImageUrls : null,
}); });
*/
await createUserComplaint(
userId,
orderIdNum,
complaintBody.trim(),
uploadedImageUrls.length > 0 ? uploadedImageUrls : null
);
res.status(200).json({ res.status(200).json({
success: true, success: true,
message: 'Complaint raised successfully' message: 'Complaint raised successfully'
}); });
}); });
/*
// Old implementation - direct DB queries:
import { db } from '@/src/db/db_index'
import { complaints } from '@/src/db/schema'
*/

View file

@ -7,6 +7,9 @@ export { db } from './src/db/db_index';
// Re-export schema // Re-export schema
export * from './src/db/schema'; export * from './src/db/schema';
// Export enum types for type safety
export { staffRoleEnum, staffPermissionEnum } from './src/db/schema';
// Admin API helpers - explicitly namespaced exports to avoid duplicates // Admin API helpers - explicitly namespaced exports to avoid duplicates
export { export {
// Banner // Banner
@ -105,6 +108,7 @@ export {
export { export {
// Staff User // Staff User
getStaffUserByName, getStaffUserByName,
getStaffUserById,
getAllStaff, getAllStaff,
getAllUsers, getAllUsers,
getUserWithDetails, getUserWithDetails,
@ -212,6 +216,8 @@ export {
getProductReviews as getUserProductReviews, getProductReviews as getUserProductReviews,
getProductById as getUserProductByIdBasic, getProductById as getUserProductByIdBasic,
createProductReview as createUserProductReview, createProductReview as createUserProductReview,
getAllProductsWithUnits,
type ProductSummaryData,
} from './src/user-apis/product'; } from './src/user-apis/product';
export { export {
@ -237,10 +243,15 @@ export {
getUserById as getUserAuthById, getUserById as getUserAuthById,
getUserCreds as getUserAuthCreds, getUserCreds as getUserAuthCreds,
getUserDetails as getUserAuthDetails, getUserDetails as getUserAuthDetails,
isUserSuspended,
createUserWithCreds as createUserAuthWithCreds, createUserWithCreds as createUserAuthWithCreds,
createUserWithMobile as createUserAuthWithMobile, createUserWithMobile as createUserAuthWithMobile,
upsertUserPassword as upsertUserAuthPassword, upsertUserPassword as upsertUserAuthPassword,
deleteUserAccount as deleteUserAuthAccount, deleteUserAccount as deleteUserAuthAccount,
// UV API helpers
createUserWithProfile,
getUserDetailsByUserId,
updateUserProfile,
} from './src/user-apis/auth'; } from './src/user-apis/auth';
export { export {
@ -284,4 +295,80 @@ export {
getRecentlyDeliveredOrderIds as getUserRecentlyDeliveredOrderIds, getRecentlyDeliveredOrderIds as getUserRecentlyDeliveredOrderIds,
getProductIdsFromOrders as getUserProductIdsFromOrders, getProductIdsFromOrders as getUserProductIdsFromOrders,
getProductsForRecentOrders as getUserProductsForRecentOrders, getProductsForRecentOrders as getUserProductsForRecentOrders,
// Post-order handler helpers
getOrdersByIdsWithFullData,
getOrderByIdWithFullData,
type OrderWithFullData,
type OrderWithCancellationData,
} from './src/user-apis/order'; } from './src/user-apis/order';
// Store Helpers (for cache initialization)
export {
// Banner Store
getAllBannersForCache,
type BannerData,
// Product Store
getAllProductsForCache,
getAllStoresForCache,
getAllDeliverySlotsForCache,
getAllSpecialDealsForCache,
getAllProductTagsForCache,
type ProductBasicData,
type StoreBasicData,
type DeliverySlotData,
type SpecialDealData,
type ProductTagData,
// Product Tag Store
getAllTagsForCache,
getAllTagProductMappings,
type TagBasicData,
type TagProductMapping,
// Slot Store
getAllSlotsWithProductsForCache,
type SlotWithProductsData,
// User Negativity Store
getAllUserNegativityScores,
getUserNegativityScore,
type UserNegativityData,
} from './src/stores/store-helpers';
// Automated Jobs Helpers
export {
toggleFlashDeliveryForItems,
toggleKeyVal,
getAllKeyValStore,
} from './src/lib/automated-jobs';
// Health Check
export {
healthCheck,
} from './src/lib/health-check';
// Common API Helpers
export {
getSuspendedProductIds,
getNextDeliveryDateWithCapacity,
} from './src/user-apis/product';
export {
getStoresSummary,
} from './src/user-apis/stores';
// Delete Orders Helper
export {
deleteOrdersWithRelations,
} from './src/lib/delete-orders';
// Seed Helpers
export {
seedUnits,
seedStaffRoles,
seedStaffPermissions,
seedRolePermissions,
seedKeyValStore,
type UnitSeedData,
type RolePermissionAssignment,
type KeyValSeedData,
type StaffRoleName,
type StaffPermissionName,
} from './src/lib/seed';

View file

@ -18,6 +18,14 @@ export async function getStaffUserByName(name: string): Promise<StaffUser | null
return staff || null; return staff || null;
} }
export async function getStaffUserById(staffId: number): Promise<StaffUser | null> {
const staff = await db.query.staffUsers.findFirst({
where: eq(staffUsers.id, staffId),
});
return staff || null;
}
export async function getAllStaff(): Promise<any[]> { export async function getAllStaff(): Promise<any[]> {
const staff = await db.query.staffUsers.findMany({ const staff = await db.query.staffUsers.findMany({
columns: { columns: {

View file

@ -0,0 +1,41 @@
import { db } from '../db/db_index'
import { productInfo, keyValStore } from '../db/schema'
import { inArray, eq } from 'drizzle-orm'
/**
* Toggle flash delivery availability for specific products
* @param isAvailable - Whether flash delivery should be available
* @param productIds - Array of product IDs to update
*/
export async function toggleFlashDeliveryForItems(
isAvailable: boolean,
productIds: number[]
): Promise<void> {
await db
.update(productInfo)
.set({ isFlashAvailable: isAvailable })
.where(inArray(productInfo.id, productIds))
}
/**
* Update key-value store
* @param key - The key to update
* @param value - The boolean value to set
*/
export async function toggleKeyVal(
key: string,
value: boolean
): Promise<void> {
await db
.update(keyValStore)
.set({ value })
.where(eq(keyValStore.key, key))
}
/**
* Get all key-value store constants
* @returns Array of all key-value pairs
*/
export async function getAllKeyValStore(): Promise<Array<{ key: string; value: any }>> {
return db.select().from(keyValStore)
}

View file

@ -0,0 +1,38 @@
import { db } from '../db/db_index'
import { orders, orderItems, orderStatus, payments, refunds, couponUsage, complaints } from '../db/schema'
import { inArray } from 'drizzle-orm'
/**
* Delete orders and all their related records
* @param orderIds Array of order IDs to delete
* @returns Promise<void>
* @throws Error if deletion fails
*/
export async function deleteOrdersWithRelations(orderIds: number[]): Promise<void> {
if (orderIds.length === 0) {
return
}
// Delete child records first (in correct order to avoid FK constraint errors)
// 1. Delete coupon usage records
await db.delete(couponUsage).where(inArray(couponUsage.orderId, orderIds))
// 2. Delete complaints related to these orders
await db.delete(complaints).where(inArray(complaints.orderId, orderIds))
// 3. Delete refunds
await db.delete(refunds).where(inArray(refunds.orderId, orderIds))
// 4. Delete payments
await db.delete(payments).where(inArray(payments.orderId, orderIds))
// 5. Delete order status records
await db.delete(orderStatus).where(inArray(orderStatus.orderId, orderIds))
// 6. Delete order items
await db.delete(orderItems).where(inArray(orderItems.orderId, orderIds))
// 7. Finally delete the orders themselves
await db.delete(orders).where(inArray(orders.id, orderIds))
}

View file

@ -0,0 +1,18 @@
import { db } from '../db/db_index'
import { keyValStore, productInfo } from '../db/schema'
/**
* Health check - test database connectivity
* Tries to select from keyValStore first, falls back to productInfo
*/
export async function healthCheck(): Promise<{ status: string }> {
try {
// Try keyValStore first (smaller table)
await db.select({ key: keyValStore.key }).from(keyValStore).limit(1)
return { status: 'ok' }
} catch {
// Fallback to productInfo
await db.select({ name: productInfo.name }).from(productInfo).limit(1)
return { status: 'ok' }
}
}

View file

@ -0,0 +1,127 @@
import { db } from '../db/db_index'
import { eq, and } from 'drizzle-orm'
// ============================================================================
// Unit Seed Helper
// ============================================================================
export interface UnitSeedData {
shortNotation: string
fullName: string
}
export async function seedUnits(unitsToSeed: UnitSeedData[]): Promise<void> {
for (const unit of unitsToSeed) {
const { units: unitsTable } = await import('../db/schema')
const existingUnit = await db.query.units.findFirst({
where: eq(unitsTable.shortNotation, unit.shortNotation),
})
if (!existingUnit) {
await db.insert(unitsTable).values(unit)
}
}
}
// ============================================================================
// Staff Role Seed Helper
// ============================================================================
// Type for staff role names based on the enum values in schema
export type StaffRoleName = 'super_admin' | 'admin' | 'marketer' | 'delivery_staff'
export async function seedStaffRoles(rolesToSeed: StaffRoleName[]): Promise<void> {
for (const roleName of rolesToSeed) {
const { staffRoles } = await import('../db/schema')
const existingRole = await db.query.staffRoles.findFirst({
where: eq(staffRoles.roleName, roleName),
})
if (!existingRole) {
await db.insert(staffRoles).values({ roleName })
}
}
}
// ============================================================================
// Staff Permission Seed Helper
// ============================================================================
// Type for staff permission names based on the enum values in schema
export type StaffPermissionName = 'crud_product' | 'make_coupon' | 'crud_staff_users'
export async function seedStaffPermissions(permissionsToSeed: StaffPermissionName[]): Promise<void> {
for (const permissionName of permissionsToSeed) {
const { staffPermissions } = await import('../db/schema')
const existingPermission = await db.query.staffPermissions.findFirst({
where: eq(staffPermissions.permissionName, permissionName),
})
if (!existingPermission) {
await db.insert(staffPermissions).values({ permissionName })
}
}
}
// ============================================================================
// Role-Permission Assignment Helper
// ============================================================================
export interface RolePermissionAssignment {
roleName: StaffRoleName
permissionName: StaffPermissionName
}
export async function seedRolePermissions(assignments: RolePermissionAssignment[]): Promise<void> {
await db.transaction(async (tx) => {
const { staffRoles, staffPermissions, staffRolePermissions } = await import('../db/schema')
for (const assignment of assignments) {
// Get role ID
const role = await tx.query.staffRoles.findFirst({
where: eq(staffRoles.roleName, assignment.roleName),
})
// Get permission ID
const permission = await tx.query.staffPermissions.findFirst({
where: eq(staffPermissions.permissionName, assignment.permissionName),
})
if (role && permission) {
const existing = await tx.query.staffRolePermissions.findFirst({
where: and(
eq(staffRolePermissions.staffRoleId, role.id),
eq(staffRolePermissions.staffPermissionId, permission.id)
),
})
if (!existing) {
await tx.insert(staffRolePermissions).values({
staffRoleId: role.id,
staffPermissionId: permission.id,
})
}
}
}
})
}
// ============================================================================
// Key-Value Store Seed Helper
// ============================================================================
export interface KeyValSeedData {
key: string
value: any
}
export async function seedKeyValStore(constantsToSeed: KeyValSeedData[]): Promise<void> {
for (const constant of constantsToSeed) {
const { keyValStore } = await import('../db/schema')
const existing = await db.query.keyValStore.findFirst({
where: eq(keyValStore.key, constant.key),
})
if (!existing) {
await db.insert(keyValStore).values({
key: constant.key,
value: constant.value,
})
}
}
}

View file

@ -0,0 +1,276 @@
// Store Helpers - Database operations for cache initialization
// These are used by stores in apps/backend/src/stores/
import { db } from '../db/db_index'
import {
homeBanners,
productInfo,
units,
productSlots,
deliverySlotInfo,
specialDeals,
storeInfo,
productTags,
productTagInfo,
userIncidents,
} from '../db/schema'
import { eq, and, gt, sql, inArray, isNotNull, asc, sum } from 'drizzle-orm'
// ============================================================================
// BANNER STORE HELPERS
// ============================================================================
export interface BannerData {
id: number
name: string
imageUrl: string | null
serialNum: number | null
productIds: number[] | null
createdAt: Date
}
export async function getAllBannersForCache(): Promise<BannerData[]> {
return db.query.homeBanners.findMany({
where: isNotNull(homeBanners.serialNum),
orderBy: asc(homeBanners.serialNum),
})
}
// ============================================================================
// PRODUCT STORE HELPERS
// ============================================================================
export interface ProductBasicData {
id: number
name: string
shortDescription: string | null
longDescription: string | null
price: string
marketPrice: string | null
images: unknown
isOutOfStock: boolean
storeId: number | null
unitShortNotation: string
incrementStep: number
productQuantity: number
isFlashAvailable: boolean
flashPrice: string | null
}
export interface StoreBasicData {
id: number
name: string
description: string | null
}
export interface DeliverySlotData {
productId: number
id: number
deliveryTime: Date
freezeTime: Date
isCapacityFull: boolean
}
export interface SpecialDealData {
productId: number
quantity: string
price: string
validTill: Date
}
export interface ProductTagData {
productId: number
tagName: string
}
export async function getAllProductsForCache(): Promise<ProductBasicData[]> {
return db
.select({
id: productInfo.id,
name: productInfo.name,
shortDescription: productInfo.shortDescription,
longDescription: productInfo.longDescription,
price: productInfo.price,
marketPrice: productInfo.marketPrice,
images: productInfo.images,
isOutOfStock: productInfo.isOutOfStock,
storeId: productInfo.storeId,
unitShortNotation: units.shortNotation,
incrementStep: productInfo.incrementStep,
productQuantity: productInfo.productQuantity,
isFlashAvailable: productInfo.isFlashAvailable,
flashPrice: productInfo.flashPrice,
})
.from(productInfo)
.innerJoin(units, eq(productInfo.unitId, units.id))
}
export async function getAllStoresForCache(): Promise<StoreBasicData[]> {
return db.query.storeInfo.findMany({
columns: { id: true, name: true, description: true },
})
}
export async function getAllDeliverySlotsForCache(): Promise<DeliverySlotData[]> {
return db
.select({
productId: productSlots.productId,
id: deliverySlotInfo.id,
deliveryTime: deliverySlotInfo.deliveryTime,
freezeTime: deliverySlotInfo.freezeTime,
isCapacityFull: deliverySlotInfo.isCapacityFull,
})
.from(productSlots)
.innerJoin(deliverySlotInfo, eq(productSlots.slotId, deliverySlotInfo.id))
.where(
and(
eq(deliverySlotInfo.isActive, true),
eq(deliverySlotInfo.isCapacityFull, false),
gt(deliverySlotInfo.deliveryTime, sql`NOW()`)
)
)
}
export async function getAllSpecialDealsForCache(): Promise<SpecialDealData[]> {
return db
.select({
productId: specialDeals.productId,
quantity: specialDeals.quantity,
price: specialDeals.price,
validTill: specialDeals.validTill,
})
.from(specialDeals)
.where(gt(specialDeals.validTill, sql`NOW()`))
}
export async function getAllProductTagsForCache(): Promise<ProductTagData[]> {
return db
.select({
productId: productTags.productId,
tagName: productTagInfo.tagName,
})
.from(productTags)
.innerJoin(productTagInfo, eq(productTags.tagId, productTagInfo.id))
}
// ============================================================================
// PRODUCT TAG STORE HELPERS
// ============================================================================
export interface TagBasicData {
id: number
tagName: string
tagDescription: string | null
imageUrl: string | null
isDashboardTag: boolean
relatedStores: unknown
}
export interface TagProductMapping {
tagId: number
productId: number
}
export async function getAllTagsForCache(): Promise<TagBasicData[]> {
return db
.select({
id: productTagInfo.id,
tagName: productTagInfo.tagName,
tagDescription: productTagInfo.tagDescription,
imageUrl: productTagInfo.imageUrl,
isDashboardTag: productTagInfo.isDashboardTag,
relatedStores: productTagInfo.relatedStores,
})
.from(productTagInfo)
}
export async function getAllTagProductMappings(): Promise<TagProductMapping[]> {
return db
.select({
tagId: productTags.tagId,
productId: productTags.productId,
})
.from(productTags)
}
// ============================================================================
// SLOT STORE HELPERS
// ============================================================================
export interface SlotWithProductsData {
id: number
deliveryTime: Date
freezeTime: Date
isActive: boolean
isCapacityFull: boolean
productSlots: Array<{
product: {
id: number
name: string
productQuantity: number
shortDescription: string | null
price: string
marketPrice: string | null
unit: { shortNotation: string } | null
store: { id: number; name: string; description: string | null } | null
images: unknown
isOutOfStock: boolean
storeId: number | null
}
}>
}
export async function getAllSlotsWithProductsForCache(): Promise<SlotWithProductsData[]> {
const now = new Date()
return db.query.deliverySlotInfo.findMany({
where: and(
eq(deliverySlotInfo.isActive, true),
gt(deliverySlotInfo.deliveryTime, now),
),
with: {
productSlots: {
with: {
product: {
with: {
unit: true,
store: true,
},
},
},
},
},
orderBy: asc(deliverySlotInfo.deliveryTime),
}) as Promise<SlotWithProductsData[]>
}
// ============================================================================
// USER NEGATIVITY STORE HELPERS
// ============================================================================
export interface UserNegativityData {
userId: number
totalNegativityScore: number
}
export async function getAllUserNegativityScores(): Promise<UserNegativityData[]> {
return db
.select({
userId: userIncidents.userId,
totalNegativityScore: sum(userIncidents.negativityScore).mapWith(Number),
})
.from(userIncidents)
.groupBy(userIncidents.userId)
}
export async function getUserNegativityScore(userId: number): Promise<number> {
const [result] = await db
.select({
totalNegativityScore: sum(userIncidents.negativityScore).mapWith(Number),
})
.from(userIncidents)
.where(eq(userIncidents.userId, userId))
.limit(1)
return result?.totalNegativityScore || 0
}

View file

@ -45,6 +45,103 @@ export async function getUserDetails(userId: number) {
return details || null return details || null
} }
export async function isUserSuspended(userId: number): Promise<boolean> {
const details = await getUserDetails(userId)
return details?.isSuspended ?? false
}
export async function createUserWithProfile(input: {
name: string
email: string
mobile: string
hashedPassword: string
profileImage?: string | null
}) {
return db.transaction(async (tx) => {
// Create user
const [user] = await tx.insert(users).values({
name: input.name,
email: input.email,
mobile: input.mobile,
}).returning()
// Create user credentials
await tx.insert(userCreds).values({
userId: user.id,
userPassword: input.hashedPassword,
})
// Create user details with profile image
await tx.insert(userDetails).values({
userId: user.id,
profileImage: input.profileImage || null,
})
return user
})
}
export async function getUserDetailsByUserId(userId: number) {
const [details] = await db.select().from(userDetails).where(eq(userDetails.userId, userId)).limit(1)
return details || null
}
export async function updateUserProfile(userId: number, data: {
name?: string
email?: string
mobile?: string
hashedPassword?: string
profileImage?: string
bio?: string
dateOfBirth?: Date | null
gender?: string
occupation?: string
}) {
return db.transaction(async (tx) => {
// Update user table
const userUpdate: any = {}
if (data.name !== undefined) userUpdate.name = data.name
if (data.email !== undefined) userUpdate.email = data.email
if (data.mobile !== undefined) userUpdate.mobile = data.mobile
if (Object.keys(userUpdate).length > 0) {
await tx.update(users).set(userUpdate).where(eq(users.id, userId))
}
// Update password if provided
if (data.hashedPassword) {
await tx.update(userCreds).set({
userPassword: data.hashedPassword,
}).where(eq(userCreds.userId, userId))
}
// Update or insert user details
const detailsUpdate: any = {}
if (data.bio !== undefined) detailsUpdate.bio = data.bio
if (data.dateOfBirth !== undefined) detailsUpdate.dateOfBirth = data.dateOfBirth
if (data.gender !== undefined) detailsUpdate.gender = data.gender
if (data.occupation !== undefined) detailsUpdate.occupation = data.occupation
if (data.profileImage !== undefined) detailsUpdate.profileImage = data.profileImage
detailsUpdate.updatedAt = new Date()
const [existingDetails] = await tx.select().from(userDetails).where(eq(userDetails.userId, userId)).limit(1)
if (existingDetails) {
await tx.update(userDetails).set(detailsUpdate).where(eq(userDetails.userId, userId))
} else {
await tx.insert(userDetails).values({
userId,
...detailsUpdate,
createdAt: new Date(),
})
}
// Return updated user
const [user] = await tx.select().from(users).where(eq(users.id, userId)).limit(1)
return user
})
}
export async function createUserWithCreds(input: { export async function createUserWithCreds(input: {
name: string name: string
email: string email: string

View file

@ -30,10 +30,16 @@ export async function getUserComplaints(userId: number): Promise<UserComplaint[]
})) }))
} }
export async function createComplaint(userId: number, orderId: number | null, complaintBody: string): Promise<void> { export async function createComplaint(
userId: number,
orderId: number | null,
complaintBody: string,
images?: string[] | null
): Promise<void> {
await db.insert(complaints).values({ await db.insert(complaints).values({
userId, userId,
orderId, orderId,
complaintBody, complaintBody,
images: images || null,
}) })
} }

View file

@ -622,3 +622,112 @@ export async function getProductsForRecentOrders(
.orderBy(desc(productInfo.createdAt)) .orderBy(desc(productInfo.createdAt))
.limit(limit) .limit(limit)
} }
// ============================================================================
// Post-Order Handler Helpers (for Telegram notifications)
// ============================================================================
export interface OrderWithFullData {
id: number
totalAmount: string
isFlashDelivery: boolean
address: {
name: string | null
addressLine1: string | null
addressLine2: string | null
city: string | null
state: string | null
pincode: string | null
phone: string | null
} | null
orderItems: Array<{
quantity: string
product: {
name: string
} | null
}>
slot: {
deliveryTime: Date
} | null
}
export async function getOrdersByIdsWithFullData(
orderIds: number[]
): Promise<OrderWithFullData[]> {
return db.query.orders.findMany({
where: inArray(orders.id, orderIds),
with: {
address: {
columns: {
name: true,
addressLine1: true,
addressLine2: true,
city: true,
state: true,
pincode: true,
phone: true,
},
},
orderItems: {
with: {
product: {
columns: {
name: true,
},
},
},
},
slot: {
columns: {
deliveryTime: true,
},
},
},
}) as Promise<OrderWithFullData[]>
}
export interface OrderWithCancellationData extends OrderWithFullData {
refunds: Array<{
refundStatus: string
}>
}
export async function getOrderByIdWithFullData(
orderId: number
): Promise<OrderWithCancellationData | null> {
return db.query.orders.findFirst({
where: eq(orders.id, orderId),
with: {
address: {
columns: {
name: true,
addressLine1: true,
addressLine2: true,
city: true,
state: true,
pincode: true,
phone: true,
},
},
orderItems: {
with: {
product: {
columns: {
name: true,
},
},
},
},
slot: {
columns: {
deliveryTime: true,
},
},
refunds: {
columns: {
refundStatus: true,
},
},
},
}) as Promise<OrderWithCancellationData | null>
}

View file

@ -1,6 +1,6 @@
import { db } from '../db/db_index' import { db } from '../db/db_index'
import { deliverySlotInfo, productInfo, productReviews, productSlots, specialDeals, storeInfo, units, users } from '../db/schema' import { deliverySlotInfo, productInfo, productReviews, productSlots, productTags, specialDeals, storeInfo, units, users } from '../db/schema'
import { and, desc, eq, gt, sql } from 'drizzle-orm' import { and, desc, eq, gt, inArray, sql } from 'drizzle-orm'
import type { UserProductDetailData, UserProductReview } from '@packages/shared' import type { UserProductDetailData, UserProductReview } from '@packages/shared'
const getStringArray = (value: unknown): string[] | null => { const getStringArray = (value: unknown): string[] | null => {
@ -179,3 +179,87 @@ export async function createProductReview(
userName: null, userName: null,
} }
} }
export interface ProductSummaryData {
id: number
name: string
shortDescription: string | null
price: string
marketPrice: string | null
images: unknown
isOutOfStock: boolean
unitShortNotation: string
productQuantity: number
}
export async function getAllProductsWithUnits(tagId?: number): Promise<ProductSummaryData[]> {
let productIds: number[] | null = null
// If tagId is provided, get products that have this tag
if (tagId) {
const taggedProducts = await db
.select({ productId: productTags.productId })
.from(productTags)
.where(eq(productTags.tagId, tagId))
productIds = taggedProducts.map(tp => tp.productId)
}
let whereCondition = undefined
// Filter by product IDs if tag filtering is applied
if (productIds && productIds.length > 0) {
whereCondition = inArray(productInfo.id, productIds)
}
return db
.select({
id: productInfo.id,
name: productInfo.name,
shortDescription: productInfo.shortDescription,
price: productInfo.price,
marketPrice: productInfo.marketPrice,
images: productInfo.images,
isOutOfStock: productInfo.isOutOfStock,
unitShortNotation: units.shortNotation,
productQuantity: productInfo.productQuantity,
})
.from(productInfo)
.innerJoin(units, eq(productInfo.unitId, units.id))
.where(whereCondition)
}
/**
* Get all suspended product IDs
*/
export async function getSuspendedProductIds(): Promise<number[]> {
const suspendedProducts = await db
.select({ id: productInfo.id })
.from(productInfo)
.where(eq(productInfo.isSuspended, true))
return suspendedProducts.map(sp => sp.id)
}
/**
* Get next delivery date for a product (with capacity check)
* This version filters by both isActive AND isCapacityFull
*/
export async function getNextDeliveryDateWithCapacity(productId: number): Promise<Date | null> {
const result = await db
.select({ deliveryTime: deliverySlotInfo.deliveryTime })
.from(productSlots)
.innerJoin(deliverySlotInfo, eq(productSlots.slotId, deliverySlotInfo.id))
.where(
and(
eq(productSlots.productId, productId),
eq(deliverySlotInfo.isActive, true),
eq(deliverySlotInfo.isCapacityFull, false),
gt(deliverySlotInfo.deliveryTime, sql`NOW()`)
)
)
.orderBy(deliverySlotInfo.deliveryTime)
.limit(1)
return result[0]?.deliveryTime || null
}

View file

@ -2,7 +2,7 @@ import { db } from '../db/db_index'
import { productInfo, storeInfo, units } from '../db/schema' import { productInfo, storeInfo, units } from '../db/schema'
import { and, eq, sql } from 'drizzle-orm' import { and, eq, sql } from 'drizzle-orm'
import type { InferSelectModel } from 'drizzle-orm' import type { InferSelectModel } from 'drizzle-orm'
import type { UserStoreDetailData, UserStoreProductData, UserStoreSummaryData } from '@packages/shared' import type { UserStoreDetailData, UserStoreProductData, UserStoreSummaryData, StoreSummary } from '@packages/shared'
type StoreRow = InferSelectModel<typeof storeInfo> type StoreRow = InferSelectModel<typeof storeInfo>
type StoreProductRow = { type StoreProductRow = {
@ -125,3 +125,17 @@ export async function getStoreDetail(storeId: number): Promise<UserStoreDetailDa
products, products,
} }
} }
/**
* Get simple store summary (id, name, description only)
* Used for common API endpoints
*/
export async function getStoresSummary(): Promise<StoreSummary[]> {
return db.query.storeInfo.findMany({
columns: {
id: true,
name: true,
description: true,
},
})
}

View file

@ -3,3 +3,4 @@
export type * from './admin'; export type * from './admin';
export type * from './user'; export type * from './user';
export type * from './store.types';

View file

@ -1,14 +1,22 @@
/** /**
* Store Types * Store Types
* Central type definitions for store-related data structures * Central type definitions for store-related data structures
* Note: Store interface is defined in admin.ts to avoid duplication
*/ */
export interface Store { /**
* Store summary for dropdowns/forms
* Minimal data for store selection UI
*/
export interface StoreSummary {
id: number; id: number;
name: string; name: string;
description: string | null; description: string | null;
imageUrl: string | null; }
owner: number;
createdAt: Date; /**
updatedAt: Date; * Response type for getStoresSummary endpoint
*/
export interface StoresSummaryResponse {
stores: StoreSummary[];
} }