From 89de986764f6a048f22bf41135696aab1740b515 Mon Sep 17 00:00:00 2001 From: shafi54 <108669266+shafi-aviz@users.noreply.github.com> Date: Thu, 26 Mar 2026 12:07:49 +0530 Subject: [PATCH] enh --- .../apis/common-product.controller.ts | 68 ++- apps/backend/src/db/porter.ts | 125 ------ apps/backend/src/db/seed.ts | 76 +++- apps/backend/src/dbService.ts | 3 + .../src/jobs/payment-status-checker.ts | 93 ++-- apps/backend/src/lib/automatedJobs.ts | 91 +++- apps/backend/src/lib/cloud_cache.ts | 41 +- apps/backend/src/lib/const-store.ts | 27 +- apps/backend/src/lib/delete-orders.ts | 25 +- apps/backend/src/lib/notif-job.ts | 2 +- apps/backend/src/lib/post-order-handler.ts | 25 +- .../backend/src/middleware/auth.middleware.ts | 34 +- apps/backend/src/middleware/staff-auth.ts | 19 +- apps/backend/src/postgresImporter.ts | 57 +++ .../src/services/user/order-service.ts | 405 ------------------ .../src/services/user/product-service.ts | 138 ------ apps/backend/src/stores/banner-store.ts | 89 ++-- apps/backend/src/stores/product-store.ts | 195 ++++----- apps/backend/src/stores/product-tag-store.ts | 132 +++--- apps/backend/src/stores/slot-store.ts | 182 ++++---- .../src/stores/user-negativity-store.ts | 108 +++-- .../apis/common-apis/common-trpc-index.ts | 42 +- .../src/trpc/apis/common-apis/common.ts | 64 +-- apps/backend/src/uv-apis/auth.controller.ts | 171 +++++--- .../src/uv-apis/user-rest.controller.ts | 24 +- packages/db_helper_postgres/index.ts | 87 ++++ .../src/admin-apis/staff-user.ts | 8 + .../src/lib/automated-jobs.ts | 41 ++ .../src/lib/delete-orders.ts | 38 ++ .../src/lib/health-check.ts | 18 + packages/db_helper_postgres/src/lib/seed.ts | 127 ++++++ .../src/stores/store-helpers.ts | 276 ++++++++++++ .../db_helper_postgres/src/user-apis/auth.ts | 97 +++++ .../src/user-apis/complaint.ts | 8 +- .../db_helper_postgres/src/user-apis/order.ts | 109 +++++ .../src/user-apis/product.ts | 88 +++- .../src/user-apis/stores.ts | 16 +- packages/shared/types/index.ts | 1 + packages/shared/types/store.types.ts | 18 +- 39 files changed, 1947 insertions(+), 1221 deletions(-) delete mode 100644 apps/backend/src/db/porter.ts delete mode 100644 apps/backend/src/services/user/order-service.ts delete mode 100644 apps/backend/src/services/user/product-service.ts create mode 100644 packages/db_helper_postgres/src/lib/automated-jobs.ts create mode 100644 packages/db_helper_postgres/src/lib/delete-orders.ts create mode 100644 packages/db_helper_postgres/src/lib/health-check.ts create mode 100644 packages/db_helper_postgres/src/lib/seed.ts create mode 100644 packages/db_helper_postgres/src/stores/store-helpers.ts diff --git a/apps/backend/src/apis/common-apis/apis/common-product.controller.ts b/apps/backend/src/apis/common-apis/apis/common-product.controller.ts index 93bd682..3594a8f 100644 --- a/apps/backend/src/apis/common-apis/apis/common-product.controller.ts +++ b/apps/backend/src/apis/common-apis/apis/common-product.controller.ts @@ -1,12 +1,67 @@ -import { eq, gt, and, sql, inArray } from "drizzle-orm"; 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 { 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 => { const result = await db .select({ deliveryTime: deliverySlotInfo.deliveryTime }) @@ -22,13 +77,9 @@ const getNextDeliveryDate = async (productId: number): Promise => { .orderBy(deliverySlotInfo.deliveryTime) .limit(1); - return result[0]?.deliveryTime || null; }; -/** - * Get all products summary for dropdown - */ export const getAllProductsSummary = async (req: Request, res: Response) => { try { 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" }); } }; +*/ diff --git a/apps/backend/src/db/porter.ts b/apps/backend/src/db/porter.ts deleted file mode 100644 index 4173d6f..0000000 --- a/apps/backend/src/db/porter.ts +++ /dev/null @@ -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); - }); diff --git a/apps/backend/src/db/seed.ts b/apps/backend/src/db/seed.ts index f894534..62fddbf 100644 --- a/apps/backend/src/db/seed.ts +++ b/apps/backend/src/db/seed.ts @@ -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 { units, productInfo, deliverySlotInfo, productSlots, keyValStore, staffRoles, staffPermissions, staffRolePermissions } from "@/src/db/schema" 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() { console.log("Seeding database..."); @@ -136,3 +207,4 @@ export async function seed() { console.log("Seeding completed."); } +*/ diff --git a/apps/backend/src/dbService.ts b/apps/backend/src/dbService.ts index 49f4478..b9551d5 100644 --- a/apps/backend/src/dbService.ts +++ b/apps/backend/src/dbService.ts @@ -155,6 +155,9 @@ export type { UserUpdateNotesResponse, UserRecentProduct, UserRecentProductsResponse, + // Store types + StoreSummary, + StoresSummaryResponse, } from '@packages/shared'; export type { diff --git a/apps/backend/src/jobs/payment-status-checker.ts b/apps/backend/src/jobs/payment-status-checker.ts index b1d504e..b1869f1 100644 --- a/apps/backend/src/jobs/payment-status-checker.ts +++ b/apps/backend/src/jobs/payment-status-checker.ts @@ -1,12 +1,9 @@ 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 { - payment: typeof payments.$inferSelect; - order: typeof orders.$inferSelect; - slot: typeof deliverySlotInfo.$inferSelect; + payment: any; + order: any; + slot: any; } export const createPaymentNotification = (record: PendingPaymentRecord) => { @@ -19,34 +16,60 @@ export const createPaymentNotification = (record: PendingPaymentRecord) => { export const checkRefundStatuses = async () => { try { - // const initiatedRefunds = await db - // .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); + // TODO: Reimplement with helpers from @/src/dbService + // This function checks Razorpay refund status and updates database + // Requires: getPendingRefunds(), updateRefundStatus() + } catch (error) { + console.error('Error in checkRefundStatuses:', error); + } +}; + +export const checkPendingPayments = async () => { + try { + // TODO: Reimplement with helpers from @/src/dbService + // This function finds pending payments and sends notifications + // Requires: getPendingPaymentsWithOrders() + } catch (error) { + console.error('Error checking pending payments:', error); + } +}; + +/* +// Old implementation - direct DB queries: +import { db } from '@/src/db/db_index' +import { payments, orders, deliverySlotInfo, refunds } from '@/src/db/schema' +import { eq, and, gt, isNotNull } from 'drizzle-orm'; + +export const checkRefundStatuses = async () => { + try { + const initiatedRefunds = await db + .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) { console.error('Error in checkRefundStatuses:', error); } @@ -75,4 +98,4 @@ export const checkPendingPayments = async () => { console.error('Error checking pending payments:', error); } }; - +*/ diff --git a/apps/backend/src/lib/automatedJobs.ts b/apps/backend/src/lib/automatedJobs.ts index 3142afe..8e7f3dd 100644 --- a/apps/backend/src/lib/automatedJobs.ts +++ b/apps/backend/src/lib/automatedJobs.ts @@ -1,7 +1,5 @@ import * as cron from 'node-cron'; -import { db } from '@/src/db/db_index' -import { productInfo, keyValStore } from '@/src/db/schema' -import { inArray, eq } from 'drizzle-orm'; +import { toggleFlashDeliveryForItems, toggleKeyVal } from '@/src/dbService'; import { CONST_KEYS } from '@/src/lib/const-keys' import { computeConstants } from '@/src/lib/const-store' @@ -24,10 +22,7 @@ export const startAutomatedJobs = () => { cron.schedule('0 12 * * *', async () => { try { console.log('Disabling flash delivery for products at 12 PM'); - await db - .update(productInfo) - .set({ isFlashAvailable: false }) - .where(inArray(productInfo.id, MUTTON_ITEMS)); + await toggleFlashDeliveryForItems(false, MUTTON_ITEMS); console.log('Flash delivery disabled successfully'); } catch (error) { console.error('Error disabling flash delivery:', error); @@ -38,10 +33,7 @@ export const startAutomatedJobs = () => { cron.schedule('0 6 * * *', async () => { try { console.log('Enabling flash delivery for products at 5 AM'); - await db - .update(productInfo) - .set({ isFlashAvailable: true }) - .where(inArray(productInfo.id, MUTTON_ITEMS)); + await toggleFlashDeliveryForItems(true, MUTTON_ITEMS); console.log('Flash delivery enabled successfully'); } catch (error) { console.error('Error enabling flash delivery:', error); @@ -52,10 +44,7 @@ export const startAutomatedJobs = () => { cron.schedule('0 21 * * *', async () => { try { console.log('Disabling flash delivery feature at 9 PM'); - await db - .update(keyValStore) - .set({ value: false }) - .where(eq(keyValStore.key, CONST_KEYS.isFlashDeliveryEnabled)); + await toggleKeyVal(CONST_KEYS.isFlashDeliveryEnabled, false); await computeConstants(); // Refresh Redis cache console.log('Flash delivery feature disabled successfully'); } catch (error) { @@ -67,10 +56,7 @@ export const startAutomatedJobs = () => { cron.schedule('0 6 * * *', async () => { try { console.log('Enabling flash delivery feature at 6 AM'); - await db - .update(keyValStore) - .set({ value: true }) - .where(eq(keyValStore.key, CONST_KEYS.isFlashDeliveryEnabled)); + await toggleKeyVal(CONST_KEYS.isFlashDeliveryEnabled, true); await computeConstants(); // Refresh Redis cache console.log('Flash delivery feature enabled successfully'); } catch (error) { @@ -81,5 +67,70 @@ export const startAutomatedJobs = () => { 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 +cron.schedule('0 12 * * *', async () => { + try { + console.log('Disabling flash delivery for products at 12 PM'); + await db + .update(productInfo) + .set({ isFlashAvailable: false }) + .where(inArray(productInfo.id, 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 db + .update(productInfo) + .set({ isFlashAvailable: true }) + .where(inArray(productInfo.id, 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 db + .update(keyValStore) + .set({ value: false }) + .where(eq(keyValStore.key, CONST_KEYS.isFlashDeliveryEnabled)); + 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 db + .update(keyValStore) + .set({ value: true }) + .where(eq(keyValStore.key, CONST_KEYS.isFlashDeliveryEnabled)); + await computeConstants(); // Refresh Redis cache + console.log('Flash delivery feature enabled successfully'); + } catch (error) { + console.error('Error enabling flash delivery feature:', error); + } +}); +*/ + // Optional: Call on import if desired, or export and call in main app -// startAutomatedJobs(); \ No newline at end of file +// startAutomatedJobs(); diff --git a/apps/backend/src/lib/cloud_cache.ts b/apps/backend/src/lib/cloud_cache.ts index 1db648d..bef3e1c 100644 --- a/apps/backend/src/lib/cloud_cache.ts +++ b/apps/backend/src/lib/cloud_cache.ts @@ -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 { scaffoldBanners } from '@/src/trpc/apis/user-apis/apis/banners' import { scaffoldStoreWithProducts } from '@/src/trpc/apis/user-apis/apis/stores' -import { storeInfo } from '@/src/db/schema' -import { db } from '@/src/db/db_index' +import { getStoresSummary } from '@/src/dbService' import { imageUploadS3 } from '@/src/lib/s3-client' import { apiCacheKey, cloudflareApiToken, cloudflareZoneId, assetsDomain } from '@/src/lib/env-exporter' import { CACHE_FILENAMES } from '@packages/shared' @@ -167,13 +166,21 @@ export async function createStoreFile(storeId: number): Promise { } export async function createAllStoresFiles(): Promise { - // 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) + */ + + // Fetch all store IDs from database using helper + const stores = await getStoresSummary() // Create cache files for all stores and collect URLs const results: string[] = [] const urls: string[] = [] - + for (const store of stores) { const s3Key = await createStoreFile(store.id) results.push(s3Key) @@ -181,7 +188,7 @@ export async function createAllStoresFiles(): Promise { } console.log(`Created ${results.length} store cache files`) - + // Purge all store caches in one batch with retry try { await retryWithExponentialBackoff(() => clearUrlCache(urls)) @@ -189,7 +196,7 @@ export async function createAllStoresFiles(): Promise { } catch (error) { console.error(`Failed to purge cache for store files after 3 retries. URLs: ${urls.join(', ')}`, error) } - + return results } @@ -204,7 +211,7 @@ export interface CreateAllCacheFilesResult { export async function createAllCacheFiles(): Promise { console.log('Starting creation of all cache files...') - + // Create all global cache files in parallel const [ productsKey, @@ -221,7 +228,7 @@ export async function createAllCacheFiles(): Promise createBannersFileInternal(), createAllStoresFilesInternal(), ]) - + // Collect all URLs for batch cache purge const urls = [ constructCacheUrl(CACHE_FILENAMES.products), @@ -231,7 +238,7 @@ export async function createAllCacheFiles(): Promise constructCacheUrl(CACHE_FILENAMES.banners), ...individualStoreKeys.map((_, index) => constructCacheUrl(`stores/${index + 1}.json`)), ] - + // Purge all caches in one batch with retry try { await retryWithExponentialBackoff(() => clearUrlCache(urls)) @@ -239,9 +246,9 @@ export async function createAllCacheFiles(): Promise } catch (error) { console.error(`Failed to purge cache for all files after 3 retries`, error) } - + console.log('All cache files created successfully') - + return { products: productsKey, essentialConsts: essentialConstsKey, @@ -289,9 +296,17 @@ async function createBannersFileInternal(): Promise { } async function createAllStoresFilesInternal(): Promise { + /* + // 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 getStoresSummary() const results: string[] = [] - + for (const store of stores) { const storeData = await scaffoldStoreWithProducts(store.id) const jsonContent = JSON.stringify(storeData, null, 2) @@ -299,7 +314,7 @@ async function createAllStoresFilesInternal(): Promise { const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/stores/${store.id}.json`) results.push(s3Key) } - + console.log(`Created ${results.length} store cache files`) return results } diff --git a/apps/backend/src/lib/const-store.ts b/apps/backend/src/lib/const-store.ts index c16609e..28c53b2 100644 --- a/apps/backend/src/lib/const-store.ts +++ b/apps/backend/src/lib/const-store.ts @@ -1,5 +1,4 @@ -import { db } from '@/src/db/db_index' -import { keyValStore } from '@/src/db/schema' +import { getAllKeyValStore } from '@/src/dbService' import redisClient from '@/src/lib/redis-client' import { CONST_KEYS, CONST_KEYS_ARRAY, type ConstKey } from '@/src/lib/const-keys' @@ -9,13 +8,21 @@ export const computeConstants = async (): Promise => { try { 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 getAllKeyValStore(); + for (const constant of constants) { const redisKey = `${CONST_REDIS_PREFIX}${constant.key}`; const value = JSON.stringify(constant.value); // console.log({redisKey, value}) - + await redisClient.set(redisKey, value); } @@ -29,11 +36,11 @@ export const computeConstants = async (): Promise => { export const getConstant = async (key: string): Promise => { const redisKey = `${CONST_REDIS_PREFIX}${key}`; const value = await redisClient.get(redisKey); - + if (!value) { return null; } - + try { return JSON.parse(value) as T; } catch { @@ -44,7 +51,7 @@ export const getConstant = async (key: string): Promise => { export const getConstants = async (keys: string[]): Promise> => { const redisKeys = keys.map(key => `${CONST_REDIS_PREFIX}${key}`); const values = await redisClient.MGET(redisKeys); - + const result: Record = {}; keys.forEach((key, index) => { const value = values[index]; @@ -58,17 +65,17 @@ export const getConstants = async (keys: string[]): Promise> => { const result: Record = {}; - + for (const key of CONST_KEYS_ARRAY) { result[key] = await getConstant(key); } - + return result as Record; }; diff --git a/apps/backend/src/lib/delete-orders.ts b/apps/backend/src/lib/delete-orders.ts index 4fb9516..3dee7c4 100644 --- a/apps/backend/src/lib/delete-orders.ts +++ b/apps/backend/src/lib/delete-orders.ts @@ -1,6 +1,4 @@ -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'; +import { deleteOrdersWithRelations } from '@/src/dbService' /** * Delete orders and all their related records @@ -8,6 +6,26 @@ import { eq, inArray } from 'drizzle-orm'; * @returns Promise * @throws Error if deletion fails */ +export const deleteOrders = async (orderIds: number[]): Promise => { + 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 => { if (orderIds.length === 0) { return; @@ -43,3 +61,4 @@ export const deleteOrders = async (orderIds: number[]): Promise => { throw error; } }; +*/ diff --git a/apps/backend/src/lib/notif-job.ts b/apps/backend/src/lib/notif-job.ts index c0c18f9..31f984c 100644 --- a/apps/backend/src/lib/notif-job.ts +++ b/apps/backend/src/lib/notif-job.ts @@ -1,7 +1,7 @@ import { Queue, Worker } from 'bullmq'; import { Expo } from 'expo-server-sdk'; 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 { NOTIFS_QUEUE, diff --git a/apps/backend/src/lib/post-order-handler.ts b/apps/backend/src/lib/post-order-handler.ts index 62953fa..c7e2f6f 100644 --- a/apps/backend/src/lib/post-order-handler.ts +++ b/apps/backend/src/lib/post-order-handler.ts @@ -1,8 +1,9 @@ -import { db } from '@/src/db/db_index' -import { orders, orderStatus } from '@/src/db/schema' +import { + getOrdersByIdsWithFullData, + getOrderByIdWithFullData, +} from '@/src/dbService' import redisClient from '@/src/lib/redis-client' import { sendTelegramMessage } from '@/src/lib/telegram-service' -import { inArray, eq } from 'drizzle-orm'; const ORDER_CHANNEL = 'orders:placed'; const CANCELLED_CHANNEL = 'orders:cancelled'; @@ -98,6 +99,12 @@ export const startOrderHandler = async (): Promise => { const { orderIds }: OrderIdMessage = JSON.parse(message); 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({ where: inArray(orders.id, orderIds), with: { @@ -106,6 +113,9 @@ export const startOrderHandler = async (): Promise => { slot: true, }, }); + */ + + const ordersData = await getOrdersByIdsWithFullData(orderIds); const telegramMessage = formatOrderMessageWithFullData(ordersData); await sendTelegramMessage(telegramMessage); @@ -143,6 +153,12 @@ export const startCancellationHandler = async (): Promise => { const cancellationData: CancellationMessage = JSON.parse(message); 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({ where: eq(orders.id, cancellationData.orderId), with: { @@ -151,6 +167,9 @@ export const startCancellationHandler = async (): Promise => { refunds: true, }, }); + */ + + const orderData = await getOrderByIdWithFullData(cancellationData.orderId); if (!orderData) { console.error('Order not found for cancellation:', cancellationData.orderId); diff --git a/apps/backend/src/middleware/auth.middleware.ts b/apps/backend/src/middleware/auth.middleware.ts index 1e381bb..47f129b 100644 --- a/apps/backend/src/middleware/auth.middleware.ts +++ b/apps/backend/src/middleware/auth.middleware.ts @@ -1,9 +1,7 @@ import { Request, Response, NextFunction } from 'express'; import jwt from 'jsonwebtoken'; -import { db } from '@/src/db/db_index' -import { staffUsers, userDetails } from '@/src/db/schema' -import { eq } from 'drizzle-orm'; -import { ApiError } from '@/src/lib/api-error' +import { getStaffUserById, isUserSuspended } from '@/src/dbService'; +import { ApiError } from '@/src/lib/api-error'; interface AuthenticatedRequest extends Request { user?: { @@ -33,10 +31,19 @@ export const authenticateUser = async (req: AuthenticatedRequest, res: Response, // Check if this is a staff token (has 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({ where: eq(staffUsers.id, decoded.staffId), }); + */ + + // This is a staff token, verify staff exists + const staff = await getStaffUserById(decoded.staffId); if (!staff) { 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 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({ where: eq(userDetails.userId, decoded.userId), }); @@ -58,10 +70,18 @@ export const authenticateUser = async (req: AuthenticatedRequest, res: Response, if (details?.isSuspended) { 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(); } catch (error) { next(error); } -}; \ No newline at end of file +}; diff --git a/apps/backend/src/middleware/staff-auth.ts b/apps/backend/src/middleware/staff-auth.ts index 2215f5d..179bafe 100644 --- a/apps/backend/src/middleware/staff-auth.ts +++ b/apps/backend/src/middleware/staff-auth.ts @@ -1,9 +1,7 @@ import { Request, Response, NextFunction } from 'express'; import jwt from 'jsonwebtoken'; -import { db } from '@/src/db/db_index' -import { staffUsers } from '@/src/db/schema' -import { eq } from 'drizzle-orm'; -import { ApiError } from '@/src/lib/api-error' +import { getStaffUserById } from '@/src/dbService'; +import { ApiError } from '@/src/lib/api-error'; // Extend Request interface to include staffUser declare global { @@ -54,10 +52,19 @@ export const authenticateStaff = async (req: Request, res: Response, next: NextF 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({ where: eq(staffUsers.id, decoded.staffId), }); + */ + + // Fetch staff user from database + const staff = await getStaffUserById(decoded.staffId); if (!staff) { throw new ApiError('Staff user not found', 401); @@ -73,4 +80,4 @@ export const authenticateStaff = async (req: Request, res: Response, next: NextF } catch (error) { next(error); } -}; \ No newline at end of file +}; diff --git a/apps/backend/src/postgresImporter.ts b/apps/backend/src/postgresImporter.ts index 5dee723..d4e5627 100644 --- a/apps/backend/src/postgresImporter.ts +++ b/apps/backend/src/postgresImporter.ts @@ -84,6 +84,7 @@ export { updateSlotDeliverySequence, // Admin - Staff User getStaffUserByName, + getStaffUserById, getAllStaff, getAllUsers, getUserWithDetails, @@ -164,6 +165,8 @@ export { getUserProductReviews, getUserProductByIdBasic, createUserProductReview, + getAllProductsWithUnits, + type ProductSummaryData, // User - Slots getUserActiveSlotsList, getUserProductAvailability, @@ -180,10 +183,15 @@ export { getUserAuthById, getUserAuthCreds, getUserAuthDetails, + isUserSuspended, createUserAuthWithCreds, createUserAuthWithMobile, upsertUserAuthPassword, deleteUserAuthAccount, + // UV API helpers + createUserWithProfile, + getUserDetailsByUserId, + updateUserProfile, // User - Coupon getUserActiveCouponsWithRelations, getUserAllCouponsWithRelations, @@ -218,4 +226,53 @@ export { getUserRecentlyDeliveredOrderIds, getUserProductIdsFromOrders, 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' diff --git a/apps/backend/src/services/user/order-service.ts b/apps/backend/src/services/user/order-service.ts deleted file mode 100644 index 8b2df28..0000000 --- a/apps/backend/src/services/user/order-service.ts +++ /dev/null @@ -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(fn: (tx: any) => Promise): Promise { - 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[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, - paymentRecordInserter?: (paymentInfoId: number, razorpayOrder: any, tx: Tx) => Promise -): Promise { - 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[] = ordersData.map( - (od) => ({ - ...od.order, - paymentInfoId: sharedPaymentInfoId, - }) - ) - - const insertedOrders = await tx.insert(orders).values(ordersToInsert).returning() - - const allOrderItems: Omit[] = [] - const allOrderStatuses: Omit[] = [] - - insertedOrders.forEach((order: 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 - }) -} diff --git a/apps/backend/src/services/user/product-service.ts b/apps/backend/src/services/user/product-service.ts deleted file mode 100644 index 0f214d9..0000000 --- a/apps/backend/src/services/user/product-service.ts +++ /dev/null @@ -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() -} diff --git a/apps/backend/src/stores/banner-store.ts b/apps/backend/src/stores/banner-store.ts index 7fbd6f1..8861f3a 100644 --- a/apps/backend/src/stores/banner-store.ts +++ b/apps/backend/src/stores/banner-store.ts @@ -1,38 +1,44 @@ -// import redisClient from '@/src/stores/redis-client'; -import redisClient from '@/src/lib/redis-client'; -import { db } from '@/src/db/db_index' -import { homeBanners } from '@/src/db/schema' -import { isNotNull, asc } from 'drizzle-orm'; -import { scaffoldAssetUrl } from '@/src/lib/s3-client'; - - - - +import redisClient from '@/src/lib/redis-client' +import { + getAllBannersForCache, + type BannerData, +} from '@/src/dbService' +import { scaffoldAssetUrl } from '@/src/lib/s3-client' // Banner Type (matches getBanners return) interface Banner { - id: number; - name: string; - imageUrl: string | null; - serialNum: number | null; - productIds: number[] | null; - createdAt: Date; - // updatedAt: Date; + id: number + name: string + imageUrl: string | null + serialNum: number | null + productIds: number[] | null + createdAt: Date } export async function initializeBannerStore(): Promise { 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({ - where: isNotNull(homeBanners.serialNum), // Only show assigned banners - orderBy: asc(homeBanners.serialNum), // Order by slot number 1-4 + where: isNotNull(homeBanners.serialNum), + orderBy: asc(homeBanners.serialNum), }); + */ // Store each banner in Redis 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 = { id: banner.id, name: banner.name, @@ -40,53 +46,52 @@ export async function initializeBannerStore(): Promise { serialNum: banner.serialNum, productIds: banner.productIds, createdAt: banner.createdAt, - // updatedAt: banner.updatedAt, - }; + } - await redisClient.set(`banner:${banner.id}`, JSON.stringify(bannerObj)); + await redisClient.set(`banner:${banner.id}`, JSON.stringify(bannerObj)) } - console.log('Banner store initialized successfully'); + console.log('Banner store initialized successfully') } catch (error) { - console.error('Error initializing banner store:', error); + console.error('Error initializing banner store:', error) } } export async function getBannerById(id: number): Promise { try { - const key = `banner:${id}`; - const data = await redisClient.get(key); - if (!data) return null; - return JSON.parse(data) as Banner; + const key = `banner:${id}` + const data = await redisClient.get(key) + if (!data) return null + return JSON.parse(data) as Banner } catch (error) { - console.error(`Error getting banner ${id}:`, error); - return null; + console.error(`Error getting banner ${id}:`, error) + return null } } export async function getAllBanners(): Promise { try { // 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 - const bannersData = await redisClient.MGET(keys); + const bannersData = await redisClient.MGET(keys) - const banners: Banner[] = []; + const banners: Banner[] = [] for (const bannerData of bannersData) { 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 - 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) { - console.error('Error getting all banners:', error); - return []; + console.error('Error getting all banners:', error) + return [] } } diff --git a/apps/backend/src/stores/product-store.ts b/apps/backend/src/stores/product-store.ts index d4cf3a2..119d3d8 100644 --- a/apps/backend/src/stores/product-store.ts +++ b/apps/backend/src/stores/product-store.ts @@ -1,36 +1,51 @@ -// import redisClient from '@/src/stores/redis-client'; -import redisClient from '@/src/lib/redis-client'; -import { db } from '@/src/db/db_index' -import { productInfo, units, productSlots, deliverySlotInfo, specialDeals, storeInfo, productTags, productTagInfo } from '@/src/db/schema' -import { eq, and, gt, sql } from 'drizzle-orm'; -import { generateSignedUrlsFromS3Urls, scaffoldAssetUrl } from '@/src/lib/s3-client'; +import redisClient from '@/src/lib/redis-client' +import { + getAllProductsForCache, + getAllStoresForCache, + getAllDeliverySlotsForCache, + 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) interface Product { - id: number; - name: string; - shortDescription: string | null; - longDescription: string | null; - price: string; - marketPrice: string | null; - unitNotation: string; - images: string[]; - isOutOfStock: boolean; - store: { id: number; name: string; description: string | null } | null; - incrementStep: number; - productQuantity: number; - isFlashAvailable: boolean; - flashPrice: string | null; - deliverySlots: Array<{ id: number; deliveryTime: Date; freezeTime: Date; isCapacityFull: boolean }>; - specialDeals: Array<{ quantity: string; price: string; validTill: Date }>; - productTags: string[]; + id: number + name: string + shortDescription: string | null + longDescription: string | null + price: string + marketPrice: string | null + unitNotation: string + images: string[] + isOutOfStock: boolean + store: { id: number; name: string; description: string | null } | null + incrementStep: number + productQuantity: number + isFlashAvailable: boolean + flashPrice: string | null + deliverySlots: Array<{ id: number; deliveryTime: Date; freezeTime: Date; isCapacityFull: boolean }> + specialDeals: Array<{ quantity: string; price: string; validTill: Date }> + productTags: string[] } export async function initializeProducts(): Promise { 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) + 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 .select({ id: productInfo.id, @@ -50,74 +65,50 @@ export async function initializeProducts(): Promise { }) .from(productInfo) .innerJoin(units, eq(productInfo.unitId, units.id)); + */ // Fetch all stores - const allStores = await db.query.storeInfo.findMany({ - columns: { id: true, name: true, description: true }, - }); - const storeMap = new Map(allStores.map(s => [s.id, s])); + const allStores = await getAllStoresForCache() + const storeMap = new Map(allStores.map((s) => [s.id, s])) // Fetch all delivery slots (excluding full capacity slots) - const allDeliverySlots = await 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()`) - ) - ); - const deliverySlotsMap = new Map(); + const allDeliverySlots = await getAllDeliverySlotsForCache() + const deliverySlotsMap = new Map() for (const slot of allDeliverySlots) { - if (!deliverySlotsMap.has(slot.productId)) deliverySlotsMap.set(slot.productId, []); - deliverySlotsMap.get(slot.productId)!.push(slot); + if (!deliverySlotsMap.has(slot.productId)) + deliverySlotsMap.set(slot.productId, []) + deliverySlotsMap.get(slot.productId)!.push(slot) } // Fetch all special deals - const allSpecialDeals = await db - .select({ - productId: specialDeals.productId, - quantity: specialDeals.quantity, - price: specialDeals.price, - validTill: specialDeals.validTill, - }) - .from(specialDeals) - .where(gt(specialDeals.validTill, sql`NOW()`)); - const specialDealsMap = new Map(); + const allSpecialDeals = await getAllSpecialDealsForCache() + const specialDealsMap = new Map() for (const deal of allSpecialDeals) { - if (!specialDealsMap.has(deal.productId)) specialDealsMap.set(deal.productId, []); - specialDealsMap.get(deal.productId)!.push(deal); + if (!specialDealsMap.has(deal.productId)) + specialDealsMap.set(deal.productId, []) + specialDealsMap.get(deal.productId)!.push(deal) } // Fetch all product tags - const allProductTags = await db - .select({ - productId: productTags.productId, - tagName: productTagInfo.tagName, - }) - .from(productTags) - .innerJoin(productTagInfo, eq(productTags.tagId, productTagInfo.id)); - const productTagsMap = new Map(); + const allProductTags = await getAllProductTagsForCache() + const productTagsMap = new Map() for (const tag of allProductTags) { - if (!productTagsMap.has(tag.productId)) productTagsMap.set(tag.productId, []); - productTagsMap.get(tag.productId)!.push(tag.tagName); + if (!productTagsMap.has(tag.productId)) + productTagsMap.set(tag.productId, []) + productTagsMap.get(tag.productId)!.push(tag.tagName) } // Store each product in Redis for (const product of productsData) { - const signedImages = scaffoldAssetUrl((product.images as string[]) || []); - const store = product.storeId ? 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 signedImages = scaffoldAssetUrl( + (product.images as string[]) || [] + ) + const store = product.storeId + ? 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 = { id: product.id, @@ -129,60 +120,70 @@ export async function initializeProducts(): Promise { unitNotation: product.unitShortNotation, images: signedImages, 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, productQuantity: product.productQuantity, isFlashAvailable: product.isFlashAvailable, flashPrice: product.flashPrice?.toString() || null, - deliverySlots: deliverySlots.map(s => ({ 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 })), + deliverySlots: deliverySlots.map((s) => ({ + 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, - }; - + } - await redisClient.set(`product:${product.id}`, JSON.stringify(productObj)); + await redisClient.set(`product:${product.id}`, JSON.stringify(productObj)) } - console.log('Product store initialized successfully'); + console.log('Product store initialized successfully') } catch (error) { - console.error('Error initializing product store:', error); + console.error('Error initializing product store:', error) } } export async function getProductById(id: number): Promise { try { - const key = `product:${id}`; - const data = await redisClient.get(key); - if (!data) return null; - return JSON.parse(data) as Product; + const key = `product:${id}` + const data = await redisClient.get(key) + if (!data) return null + return JSON.parse(data) as Product } catch (error) { - console.error(`Error getting product ${id}:`, error); - return null; + console.error(`Error getting product ${id}:`, error) + return null } } export async function getAllProducts(): Promise { try { // Get all keys matching the pattern "product:*" - const keys = await redisClient.KEYS('product:*'); + const keys = await redisClient.KEYS('product:*') if (keys.length === 0) { - return []; + return [] } // 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) { if (productData) { - products.push(JSON.parse(productData) as Product); + products.push(JSON.parse(productData) as Product) } } - return products; + return products } catch (error) { - console.error('Error getting all products:', error); - return []; + console.error('Error getting all products:', error) + return [] } } diff --git a/apps/backend/src/stores/product-tag-store.ts b/apps/backend/src/stores/product-tag-store.ts index f6a3175..53ca558 100644 --- a/apps/backend/src/stores/product-tag-store.ts +++ b/apps/backend/src/stores/product-tag-store.ts @@ -1,26 +1,35 @@ -// import redisClient from '@/src/stores/redis-client'; -import redisClient from '@/src/lib/redis-client'; -import { db } from '@/src/db/db_index' -import { productTagInfo, productTags } from '@/src/db/schema' -import { eq, inArray } from 'drizzle-orm'; -import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client'; +import redisClient from '@/src/lib/redis-client' +import { + getAllTagsForCache, + getAllTagProductMappings, + type TagBasicData, + type TagProductMapping, +} from '@/src/dbService' +import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client' // Tag Type (matches getDashboardTags return) interface Tag { - id: number; - tagName: string; - tagDescription: string | null; - imageUrl: string | null; - isDashboardTag: boolean; - relatedStores: number[]; - productIds: number[]; + id: number + tagName: string + tagDescription: string | null + imageUrl: string | null + isDashboardTag: boolean + relatedStores: number[] + productIds: number[] } export async function initializeProductTagStore(): Promise { try { - console.log('Initializing product tag store in Redis...'); + console.log('Initializing product tag store in Redis...') // 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 .select({ id: productTagInfo.id, @@ -31,8 +40,16 @@ export async function initializeProductTagStore(): Promise { relatedStores: productTagInfo.relatedStores, }) .from(productTagInfo); + */ // 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 productTagsData = await db .select({ @@ -41,20 +58,23 @@ export async function initializeProductTagStore(): Promise { }) .from(productTags) .where(inArray(productTags.tagId, tagIds)); - + */ + // Group product IDs by tag - const productIdsByTag = new Map(); + const productIdsByTag = new Map() for (const pt of productTagsData) { 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 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 = { id: tag.id, tagName: tag.tagName, @@ -63,109 +83,109 @@ export async function initializeProductTagStore(): Promise { isDashboardTag: tag.isDashboardTag, relatedStores: (tag.relatedStores as number[]) || [], productIds: productIdsByTag.get(tag.id) || [], - }; + } - await redisClient.set(`tag:${tag.id}`, JSON.stringify(tagObj)); + await redisClient.set(`tag:${tag.id}`, JSON.stringify(tagObj)) } - console.log('Product tag store initialized successfully'); + console.log('Product tag store initialized successfully') } 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 { try { - const key = `tag:${id}`; - const data = await redisClient.get(key); - if (!data) return null; - return JSON.parse(data) as Tag; + const key = `tag:${id}` + const data = await redisClient.get(key) + if (!data) return null + return JSON.parse(data) as Tag } catch (error) { - console.error(`Error getting tag ${id}:`, error); - return null; + console.error(`Error getting tag ${id}:`, error) + return null } } export async function getAllTags(): Promise { try { // Get all keys matching the pattern "tag:*" - const keys = await redisClient.KEYS('tag:*'); + const keys = await redisClient.KEYS('tag:*') if (keys.length === 0) { - return []; + return [] } // 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) { if (tagData) { - tags.push(JSON.parse(tagData) as Tag); + tags.push(JSON.parse(tagData) as Tag) } } - return tags; + return tags } catch (error) { - console.error('Error getting all tags:', error); - return []; + console.error('Error getting all tags:', error) + return [] } } export async function getDashboardTags(): Promise { try { // Get all keys matching the pattern "tag:*" - const keys = await redisClient.KEYS('tag:*'); + const keys = await redisClient.KEYS('tag:*') if (keys.length === 0) { - return []; + return [] } // 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) { if (tagData) { - const tag = JSON.parse(tagData) as Tag; + const tag = JSON.parse(tagData) as Tag if (tag.isDashboardTag) { - dashboardTags.push(tag); + dashboardTags.push(tag) } } } - return dashboardTags; + return dashboardTags } catch (error) { - console.error('Error getting dashboard tags:', error); - return []; + console.error('Error getting dashboard tags:', error) + return [] } } export async function getTagsByStoreId(storeId: number): Promise { try { // Get all keys matching the pattern "tag:*" - const keys = await redisClient.KEYS('tag:*'); + const keys = await redisClient.KEYS('tag:*') if (keys.length === 0) { - return []; + return [] } // 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) { if (tagData) { - const tag = JSON.parse(tagData) as Tag; + const tag = JSON.parse(tagData) as Tag if (tag.relatedStores.includes(storeId)) { - storeTags.push(tag); + storeTags.push(tag) } } } - return storeTags; + return storeTags } catch (error) { - console.error(`Error getting tags for store ${storeId}:`, error); - return []; + console.error(`Error getting tags for store ${storeId}:`, error) + return [] } } diff --git a/apps/backend/src/stores/slot-store.ts b/apps/backend/src/stores/slot-store.ts index 3a3ed68..54a453c 100644 --- a/apps/backend/src/stores/slot-store.ts +++ b/apps/backend/src/stores/slot-store.ts @@ -1,50 +1,58 @@ -import redisClient from '@/src/lib/redis-client'; -import { db } from '@/src/db/db_index' -import { deliverySlotInfo, productSlots, productInfo, units } from '@/src/db/schema' -import { eq, and, gt, asc } from 'drizzle-orm'; -import { generateSignedUrlsFromS3Urls, scaffoldAssetUrl } from '@/src/lib/s3-client'; -import dayjs from 'dayjs'; +import redisClient from '@/src/lib/redis-client' +import { + getAllSlotsWithProductsForCache, + type SlotWithProductsData, +} from '@/src/dbService' +import { scaffoldAssetUrl } from '@/src/lib/s3-client' +import dayjs from 'dayjs' // Define the structure for slot with products interface SlotWithProducts { - id: number; - deliveryTime: Date; - freezeTime: Date; - isActive: boolean; - isCapacityFull: boolean; + id: number + deliveryTime: Date + freezeTime: Date + isActive: boolean + isCapacityFull: boolean products: Array<{ - id: number; - name: string; - shortDescription: string | null; - productQuantity: number; - price: string; - marketPrice: string | null; - unit: string | null; - images: string[]; - isOutOfStock: boolean; - storeId: number | null; - nextDeliveryDate: Date; - }>; + id: number + name: string + shortDescription: string | null + productQuantity: number + price: string + marketPrice: string | null + unit: string | null + images: string[] + isOutOfStock: boolean + storeId: number | null + nextDeliveryDate: Date + }> } interface SlotInfo { - id: number; - deliveryTime: Date; - freezeTime: Date; - isCapacityFull: boolean; + id: number + deliveryTime: Date + freezeTime: Date + isCapacityFull: boolean } export async function initializeSlotStore(): Promise { try { - console.log('Initializing slot store in Redis...'); - - const now = new Date(); + console.log('Initializing slot store in Redis...') // 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({ where: and( eq(deliverySlotInfo.isActive, true), - gt(deliverySlotInfo.deliveryTime, now), // Only future slots + gt(deliverySlotInfo.deliveryTime, now), ), with: { productSlots: { @@ -60,6 +68,7 @@ export async function initializeSlotStore(): Promise { }, orderBy: asc(deliverySlotInfo.deliveryTime), }); + */ // Transform data for storage const slotsWithProducts = await Promise.all( @@ -79,151 +88,156 @@ export async function initializeSlotStore(): Promise { marketPrice: productSlot.product.marketPrice?.toString() || null, unit: productSlot.product.unit?.shortNotation || null, images: scaffoldAssetUrl( - (productSlot.product.images as string[]) || [], + (productSlot.product.images as string[]) || [] ), isOutOfStock: productSlot.product.isOutOfStock, storeId: productSlot.product.storeId, nextDeliveryDate: slot.deliveryTime, - })), + })) ), - })), - ); + })) + ) // Store each slot in Redis with key pattern "slot:{id}" 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 // Group slots by productId - const productSlotsMap: Record = {}; + const productSlotsMap: Record = {} for (const slot of slotsWithProducts) { for (const product of slot.products) { if (!productSlotsMap[product.id]) { - productSlotsMap[product.id] = []; + productSlotsMap[product.id] = [] } productSlotsMap[product.id].push({ id: slot.id, deliveryTime: slot.deliveryTime, freezeTime: slot.freezeTime, isCapacityFull: slot.isCapacityFull, - }); + }) } } // Store each product's slots in Redis with key pattern "product:{id}:slots" 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) { - console.error('Error initializing slot store:', error); + console.error('Error initializing slot store:', error) } } export async function getSlotById(slotId: number): Promise { try { - const key = `slot:${slotId}`; - const data = await redisClient.get(key); - if (!data) return null; - return JSON.parse(data) as SlotWithProducts; + const key = `slot:${slotId}` + const data = await redisClient.get(key) + if (!data) return null + return JSON.parse(data) as SlotWithProducts } catch (error) { - console.error(`Error getting slot ${slotId}:`, error); - return null; + console.error(`Error getting slot ${slotId}:`, error) + return null } } export async function getAllSlots(): Promise { try { // 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 - const slotsData = await redisClient.MGET(keys); + const slotsData = await redisClient.MGET(keys) - const slots: SlotWithProducts[] = []; + const slots: SlotWithProducts[] = [] for (const slotData of slotsData) { if (slotData) { - slots.push(JSON.parse(slotData) as SlotWithProducts); + slots.push(JSON.parse(slotData) as SlotWithProducts) } } - return slots; + return slots } catch (error) { - console.error('Error getting all slots:', error); - return []; + console.error('Error getting all slots:', error) + return [] } } export async function getProductSlots(productId: number): Promise { try { - const key = `product:${productId}:slots`; - const data = await redisClient.get(key); - if (!data) return []; - return JSON.parse(data) as SlotInfo[]; + const key = `product:${productId}:slots` + const data = await redisClient.get(key) + if (!data) return [] + return JSON.parse(data) as SlotInfo[] } catch (error) { - console.error(`Error getting slots for product ${productId}:`, error); - return []; + console.error(`Error getting slots for product ${productId}:`, error) + return [] } } export async function getAllProductsSlots(): Promise> { try { // 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 - const productsData = await redisClient.MGET(keys); + const productsData = await redisClient.MGET(keys) - const result: Record = {}; + const result: Record = {} for (const key of keys) { // Extract productId from key "product:{id}:slots" - const match = key.match(/product:(\d+):slots/); + const match = key.match(/product:(\d+):slots/) if (match) { - const productId = parseInt(match[1], 10); - const dataIndex = keys.indexOf(key); + const productId = parseInt(match[1], 10) + const dataIndex = keys.indexOf(key) 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) { - console.error('Error getting all products slots:', error); - return {}; + console.error('Error getting all products slots:', error) + return {} } } -export async function getMultipleProductsSlots(productIds: number[]): Promise> { +export async function getMultipleProductsSlots( + productIds: number[] +): Promise> { try { - if (productIds.length === 0) return {}; + if (productIds.length === 0) return {} // 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 - const productsData = await redisClient.MGET(keys); + const productsData = await redisClient.MGET(keys) - const result: Record = {}; + const result: Record = {} for (let i = 0; i < productIds.length; i++) { - const data = productsData[i]; + const data = productsData[i] if (data) { - const slots = JSON.parse(data) as SlotInfo[]; + const slots = JSON.parse(data) as SlotInfo[] // 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) { - console.error('Error getting products slots:', error); - return {}; + console.error('Error getting products slots:', error) + return {} } } diff --git a/apps/backend/src/stores/user-negativity-store.ts b/apps/backend/src/stores/user-negativity-store.ts index d518435..f24a3b9 100644 --- a/apps/backend/src/stores/user-negativity-store.ts +++ b/apps/backend/src/stores/user-negativity-store.ts @@ -1,11 +1,21 @@ -import redisClient from '@/src/lib/redis-client'; -import { db } from '@/src/db/db_index' -import { userIncidents } from '@/src/db/schema' -import { eq, sum } from 'drizzle-orm'; +import redisClient from '@/src/lib/redis-client' +import { + getAllUserNegativityScores as getAllUserNegativityScoresFromDb, + getUserNegativityScore as getUserNegativityScoreFromDb, + type UserNegativityData, +} from '@/src/dbService' export async function initializeUserNegativityStore(): Promise { 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 .select({ @@ -14,91 +24,102 @@ export async function initializeUserNegativityStore(): Promise { }) .from(userIncidents) .groupBy(userIncidents.userId); + */ for (const { userId, totalNegativityScore } of results) { await redisClient.set( `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) { - console.error('Error initializing user negativity store:', error); - throw error; + console.error('Error initializing user negativity store:', error) + throw error } } export async function getUserNegativity(userId: number): Promise { try { - const key = `user:negativity:${userId}`; - const data = await redisClient.get(key); + const key = `user:negativity:${userId}` + const data = await redisClient.get(key) if (!data) { - return 0; + return 0 } - return parseInt(data, 10); + return parseInt(data, 10) } catch (error) { - console.error(`Error getting negativity score for user ${userId}:`, error); - return 0; + console.error(`Error getting negativity score for user ${userId}:`, error) + return 0 } } export async function getAllUserNegativityScores(): Promise> { 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 = {}; + const result: Record = {} for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - const value = values[i]; + const key = keys[i] + const value = values[i] - const match = key.match(/user:negativity:(\d+)/); + const match = key.match(/user:negativity:(\d+)/) if (match && value) { - const userId = parseInt(match[1], 10); - result[userId] = parseInt(value, 10); + const userId = parseInt(match[1], 10) + result[userId] = parseInt(value, 10) } } - return result; + return result } catch (error) { - console.error('Error getting all user negativity scores:', error); - return {}; + console.error('Error getting all user negativity scores:', error) + return {} } } -export async function getMultipleUserNegativityScores(userIds: number[]): Promise> { +export async function getMultipleUserNegativityScores( + userIds: number[] +): Promise> { 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 = {}; + const result: Record = {} for (let i = 0; i < userIds.length; i++) { - const value = values[i]; + const value = values[i] if (value) { - result[userIds[i]] = parseInt(value, 10); + result[userIds[i]] = parseInt(value, 10) } else { - result[userIds[i]] = 0; + result[userIds[i]] = 0 } } - return result; + return result } catch (error) { - console.error('Error getting multiple user negativity scores:', error); - return {}; + console.error('Error getting multiple user negativity scores:', error) + return {} } } export async function recomputeUserNegativityScore(userId: number): Promise { 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 .select({ totalNegativityScore: sum(userIncidents.negativityScore).mapWith(Number), @@ -108,11 +129,12 @@ export async function recomputeUserNegativityScore(userId: number): Promise { + .query(async (): Promise => { + /* + // 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({ columns: { id: true, @@ -46,11 +55,15 @@ export const commonApiRouter = router({ description: true, }, }); + */ + + const stores = await getStoresSummary(); return { stores, }; }), + checkLocationInPolygon: publicProcedure .input(z.object({ lat: z.number().min(-90).max(90), @@ -110,16 +123,23 @@ export const commonApiRouter = router({ } return { uploadUrls }; }), - healthCheck: publicProcedure - .query(async () => { - // Test DB connection by selecting product names - // await db.select({ name: productInfo.name }).from(productInfo).limit(1); - await db.select({ key: keyValStore.key }).from(keyValStore).limit(1); - return { - status: "ok", - }; + healthCheck: publicProcedure + .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 + // await db.select({ name: productInfo.name }).from(productInfo).limit(1); + await db.select({ key: keyValStore.key }).from(keyValStore).limit(1); + */ + + const result = await healthCheck(); + return result; }), + essentialConsts: publicProcedure .query(async () => { const response = await scaffoldEssentialConsts(); diff --git a/apps/backend/src/trpc/apis/common-apis/common.ts b/apps/backend/src/trpc/apis/common-apis/common.ts index 2896035..64cac9b 100644 --- a/apps/backend/src/trpc/apis/common-apis/common.ts +++ b/apps/backend/src/trpc/apis/common-apis/common.ts @@ -1,43 +1,35 @@ import { router, publicProcedure } from '@/src/trpc/trpc-index' -import { db } from '@/src/db/db_index' -import { productInfo, units, productSlots, deliverySlotInfo, storeInfo } from '@/src/db/schema' -import { eq, gt, and, sql, inArray } from 'drizzle-orm'; +import { + getSuspendedProductIds, + getNextDeliveryDateWithCapacity, + getStoresSummary, +} from '@/src/dbService' import { generateSignedUrlsFromS3Urls, generateSignedUrlFromS3Url } from '@/src/lib/s3-client' import { getAllProducts as getAllProductsFromCache } from '@/src/stores/product-store' import { getDashboardTags as getDashboardTagsFromCache } from '@/src/stores/product-tag-store' -export const getNextDeliveryDate = async (productId: number): Promise => { - 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; -}; +// Re-export with original name for backwards compatibility +export const getNextDeliveryDate = getNextDeliveryDateWithCapacity export async function scaffoldProducts() { // Get all products from cache let products = await getAllProductsFromCache(); 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 const suspendedProducts = await db .select({ id: productInfo.id }) .from(productInfo) .where(eq(productInfo.isSuspended, true)); + */ - const suspendedProductIds = new Set(suspendedProducts.map(sp => sp.id)); + const suspendedProductIds = new Set(await getSuspendedProductIds()); // Filter out suspended products products = products.filter(product => !suspendedProductIds.has(product.id)); @@ -45,7 +37,7 @@ export async function scaffoldProducts() { // Format products to match the expected response structure const formattedProducts = await Promise.all( products.map(async (product) => { - const nextDeliveryDate = await getNextDeliveryDate(product.id); + const nextDeliveryDate = await getNextDeliveryDateWithCapacity(product.id); return { id: product.id, name: product.name, @@ -89,28 +81,18 @@ export const commonRouter = router({ return response; }), + /* + // Old implementation - moved to common-trpc-index.ts: getStoresSummary: publicProcedure .query(async () => { - const stores = await db.query.storeInfo.findMany({ - columns: { - id: true, - name: true, - description: true, - }, - }); - - return { - stores, - }; + const stores = await getStoresSummary(); + return { stores }; }), healthCheck: publicProcedure .query(async () => { - // Test DB connection by selecting product names - await db.select({ name: productInfo.name }).from(productInfo).limit(1); - - return { - status: "ok", - }; + const result = await healthCheck(); + return result; }), + */ }); diff --git a/apps/backend/src/uv-apis/auth.controller.ts b/apps/backend/src/uv-apis/auth.controller.ts index 8cdc9c0..02c103b 100644 --- a/apps/backend/src/uv-apis/auth.controller.ts +++ b/apps/backend/src/uv-apis/auth.controller.ts @@ -1,9 +1,14 @@ import { Request, Response, NextFunction } from 'express'; import bcrypt from 'bcryptjs'; import jwt from 'jsonwebtoken'; -import { eq } from 'drizzle-orm'; -import { db } from '@/src/db/db_index' -import { users, userCreds, userDetails } from '@/src/db/schema' +import { + getUserAuthByEmail, + getUserAuthByMobile, + createUserWithProfile, + getUserAuthById, + getUserDetailsByUserId, + updateUserProfile, +} from '@/src/dbService'; import { ApiError } from '@/src/lib/api-error' import catchAsync from '@/src/lib/catch-async' 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); } - // 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 .select() .from(users) .where(eq(users.email, email.toLowerCase())) .limit(1); + */ + // Check if email already exists + const existingEmail = await getUserAuthByEmail(email.toLowerCase()); if (existingEmail) { throw new ApiError('Email already registered', 409); } - // Check if mobile already exists + /* + // Old implementation - direct DB queries: const [existingMobile] = await db .select() .from(users) .where(eq(users.mobile, cleanMobile)) .limit(1); + */ + // Check if mobile already exists + const existingMobile = await getUserAuthByMobile(cleanMobile); if (existingMobile) { 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 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) => { - // Create user const [user] = await tx .insert(users) .values({ @@ -116,24 +135,28 @@ export const register = catchAsync(async (req: Request, res: Response, next: Nex }) .returning(); - // Create user credentials - await tx - .insert(userCreds) - .values({ - userId: user.id, - userPassword: hashedPassword, - }); + await tx.insert(userCreds).values({ + userId: user.id, + userPassword: hashedPassword, + }); - // Create user details with profile image - await tx - .insert(userDetails) - .values({ - userId: user.id, - profileImage: profileImageUrl, - }); + await tx.insert(userDetails).values({ + userId: user.id, + profileImage: profileImageUrl, + }); 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); @@ -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) { const [existingEmail] = await db .select() @@ -207,8 +235,18 @@ export const updateProfile = catchAsync(async (req: Request, res: Response, next 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) { const cleanMobile = mobile.replace(/\D/g, ''); 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); } } + */ - // 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) => { // Update user table 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 (Object.keys(updateData).length > 0) { - await tx - .update(users) - .set(updateData) - .where(eq(users.id, userId)); + await tx.update(users).set(updateData).where(eq(users.id, userId)); } // Update password if provided if (password) { const hashedPassword = await bcrypt.hash(password, 12); - await tx - .update(userCreds) - .set({ userPassword: hashedPassword }) - .where(eq(userCreds.userId, userId)); + await tx.update(userCreds).set({ userPassword: hashedPassword }).where(eq(userCreds.userId, userId)); } // Update or insert user details @@ -255,44 +304,41 @@ export const updateProfile = catchAsync(async (req: Request, res: Response, next if (profileImageUrl) userDetailsUpdate.profileImage = profileImageUrl; 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) { - // 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 { - // Create new record userDetailsUpdate.userId = userId; userDetailsUpdate.createdAt = new Date(); - await tx - .insert(userDetails) - .values(userDetailsUpdate); + await tx.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; }); + */ + + // 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 - const [userDetail] = await db - .select() - .from(userDetails) - .where(eq(userDetails.userId, userId)) - .limit(1); + const userDetail = await getUserDetailsByUserId(userId); // Generate signed URL for profile image if it exists const profileImageSignedUrl = userDetail?.profileImage @@ -319,3 +365,10 @@ export const updateProfile = catchAsync(async (req: Request, res: Response, next 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'; +*/ diff --git a/apps/backend/src/uv-apis/user-rest.controller.ts b/apps/backend/src/uv-apis/user-rest.controller.ts index 8fd02a1..4cb8c16 100644 --- a/apps/backend/src/uv-apis/user-rest.controller.ts +++ b/apps/backend/src/uv-apis/user-rest.controller.ts @@ -1,6 +1,5 @@ import { Request, Response, NextFunction } from 'express'; -import { db } from '@/src/db/db_index' -import { complaints } from '@/src/db/schema' +import { createUserComplaint } from '@/src/dbService'; import { ApiError } from '@/src/lib/api-error' import catchAsync from '@/src/lib/catch-async' 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); } + /* + // Old implementation - direct DB queries: + import { db } from '@/src/db/db_index' + import { complaints } from '@/src/db/schema' + await db.insert(complaints).values({ userId, orderId: orderIdNum, complaintBody: complaintBody.trim(), images: uploadedImageUrls.length > 0 ? uploadedImageUrls : null, }); + */ + + await createUserComplaint( + userId, + orderIdNum, + complaintBody.trim(), + uploadedImageUrls.length > 0 ? uploadedImageUrls : null + ); res.status(200).json({ success: true, message: 'Complaint raised successfully' }); -}); \ No newline at end of file +}); + +/* +// Old implementation - direct DB queries: +import { db } from '@/src/db/db_index' +import { complaints } from '@/src/db/schema' +*/ diff --git a/packages/db_helper_postgres/index.ts b/packages/db_helper_postgres/index.ts index ddf7441..387c9ea 100644 --- a/packages/db_helper_postgres/index.ts +++ b/packages/db_helper_postgres/index.ts @@ -7,6 +7,9 @@ export { db } from './src/db/db_index'; // Re-export 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 export { // Banner @@ -105,6 +108,7 @@ export { export { // Staff User getStaffUserByName, + getStaffUserById, getAllStaff, getAllUsers, getUserWithDetails, @@ -212,6 +216,8 @@ export { getProductReviews as getUserProductReviews, getProductById as getUserProductByIdBasic, createProductReview as createUserProductReview, + getAllProductsWithUnits, + type ProductSummaryData, } from './src/user-apis/product'; export { @@ -237,10 +243,15 @@ export { getUserById as getUserAuthById, getUserCreds as getUserAuthCreds, getUserDetails as getUserAuthDetails, + isUserSuspended, createUserWithCreds as createUserAuthWithCreds, createUserWithMobile as createUserAuthWithMobile, upsertUserPassword as upsertUserAuthPassword, deleteUserAccount as deleteUserAuthAccount, + // UV API helpers + createUserWithProfile, + getUserDetailsByUserId, + updateUserProfile, } from './src/user-apis/auth'; export { @@ -284,4 +295,80 @@ export { getRecentlyDeliveredOrderIds as getUserRecentlyDeliveredOrderIds, getProductIdsFromOrders as getUserProductIdsFromOrders, getProductsForRecentOrders as getUserProductsForRecentOrders, + // Post-order handler helpers + getOrdersByIdsWithFullData, + getOrderByIdWithFullData, + type OrderWithFullData, + type OrderWithCancellationData, } 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'; diff --git a/packages/db_helper_postgres/src/admin-apis/staff-user.ts b/packages/db_helper_postgres/src/admin-apis/staff-user.ts index dabef01..b0ecfcf 100644 --- a/packages/db_helper_postgres/src/admin-apis/staff-user.ts +++ b/packages/db_helper_postgres/src/admin-apis/staff-user.ts @@ -18,6 +18,14 @@ export async function getStaffUserByName(name: string): Promise { + const staff = await db.query.staffUsers.findFirst({ + where: eq(staffUsers.id, staffId), + }); + + return staff || null; +} + export async function getAllStaff(): Promise { const staff = await db.query.staffUsers.findMany({ columns: { diff --git a/packages/db_helper_postgres/src/lib/automated-jobs.ts b/packages/db_helper_postgres/src/lib/automated-jobs.ts new file mode 100644 index 0000000..75f5723 --- /dev/null +++ b/packages/db_helper_postgres/src/lib/automated-jobs.ts @@ -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 { + 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 { + 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> { + return db.select().from(keyValStore) +} diff --git a/packages/db_helper_postgres/src/lib/delete-orders.ts b/packages/db_helper_postgres/src/lib/delete-orders.ts new file mode 100644 index 0000000..b3e91d5 --- /dev/null +++ b/packages/db_helper_postgres/src/lib/delete-orders.ts @@ -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 + * @throws Error if deletion fails + */ +export async function deleteOrdersWithRelations(orderIds: number[]): Promise { + 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)) +} diff --git a/packages/db_helper_postgres/src/lib/health-check.ts b/packages/db_helper_postgres/src/lib/health-check.ts new file mode 100644 index 0000000..1dfabfb --- /dev/null +++ b/packages/db_helper_postgres/src/lib/health-check.ts @@ -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' } + } +} diff --git a/packages/db_helper_postgres/src/lib/seed.ts b/packages/db_helper_postgres/src/lib/seed.ts new file mode 100644 index 0000000..f3410b5 --- /dev/null +++ b/packages/db_helper_postgres/src/lib/seed.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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, + }) + } + } +} diff --git a/packages/db_helper_postgres/src/stores/store-helpers.ts b/packages/db_helper_postgres/src/stores/store-helpers.ts new file mode 100644 index 0000000..a18ab53 --- /dev/null +++ b/packages/db_helper_postgres/src/stores/store-helpers.ts @@ -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 { + 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 { + 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 { + return db.query.storeInfo.findMany({ + columns: { id: true, name: true, description: true }, + }) +} + +export async function getAllDeliverySlotsForCache(): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 +} + +// ============================================================================ +// USER NEGATIVITY STORE HELPERS +// ============================================================================ + +export interface UserNegativityData { + userId: number + totalNegativityScore: number +} + +export async function getAllUserNegativityScores(): Promise { + return db + .select({ + userId: userIncidents.userId, + totalNegativityScore: sum(userIncidents.negativityScore).mapWith(Number), + }) + .from(userIncidents) + .groupBy(userIncidents.userId) +} + +export async function getUserNegativityScore(userId: number): Promise { + const [result] = await db + .select({ + totalNegativityScore: sum(userIncidents.negativityScore).mapWith(Number), + }) + .from(userIncidents) + .where(eq(userIncidents.userId, userId)) + .limit(1) + + return result?.totalNegativityScore || 0 +} diff --git a/packages/db_helper_postgres/src/user-apis/auth.ts b/packages/db_helper_postgres/src/user-apis/auth.ts index 0ed5424..3347355 100644 --- a/packages/db_helper_postgres/src/user-apis/auth.ts +++ b/packages/db_helper_postgres/src/user-apis/auth.ts @@ -45,6 +45,103 @@ export async function getUserDetails(userId: number) { return details || null } +export async function isUserSuspended(userId: number): Promise { + 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: { name: string email: string diff --git a/packages/db_helper_postgres/src/user-apis/complaint.ts b/packages/db_helper_postgres/src/user-apis/complaint.ts index 02b2913..60391f7 100644 --- a/packages/db_helper_postgres/src/user-apis/complaint.ts +++ b/packages/db_helper_postgres/src/user-apis/complaint.ts @@ -30,10 +30,16 @@ export async function getUserComplaints(userId: number): Promise { +export async function createComplaint( + userId: number, + orderId: number | null, + complaintBody: string, + images?: string[] | null +): Promise { await db.insert(complaints).values({ userId, orderId, complaintBody, + images: images || null, }) } diff --git a/packages/db_helper_postgres/src/user-apis/order.ts b/packages/db_helper_postgres/src/user-apis/order.ts index 19fbfe3..7d80a3e 100644 --- a/packages/db_helper_postgres/src/user-apis/order.ts +++ b/packages/db_helper_postgres/src/user-apis/order.ts @@ -622,3 +622,112 @@ export async function getProductsForRecentOrders( .orderBy(desc(productInfo.createdAt)) .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 { + 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 +} + +export interface OrderWithCancellationData extends OrderWithFullData { + refunds: Array<{ + refundStatus: string + }> +} + +export async function getOrderByIdWithFullData( + orderId: number +): Promise { + 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 +} diff --git a/packages/db_helper_postgres/src/user-apis/product.ts b/packages/db_helper_postgres/src/user-apis/product.ts index 907bee3..06d6903 100644 --- a/packages/db_helper_postgres/src/user-apis/product.ts +++ b/packages/db_helper_postgres/src/user-apis/product.ts @@ -1,6 +1,6 @@ import { db } from '../db/db_index' -import { deliverySlotInfo, productInfo, productReviews, productSlots, specialDeals, storeInfo, units, users } from '../db/schema' -import { and, desc, eq, gt, sql } from 'drizzle-orm' +import { deliverySlotInfo, productInfo, productReviews, productSlots, productTags, specialDeals, storeInfo, units, users } from '../db/schema' +import { and, desc, eq, gt, inArray, sql } from 'drizzle-orm' import type { UserProductDetailData, UserProductReview } from '@packages/shared' const getStringArray = (value: unknown): string[] | null => { @@ -179,3 +179,87 @@ export async function createProductReview( 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 { + 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 { + 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 { + 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 +} diff --git a/packages/db_helper_postgres/src/user-apis/stores.ts b/packages/db_helper_postgres/src/user-apis/stores.ts index 2ae1875..402b0c5 100644 --- a/packages/db_helper_postgres/src/user-apis/stores.ts +++ b/packages/db_helper_postgres/src/user-apis/stores.ts @@ -2,7 +2,7 @@ import { db } from '../db/db_index' import { productInfo, storeInfo, units } from '../db/schema' import { and, eq, sql } 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 type StoreProductRow = { @@ -125,3 +125,17 @@ export async function getStoreDetail(storeId: number): Promise { + return db.query.storeInfo.findMany({ + columns: { + id: true, + name: true, + description: true, + }, + }) +} diff --git a/packages/shared/types/index.ts b/packages/shared/types/index.ts index e1116f4..75d647f 100644 --- a/packages/shared/types/index.ts +++ b/packages/shared/types/index.ts @@ -3,3 +3,4 @@ export type * from './admin'; export type * from './user'; +export type * from './store.types'; diff --git a/packages/shared/types/store.types.ts b/packages/shared/types/store.types.ts index 65c2e0e..10c3afd 100644 --- a/packages/shared/types/store.types.ts +++ b/packages/shared/types/store.types.ts @@ -1,14 +1,22 @@ /** * Store Types * 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; name: string; description: string | null; - imageUrl: string | null; - owner: number; - createdAt: Date; - updatedAt: Date; +} + +/** + * Response type for getStoresSummary endpoint + */ +export interface StoresSummaryResponse { + stores: StoreSummary[]; }