enh
This commit is contained in:
parent
9137b5e1e6
commit
89de986764
39 changed files with 1947 additions and 1221 deletions
|
|
@ -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<Date | null> => {
|
||||
const result = await db
|
||||
.select({ deliveryTime: deliverySlotInfo.deliveryTime })
|
||||
|
|
@ -22,13 +77,9 @@ const getNextDeliveryDate = async (productId: number): Promise<Date | null> => {
|
|||
.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" });
|
||||
}
|
||||
};
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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.");
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -155,6 +155,9 @@ export type {
|
|||
UserUpdateNotesResponse,
|
||||
UserRecentProduct,
|
||||
UserRecentProductsResponse,
|
||||
// Store types
|
||||
StoreSummary,
|
||||
StoresSummaryResponse,
|
||||
} from '@packages/shared';
|
||||
|
||||
export type {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
@ -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,8 +166,16 @@ export async function createStoreFile(storeId: number): Promise<string> {
|
|||
}
|
||||
|
||||
export async function createAllStoresFiles(): Promise<string[]> {
|
||||
// Fetch all store IDs from database
|
||||
/*
|
||||
// Old implementation - direct DB queries:
|
||||
import { db } from '@/src/db/db_index'
|
||||
import { storeInfo } from '@/src/db/schema'
|
||||
|
||||
const stores = await db.select({ id: storeInfo.id }).from(storeInfo)
|
||||
*/
|
||||
|
||||
// Fetch all store IDs from database using helper
|
||||
const stores = await getStoresSummary()
|
||||
|
||||
// Create cache files for all stores and collect URLs
|
||||
const results: string[] = []
|
||||
|
|
@ -289,7 +296,15 @@ async function createBannersFileInternal(): Promise<string> {
|
|||
}
|
||||
|
||||
async function createAllStoresFilesInternal(): Promise<string[]> {
|
||||
/*
|
||||
// Old implementation - direct DB queries:
|
||||
import { db } from '@/src/db/db_index'
|
||||
import { storeInfo } from '@/src/db/schema'
|
||||
|
||||
const stores = await db.select({ id: storeInfo.id }).from(storeInfo)
|
||||
*/
|
||||
|
||||
const stores = await getStoresSummary()
|
||||
const results: string[] = []
|
||||
|
||||
for (const store of stores) {
|
||||
|
|
|
|||
|
|
@ -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,7 +8,15 @@ export const computeConstants = async (): Promise<void> => {
|
|||
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}`;
|
||||
|
|
|
|||
|
|
@ -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<void>
|
||||
* @throws Error if deletion fails
|
||||
*/
|
||||
export const deleteOrders = async (orderIds: number[]): Promise<void> => {
|
||||
if (orderIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteOrdersWithRelations(orderIds);
|
||||
console.log(`Successfully deleted ${orderIds.length} orders and all related records`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to delete orders ${orderIds.join(', ')}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
// Old implementation - direct DB queries:
|
||||
import { db } from '@/src/db/db_index'
|
||||
import { orders, orderItems, orderStatus, payments, refunds, couponUsage, complaints } from '@/src/db/schema'
|
||||
import { eq, inArray } from 'drizzle-orm';
|
||||
|
||||
export const deleteOrders = async (orderIds: number[]): Promise<void> => {
|
||||
if (orderIds.length === 0) {
|
||||
return;
|
||||
|
|
@ -43,3 +61,4 @@ export const deleteOrders = async (orderIds: number[]): Promise<void> => {
|
|||
throw error;
|
||||
}
|
||||
};
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<void> => {
|
|||
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<void> => {
|
|||
slot: true,
|
||||
},
|
||||
});
|
||||
*/
|
||||
|
||||
const ordersData = await getOrdersByIdsWithFullData(orderIds);
|
||||
|
||||
const telegramMessage = formatOrderMessageWithFullData(ordersData);
|
||||
await sendTelegramMessage(telegramMessage);
|
||||
|
|
@ -143,6 +153,12 @@ export const startCancellationHandler = async (): Promise<void> => {
|
|||
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<void> => {
|
|||
refunds: true,
|
||||
},
|
||||
});
|
||||
*/
|
||||
|
||||
const orderData = await getOrderByIdWithFullData(cancellationData.orderId);
|
||||
|
||||
if (!orderData) {
|
||||
console.error('Order not found for cancellation:', cancellationData.orderId);
|
||||
|
|
|
|||
|
|
@ -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,6 +70,14 @@ 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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -1,405 +0,0 @@
|
|||
import { db } from '@/src/db/db_index'
|
||||
import {
|
||||
orders,
|
||||
orderItems,
|
||||
orderStatus,
|
||||
addresses,
|
||||
productInfo,
|
||||
paymentInfoTable,
|
||||
coupons,
|
||||
couponUsage,
|
||||
payments,
|
||||
cartItems,
|
||||
refunds,
|
||||
units,
|
||||
userDetails,
|
||||
} from '@/src/db/schema'
|
||||
import { eq, and, inArray, desc, gte } from 'drizzle-orm'
|
||||
|
||||
// ============ User/Auth Queries ============
|
||||
|
||||
/**
|
||||
* Get user details by user ID
|
||||
*/
|
||||
export async function getUserDetails(userId: number) {
|
||||
return db.query.userDetails.findFirst({
|
||||
where: eq(userDetails.userId, userId),
|
||||
})
|
||||
}
|
||||
|
||||
// ============ Address Queries ============
|
||||
|
||||
/**
|
||||
* Get user address by ID
|
||||
*/
|
||||
export async function getUserAddress(userId: number, addressId: number) {
|
||||
return db.query.addresses.findFirst({
|
||||
where: and(eq(addresses.userId, userId), eq(addresses.id, addressId)),
|
||||
})
|
||||
}
|
||||
|
||||
// ============ Product Queries ============
|
||||
|
||||
/**
|
||||
* Get product by ID
|
||||
*/
|
||||
export async function getProductById(productId: number) {
|
||||
return db.query.productInfo.findFirst({
|
||||
where: eq(productInfo.id, productId),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get multiple products by IDs with unit info
|
||||
*/
|
||||
export async function getProductsByIdsWithUnits(productIds: number[]) {
|
||||
return db
|
||||
.select({
|
||||
id: productInfo.id,
|
||||
name: productInfo.name,
|
||||
shortDescription: productInfo.shortDescription,
|
||||
price: productInfo.price,
|
||||
images: productInfo.images,
|
||||
isOutOfStock: productInfo.isOutOfStock,
|
||||
unitShortNotation: units.shortNotation,
|
||||
incrementStep: productInfo.incrementStep,
|
||||
})
|
||||
.from(productInfo)
|
||||
.innerJoin(units, eq(productInfo.unitId, units.id))
|
||||
.where(and(inArray(productInfo.id, productIds), eq(productInfo.isSuspended, false)))
|
||||
.orderBy(desc(productInfo.createdAt))
|
||||
}
|
||||
|
||||
// ============ Coupon Queries ============
|
||||
|
||||
/**
|
||||
* Get coupon with usages for user
|
||||
*/
|
||||
export async function getCouponWithUsages(couponId: number, userId: number) {
|
||||
return db.query.coupons.findFirst({
|
||||
where: eq(coupons.id, couponId),
|
||||
with: {
|
||||
usages: { where: eq(couponUsage.userId, userId) },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert coupon usage
|
||||
*/
|
||||
export async function insertCouponUsage(data: {
|
||||
userId: number
|
||||
couponId: number
|
||||
orderId: number
|
||||
orderItemId: number | null
|
||||
usedAt: Date
|
||||
}) {
|
||||
return db.insert(couponUsage).values(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get coupon usages for order
|
||||
*/
|
||||
export async function getCouponUsagesForOrder(orderId: number) {
|
||||
return db.query.couponUsage.findMany({
|
||||
where: eq(couponUsage.orderId, orderId),
|
||||
with: {
|
||||
coupon: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ============ Cart Queries ============
|
||||
|
||||
/**
|
||||
* Delete cart items for user by product IDs
|
||||
*/
|
||||
export async function deleteCartItems(userId: number, productIds: number[]) {
|
||||
return db.delete(cartItems).where(
|
||||
and(
|
||||
eq(cartItems.userId, userId),
|
||||
inArray(cartItems.productId, productIds)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// ============ Payment Info Queries ============
|
||||
|
||||
/**
|
||||
* Create payment info
|
||||
*/
|
||||
export async function createPaymentInfo(data: {
|
||||
status: string
|
||||
gateway: string
|
||||
merchantOrderId: string
|
||||
}) {
|
||||
return db.insert(paymentInfoTable).values(data).returning()
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ============ Order Queries ============
|
||||
|
||||
/**
|
||||
* Insert multiple orders
|
||||
*/
|
||||
export async function insertOrders(ordersData: any[]) {
|
||||
return db.insert(orders).values(ordersData).returning()
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert multiple order items
|
||||
*/
|
||||
export async function insertOrderItems(itemsData: any[]) {
|
||||
return db.insert(orderItems).values(itemsData)
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert multiple order statuses
|
||||
*/
|
||||
export async function insertOrderStatuses(statusesData: any[]) {
|
||||
return db.insert(orderStatus).values(statusesData)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user orders with all relations
|
||||
*/
|
||||
export async function getUserOrdersWithRelations(userId: number, limit: number, offset: number) {
|
||||
return db.query.orders.findMany({
|
||||
where: eq(orders.userId, userId),
|
||||
with: {
|
||||
orderItems: {
|
||||
with: {
|
||||
product: true,
|
||||
},
|
||||
},
|
||||
slot: true,
|
||||
paymentInfo: true,
|
||||
orderStatus: true,
|
||||
refunds: true,
|
||||
},
|
||||
orderBy: (orders, { desc }) => [desc(orders.createdAt)],
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Count user orders
|
||||
*/
|
||||
export async function countUserOrders(userId: number) {
|
||||
return db.$count(orders, eq(orders.userId, userId))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get order by ID with all relations
|
||||
*/
|
||||
export async function getOrderByIdWithRelations(orderId: number) {
|
||||
return db.query.orders.findFirst({
|
||||
where: eq(orders.id, orderId),
|
||||
with: {
|
||||
orderItems: {
|
||||
with: {
|
||||
product: true,
|
||||
},
|
||||
},
|
||||
slot: true,
|
||||
paymentInfo: true,
|
||||
orderStatus: {
|
||||
with: {
|
||||
refundCoupon: true,
|
||||
},
|
||||
},
|
||||
refunds: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get order by ID with order status
|
||||
*/
|
||||
export async function getOrderWithStatus(orderId: number) {
|
||||
return db.query.orders.findFirst({
|
||||
where: eq(orders.id, orderId),
|
||||
with: {
|
||||
orderStatus: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Update order status to cancelled
|
||||
*/
|
||||
export async function updateOrderStatusToCancelled(
|
||||
statusId: number,
|
||||
data: {
|
||||
isCancelled: boolean
|
||||
cancelReason: string
|
||||
cancellationUserNotes: string
|
||||
cancellationReviewed: boolean
|
||||
}
|
||||
) {
|
||||
return db
|
||||
.update(orderStatus)
|
||||
.set(data)
|
||||
.where(eq(orderStatus.id, statusId))
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert refund record
|
||||
*/
|
||||
export async function insertRefund(data: { orderId: number; refundStatus: string }) {
|
||||
return db.insert(refunds).values(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update order notes
|
||||
*/
|
||||
export async function updateOrderNotes(orderId: number, userNotes: string | null) {
|
||||
return db
|
||||
.update(orders)
|
||||
.set({ userNotes })
|
||||
.where(eq(orders.id, orderId))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent delivered orders for user
|
||||
*/
|
||||
export async function getRecentDeliveredOrders(
|
||||
userId: number,
|
||||
since: Date,
|
||||
limit: number
|
||||
) {
|
||||
return db
|
||||
.select({ id: orders.id })
|
||||
.from(orders)
|
||||
.innerJoin(orderStatus, eq(orders.id, orderStatus.orderId))
|
||||
.where(
|
||||
and(
|
||||
eq(orders.userId, userId),
|
||||
eq(orderStatus.isDelivered, true),
|
||||
gte(orders.createdAt, since)
|
||||
)
|
||||
)
|
||||
.orderBy(desc(orders.createdAt))
|
||||
.limit(limit)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get order items by order IDs
|
||||
*/
|
||||
export async function getOrderItemsByOrderIds(orderIds: number[]) {
|
||||
return db
|
||||
.select({ productId: orderItems.productId })
|
||||
.from(orderItems)
|
||||
.where(inArray(orderItems.orderId, orderIds))
|
||||
}
|
||||
|
||||
// ============ Transaction Helper ============
|
||||
|
||||
/**
|
||||
* Execute function within a database transaction
|
||||
*/
|
||||
export async function withTransaction<T>(fn: (tx: any) => Promise<T>): Promise<T> {
|
||||
return db.transaction(fn)
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel order with refund record in a transaction
|
||||
*/
|
||||
export async function cancelOrderWithRefund(
|
||||
statusId: number,
|
||||
orderId: number,
|
||||
isCod: boolean,
|
||||
reason: string
|
||||
): Promise<{ orderId: number }> {
|
||||
return db.transaction(async (tx) => {
|
||||
// Update order status
|
||||
await tx
|
||||
.update(orderStatus)
|
||||
.set({
|
||||
isCancelled: true,
|
||||
cancelReason: reason,
|
||||
cancellationUserNotes: reason,
|
||||
cancellationReviewed: false,
|
||||
})
|
||||
.where(eq(orderStatus.id, statusId))
|
||||
|
||||
// Insert refund record
|
||||
const refundStatus = isCod ? "na" : "pending"
|
||||
await tx.insert(refunds).values({
|
||||
orderId,
|
||||
refundStatus,
|
||||
})
|
||||
|
||||
return { orderId }
|
||||
})
|
||||
}
|
||||
|
||||
type Tx = Parameters<Parameters<typeof db.transaction>[0]>[0]
|
||||
|
||||
/**
|
||||
* Create orders with payment info in a transaction
|
||||
*/
|
||||
export async function createOrdersWithPayment(
|
||||
ordersData: any[],
|
||||
paymentMethod: "online" | "cod",
|
||||
totalWithDelivery: number,
|
||||
razorpayOrderCreator?: (paymentInfoId: number, amount: string) => Promise<any>,
|
||||
paymentRecordInserter?: (paymentInfoId: number, razorpayOrder: any, tx: Tx) => Promise<any>
|
||||
): Promise<typeof orders.$inferSelect[]> {
|
||||
return db.transaction(async (tx) => {
|
||||
let sharedPaymentInfoId: number | null = null
|
||||
if (paymentMethod === "online") {
|
||||
const [paymentInfo] = await tx
|
||||
.insert(paymentInfoTable)
|
||||
.values({
|
||||
status: "pending",
|
||||
gateway: "razorpay",
|
||||
merchantOrderId: `multi_order_${Date.now()}`,
|
||||
})
|
||||
.returning()
|
||||
sharedPaymentInfoId = paymentInfo.id
|
||||
}
|
||||
|
||||
const ordersToInsert: Omit<typeof orders.$inferInsert, "id">[] = ordersData.map(
|
||||
(od) => ({
|
||||
...od.order,
|
||||
paymentInfoId: sharedPaymentInfoId,
|
||||
})
|
||||
)
|
||||
|
||||
const insertedOrders = await tx.insert(orders).values(ordersToInsert).returning()
|
||||
|
||||
const allOrderItems: Omit<typeof orderItems.$inferInsert, "id">[] = []
|
||||
const allOrderStatuses: Omit<typeof orderStatus.$inferInsert, "id">[] = []
|
||||
|
||||
insertedOrders.forEach((order: typeof orders.$inferSelect, index: number) => {
|
||||
const od = ordersData[index]
|
||||
od.orderItems.forEach((item: any) => {
|
||||
allOrderItems.push({ ...item, orderId: order.id as number })
|
||||
})
|
||||
allOrderStatuses.push({
|
||||
...od.orderStatus,
|
||||
orderId: order.id as number,
|
||||
})
|
||||
})
|
||||
|
||||
await tx.insert(orderItems).values(allOrderItems)
|
||||
await tx.insert(orderStatus).values(allOrderStatuses)
|
||||
|
||||
if (paymentMethod === "online" && sharedPaymentInfoId && razorpayOrderCreator && paymentRecordInserter) {
|
||||
const razorpayOrder = await razorpayOrderCreator(
|
||||
sharedPaymentInfoId,
|
||||
totalWithDelivery.toString()
|
||||
)
|
||||
await paymentRecordInserter(
|
||||
sharedPaymentInfoId,
|
||||
razorpayOrder,
|
||||
tx
|
||||
)
|
||||
}
|
||||
|
||||
return insertedOrders
|
||||
})
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -1,37 +1,43 @@
|
|||
// 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<void> {
|
||||
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,
|
||||
|
|
@ -40,53 +46,52 @@ export async function initializeBannerStore(): Promise<void> {
|
|||
serialNum: banner.serialNum,
|
||||
productIds: banner.productIds,
|
||||
createdAt: banner.createdAt,
|
||||
// updatedAt: banner.updatedAt,
|
||||
};
|
||||
|
||||
await redisClient.set(`banner:${banner.id}`, JSON.stringify(bannerObj));
|
||||
}
|
||||
|
||||
console.log('Banner store initialized successfully');
|
||||
await redisClient.set(`banner:${banner.id}`, JSON.stringify(bannerObj))
|
||||
}
|
||||
|
||||
console.log('Banner store initialized successfully')
|
||||
} catch (error) {
|
||||
console.error('Error initializing banner store:', error);
|
||||
console.error('Error initializing banner store:', error)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getBannerById(id: number): Promise<Banner | null> {
|
||||
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<Banner[]> {
|
||||
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 []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
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<void> {
|
|||
})
|
||||
.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<number, typeof allDeliverySlots>();
|
||||
const allDeliverySlots = await getAllDeliverySlotsForCache()
|
||||
const deliverySlotsMap = new Map<number, DeliverySlotData[]>()
|
||||
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<number, typeof allSpecialDeals>();
|
||||
const allSpecialDeals = await getAllSpecialDealsForCache()
|
||||
const specialDealsMap = new Map<number, SpecialDealData[]>()
|
||||
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<number, string[]>();
|
||||
const allProductTags = await getAllProductTagsForCache()
|
||||
const productTagsMap = new Map<number, string[]>()
|
||||
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<void> {
|
|||
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));
|
||||
}
|
||||
|
||||
console.log('Product store initialized successfully');
|
||||
await redisClient.set(`product:${product.id}`, JSON.stringify(productObj))
|
||||
}
|
||||
|
||||
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<Product | null> {
|
||||
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<Product[]> {
|
||||
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 []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
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<void> {
|
|||
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,19 +58,22 @@ export async function initializeProductTagStore(): Promise<void> {
|
|||
})
|
||||
.from(productTags)
|
||||
.where(inArray(productTags.tagId, tagIds));
|
||||
*/
|
||||
|
||||
// Group product IDs by tag
|
||||
const productIdsByTag = new Map<number, number[]>();
|
||||
const productIdsByTag = new Map<number, number[]>()
|
||||
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,
|
||||
|
|
@ -63,109 +83,109 @@ export async function initializeProductTagStore(): Promise<void> {
|
|||
isDashboardTag: tag.isDashboardTag,
|
||||
relatedStores: (tag.relatedStores as number[]) || [],
|
||||
productIds: productIdsByTag.get(tag.id) || [],
|
||||
};
|
||||
|
||||
await redisClient.set(`tag:${tag.id}`, JSON.stringify(tagObj));
|
||||
}
|
||||
|
||||
console.log('Product tag store initialized successfully');
|
||||
await redisClient.set(`tag:${tag.id}`, JSON.stringify(tagObj))
|
||||
}
|
||||
|
||||
console.log('Product tag store initialized successfully')
|
||||
} catch (error) {
|
||||
console.error('Error initializing product tag store:', error);
|
||||
console.error('Error initializing product tag store:', error)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTagById(id: number): Promise<Tag | null> {
|
||||
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<Tag[]> {
|
||||
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<Tag[]> {
|
||||
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<Tag[]> {
|
||||
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 []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
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<void> {
|
|||
},
|
||||
orderBy: asc(deliverySlotInfo.deliveryTime),
|
||||
});
|
||||
*/
|
||||
|
||||
// Transform data for storage
|
||||
const slotsWithProducts = await Promise.all(
|
||||
|
|
@ -79,151 +88,156 @@ export async function initializeSlotStore(): Promise<void> {
|
|||
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<number, SlotInfo[]> = {};
|
||||
const productSlotsMap: Record<number, SlotInfo[]> = {}
|
||||
|
||||
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<SlotWithProducts | null> {
|
||||
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<SlotWithProducts[]> {
|
||||
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<SlotInfo[]> {
|
||||
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<Record<number, SlotInfo[]>> {
|
||||
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<number, SlotInfo[]> = {};
|
||||
const result: Record<number, SlotInfo[]> = {}
|
||||
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<Record<number, SlotInfo[]>> {
|
||||
export async function getMultipleProductsSlots(
|
||||
productIds: number[]
|
||||
): Promise<Record<number, SlotInfo[]>> {
|
||||
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<number, SlotInfo[]> = {};
|
||||
const result: Record<number, SlotInfo[]> = {}
|
||||
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 {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
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<void> {
|
|||
})
|
||||
.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<number> {
|
||||
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<Record<number, number>> {
|
||||
try {
|
||||
const keys = await redisClient.KEYS('user:negativity:*');
|
||||
const keys = await redisClient.KEYS('user:negativity:*')
|
||||
|
||||
if (keys.length === 0) return {};
|
||||
if (keys.length === 0) return {}
|
||||
|
||||
const values = await redisClient.MGET(keys);
|
||||
const values = await redisClient.MGET(keys)
|
||||
|
||||
const result: Record<number, number> = {};
|
||||
const result: Record<number, number> = {}
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
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<Record<number, number>> {
|
||||
export async function getMultipleUserNegativityScores(
|
||||
userIds: number[]
|
||||
): Promise<Record<number, number>> {
|
||||
try {
|
||||
if (userIds.length === 0) return {};
|
||||
if (userIds.length === 0) return {}
|
||||
|
||||
const keys = userIds.map(id => `user:negativity:${id}`);
|
||||
const keys = userIds.map((id) => `user:negativity:${id}`)
|
||||
|
||||
const values = await redisClient.MGET(keys);
|
||||
const values = await redisClient.MGET(keys)
|
||||
|
||||
const result: Record<number, number> = {};
|
||||
const result: Record<number, number> = {}
|
||||
for (let i = 0; i < userIds.length; i++) {
|
||||
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<void> {
|
||||
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<void
|
|||
.limit(1);
|
||||
|
||||
const totalScore = result?.totalNegativityScore || 0;
|
||||
*/
|
||||
|
||||
const key = `user:negativity:${userId}`;
|
||||
await redisClient.set(key, totalScore.toString());
|
||||
const key = `user:negativity:${userId}`
|
||||
await redisClient.set(key, totalScore.toString())
|
||||
} catch (error) {
|
||||
console.error(`Error recomputing negativity score for user ${userId}:`, error);
|
||||
throw error;
|
||||
console.error(`Error recomputing negativity score for user ${userId}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index'
|
||||
import { commonRouter } from '@/src/trpc/apis/common-apis/common'
|
||||
import { db } from '@/src/db/db_index'
|
||||
import { keyValStore, productInfo, storeInfo } from '@/src/db/schema'
|
||||
import {
|
||||
getStoresSummary,
|
||||
healthCheck,
|
||||
} from '@/src/dbService'
|
||||
import type { StoresSummaryResponse } from '@packages/shared'
|
||||
import * as turf from '@turf/turf';
|
||||
import { z } from 'zod';
|
||||
import { mbnrGeoJson } from '@/src/lib/mbnr-geojson'
|
||||
|
|
@ -37,8 +40,14 @@ export async function scaffoldEssentialConsts() {
|
|||
|
||||
export const commonApiRouter = router({
|
||||
product: commonRouter,
|
||||
|
||||
getStoresSummary: publicProcedure
|
||||
.query(async () => {
|
||||
.query(async (): Promise<StoresSummaryResponse> => {
|
||||
/*
|
||||
// Old implementation - direct DB queries:
|
||||
import { db } from '@/src/db/db_index'
|
||||
import { storeInfo } from '@/src/db/schema'
|
||||
|
||||
const stores = await db.query.storeInfo.findMany({
|
||||
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 () => {
|
||||
/*
|
||||
// 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);
|
||||
*/
|
||||
|
||||
return {
|
||||
status: "ok",
|
||||
};
|
||||
const result = await healthCheck();
|
||||
return result;
|
||||
}),
|
||||
|
||||
essentialConsts: publicProcedure
|
||||
.query(async () => {
|
||||
const response = await scaffoldEssentialConsts();
|
||||
|
|
|
|||
|
|
@ -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<Date | null> => {
|
||||
const result = await db
|
||||
.select({ deliveryTime: deliverySlotInfo.deliveryTime })
|
||||
.from(productSlots)
|
||||
.innerJoin(deliverySlotInfo, eq(productSlots.slotId, deliverySlotInfo.id))
|
||||
.where(
|
||||
and(
|
||||
eq(productSlots.productId, productId),
|
||||
eq(deliverySlotInfo.isActive, true),
|
||||
eq(deliverySlotInfo.isCapacityFull, false),
|
||||
gt(deliverySlotInfo.deliveryTime, sql`NOW()`)
|
||||
)
|
||||
)
|
||||
.orderBy(deliverySlotInfo.deliveryTime)
|
||||
.limit(1);
|
||||
|
||||
|
||||
return result[0]?.deliveryTime || null;
|
||||
};
|
||||
// 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;
|
||||
}),
|
||||
*/
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
await tx.insert(userCreds).values({
|
||||
userId: user.id,
|
||||
userPassword: hashedPassword,
|
||||
});
|
||||
|
||||
// Create user details with profile image
|
||||
await tx
|
||||
.insert(userDetails)
|
||||
.values({
|
||||
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';
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
// Old implementation - direct DB queries:
|
||||
import { db } from '@/src/db/db_index'
|
||||
import { complaints } from '@/src/db/schema'
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -18,6 +18,14 @@ export async function getStaffUserByName(name: string): Promise<StaffUser | null
|
|||
return staff || null;
|
||||
}
|
||||
|
||||
export async function getStaffUserById(staffId: number): Promise<StaffUser | null> {
|
||||
const staff = await db.query.staffUsers.findFirst({
|
||||
where: eq(staffUsers.id, staffId),
|
||||
});
|
||||
|
||||
return staff || null;
|
||||
}
|
||||
|
||||
export async function getAllStaff(): Promise<any[]> {
|
||||
const staff = await db.query.staffUsers.findMany({
|
||||
columns: {
|
||||
|
|
|
|||
41
packages/db_helper_postgres/src/lib/automated-jobs.ts
Normal file
41
packages/db_helper_postgres/src/lib/automated-jobs.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { db } from '../db/db_index'
|
||||
import { productInfo, keyValStore } from '../db/schema'
|
||||
import { inArray, eq } from 'drizzle-orm'
|
||||
|
||||
/**
|
||||
* Toggle flash delivery availability for specific products
|
||||
* @param isAvailable - Whether flash delivery should be available
|
||||
* @param productIds - Array of product IDs to update
|
||||
*/
|
||||
export async function toggleFlashDeliveryForItems(
|
||||
isAvailable: boolean,
|
||||
productIds: number[]
|
||||
): Promise<void> {
|
||||
await db
|
||||
.update(productInfo)
|
||||
.set({ isFlashAvailable: isAvailable })
|
||||
.where(inArray(productInfo.id, productIds))
|
||||
}
|
||||
|
||||
/**
|
||||
* Update key-value store
|
||||
* @param key - The key to update
|
||||
* @param value - The boolean value to set
|
||||
*/
|
||||
export async function toggleKeyVal(
|
||||
key: string,
|
||||
value: boolean
|
||||
): Promise<void> {
|
||||
await db
|
||||
.update(keyValStore)
|
||||
.set({ value })
|
||||
.where(eq(keyValStore.key, key))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all key-value store constants
|
||||
* @returns Array of all key-value pairs
|
||||
*/
|
||||
export async function getAllKeyValStore(): Promise<Array<{ key: string; value: any }>> {
|
||||
return db.select().from(keyValStore)
|
||||
}
|
||||
38
packages/db_helper_postgres/src/lib/delete-orders.ts
Normal file
38
packages/db_helper_postgres/src/lib/delete-orders.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { db } from '../db/db_index'
|
||||
import { orders, orderItems, orderStatus, payments, refunds, couponUsage, complaints } from '../db/schema'
|
||||
import { inArray } from 'drizzle-orm'
|
||||
|
||||
/**
|
||||
* Delete orders and all their related records
|
||||
* @param orderIds Array of order IDs to delete
|
||||
* @returns Promise<void>
|
||||
* @throws Error if deletion fails
|
||||
*/
|
||||
export async function deleteOrdersWithRelations(orderIds: number[]): Promise<void> {
|
||||
if (orderIds.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete child records first (in correct order to avoid FK constraint errors)
|
||||
|
||||
// 1. Delete coupon usage records
|
||||
await db.delete(couponUsage).where(inArray(couponUsage.orderId, orderIds))
|
||||
|
||||
// 2. Delete complaints related to these orders
|
||||
await db.delete(complaints).where(inArray(complaints.orderId, orderIds))
|
||||
|
||||
// 3. Delete refunds
|
||||
await db.delete(refunds).where(inArray(refunds.orderId, orderIds))
|
||||
|
||||
// 4. Delete payments
|
||||
await db.delete(payments).where(inArray(payments.orderId, orderIds))
|
||||
|
||||
// 5. Delete order status records
|
||||
await db.delete(orderStatus).where(inArray(orderStatus.orderId, orderIds))
|
||||
|
||||
// 6. Delete order items
|
||||
await db.delete(orderItems).where(inArray(orderItems.orderId, orderIds))
|
||||
|
||||
// 7. Finally delete the orders themselves
|
||||
await db.delete(orders).where(inArray(orders.id, orderIds))
|
||||
}
|
||||
18
packages/db_helper_postgres/src/lib/health-check.ts
Normal file
18
packages/db_helper_postgres/src/lib/health-check.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { db } from '../db/db_index'
|
||||
import { keyValStore, productInfo } from '../db/schema'
|
||||
|
||||
/**
|
||||
* Health check - test database connectivity
|
||||
* Tries to select from keyValStore first, falls back to productInfo
|
||||
*/
|
||||
export async function healthCheck(): Promise<{ status: string }> {
|
||||
try {
|
||||
// Try keyValStore first (smaller table)
|
||||
await db.select({ key: keyValStore.key }).from(keyValStore).limit(1)
|
||||
return { status: 'ok' }
|
||||
} catch {
|
||||
// Fallback to productInfo
|
||||
await db.select({ name: productInfo.name }).from(productInfo).limit(1)
|
||||
return { status: 'ok' }
|
||||
}
|
||||
}
|
||||
127
packages/db_helper_postgres/src/lib/seed.ts
Normal file
127
packages/db_helper_postgres/src/lib/seed.ts
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import { db } from '../db/db_index'
|
||||
import { eq, and } from 'drizzle-orm'
|
||||
|
||||
// ============================================================================
|
||||
// Unit Seed Helper
|
||||
// ============================================================================
|
||||
|
||||
export interface UnitSeedData {
|
||||
shortNotation: string
|
||||
fullName: string
|
||||
}
|
||||
|
||||
export async function seedUnits(unitsToSeed: UnitSeedData[]): Promise<void> {
|
||||
for (const unit of unitsToSeed) {
|
||||
const { units: unitsTable } = await import('../db/schema')
|
||||
const existingUnit = await db.query.units.findFirst({
|
||||
where: eq(unitsTable.shortNotation, unit.shortNotation),
|
||||
})
|
||||
if (!existingUnit) {
|
||||
await db.insert(unitsTable).values(unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Staff Role Seed Helper
|
||||
// ============================================================================
|
||||
|
||||
// Type for staff role names based on the enum values in schema
|
||||
export type StaffRoleName = 'super_admin' | 'admin' | 'marketer' | 'delivery_staff'
|
||||
|
||||
export async function seedStaffRoles(rolesToSeed: StaffRoleName[]): Promise<void> {
|
||||
for (const roleName of rolesToSeed) {
|
||||
const { staffRoles } = await import('../db/schema')
|
||||
const existingRole = await db.query.staffRoles.findFirst({
|
||||
where: eq(staffRoles.roleName, roleName),
|
||||
})
|
||||
if (!existingRole) {
|
||||
await db.insert(staffRoles).values({ roleName })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Staff Permission Seed Helper
|
||||
// ============================================================================
|
||||
|
||||
// Type for staff permission names based on the enum values in schema
|
||||
export type StaffPermissionName = 'crud_product' | 'make_coupon' | 'crud_staff_users'
|
||||
|
||||
export async function seedStaffPermissions(permissionsToSeed: StaffPermissionName[]): Promise<void> {
|
||||
for (const permissionName of permissionsToSeed) {
|
||||
const { staffPermissions } = await import('../db/schema')
|
||||
const existingPermission = await db.query.staffPermissions.findFirst({
|
||||
where: eq(staffPermissions.permissionName, permissionName),
|
||||
})
|
||||
if (!existingPermission) {
|
||||
await db.insert(staffPermissions).values({ permissionName })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Role-Permission Assignment Helper
|
||||
// ============================================================================
|
||||
|
||||
export interface RolePermissionAssignment {
|
||||
roleName: StaffRoleName
|
||||
permissionName: StaffPermissionName
|
||||
}
|
||||
|
||||
export async function seedRolePermissions(assignments: RolePermissionAssignment[]): Promise<void> {
|
||||
await db.transaction(async (tx) => {
|
||||
const { staffRoles, staffPermissions, staffRolePermissions } = await import('../db/schema')
|
||||
|
||||
for (const assignment of assignments) {
|
||||
// Get role ID
|
||||
const role = await tx.query.staffRoles.findFirst({
|
||||
where: eq(staffRoles.roleName, assignment.roleName),
|
||||
})
|
||||
|
||||
// Get permission ID
|
||||
const permission = await tx.query.staffPermissions.findFirst({
|
||||
where: eq(staffPermissions.permissionName, assignment.permissionName),
|
||||
})
|
||||
|
||||
if (role && permission) {
|
||||
const existing = await tx.query.staffRolePermissions.findFirst({
|
||||
where: and(
|
||||
eq(staffRolePermissions.staffRoleId, role.id),
|
||||
eq(staffRolePermissions.staffPermissionId, permission.id)
|
||||
),
|
||||
})
|
||||
if (!existing) {
|
||||
await tx.insert(staffRolePermissions).values({
|
||||
staffRoleId: role.id,
|
||||
staffPermissionId: permission.id,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Key-Value Store Seed Helper
|
||||
// ============================================================================
|
||||
|
||||
export interface KeyValSeedData {
|
||||
key: string
|
||||
value: any
|
||||
}
|
||||
|
||||
export async function seedKeyValStore(constantsToSeed: KeyValSeedData[]): Promise<void> {
|
||||
for (const constant of constantsToSeed) {
|
||||
const { keyValStore } = await import('../db/schema')
|
||||
const existing = await db.query.keyValStore.findFirst({
|
||||
where: eq(keyValStore.key, constant.key),
|
||||
})
|
||||
if (!existing) {
|
||||
await db.insert(keyValStore).values({
|
||||
key: constant.key,
|
||||
value: constant.value,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
276
packages/db_helper_postgres/src/stores/store-helpers.ts
Normal file
276
packages/db_helper_postgres/src/stores/store-helpers.ts
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
// Store Helpers - Database operations for cache initialization
|
||||
// These are used by stores in apps/backend/src/stores/
|
||||
|
||||
import { db } from '../db/db_index'
|
||||
import {
|
||||
homeBanners,
|
||||
productInfo,
|
||||
units,
|
||||
productSlots,
|
||||
deliverySlotInfo,
|
||||
specialDeals,
|
||||
storeInfo,
|
||||
productTags,
|
||||
productTagInfo,
|
||||
userIncidents,
|
||||
} from '../db/schema'
|
||||
import { eq, and, gt, sql, inArray, isNotNull, asc, sum } from 'drizzle-orm'
|
||||
|
||||
// ============================================================================
|
||||
// BANNER STORE HELPERS
|
||||
// ============================================================================
|
||||
|
||||
export interface BannerData {
|
||||
id: number
|
||||
name: string
|
||||
imageUrl: string | null
|
||||
serialNum: number | null
|
||||
productIds: number[] | null
|
||||
createdAt: Date
|
||||
}
|
||||
|
||||
export async function getAllBannersForCache(): Promise<BannerData[]> {
|
||||
return db.query.homeBanners.findMany({
|
||||
where: isNotNull(homeBanners.serialNum),
|
||||
orderBy: asc(homeBanners.serialNum),
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PRODUCT STORE HELPERS
|
||||
// ============================================================================
|
||||
|
||||
export interface ProductBasicData {
|
||||
id: number
|
||||
name: string
|
||||
shortDescription: string | null
|
||||
longDescription: string | null
|
||||
price: string
|
||||
marketPrice: string | null
|
||||
images: unknown
|
||||
isOutOfStock: boolean
|
||||
storeId: number | null
|
||||
unitShortNotation: string
|
||||
incrementStep: number
|
||||
productQuantity: number
|
||||
isFlashAvailable: boolean
|
||||
flashPrice: string | null
|
||||
}
|
||||
|
||||
export interface StoreBasicData {
|
||||
id: number
|
||||
name: string
|
||||
description: string | null
|
||||
}
|
||||
|
||||
export interface DeliverySlotData {
|
||||
productId: number
|
||||
id: number
|
||||
deliveryTime: Date
|
||||
freezeTime: Date
|
||||
isCapacityFull: boolean
|
||||
}
|
||||
|
||||
export interface SpecialDealData {
|
||||
productId: number
|
||||
quantity: string
|
||||
price: string
|
||||
validTill: Date
|
||||
}
|
||||
|
||||
export interface ProductTagData {
|
||||
productId: number
|
||||
tagName: string
|
||||
}
|
||||
|
||||
export async function getAllProductsForCache(): Promise<ProductBasicData[]> {
|
||||
return db
|
||||
.select({
|
||||
id: productInfo.id,
|
||||
name: productInfo.name,
|
||||
shortDescription: productInfo.shortDescription,
|
||||
longDescription: productInfo.longDescription,
|
||||
price: productInfo.price,
|
||||
marketPrice: productInfo.marketPrice,
|
||||
images: productInfo.images,
|
||||
isOutOfStock: productInfo.isOutOfStock,
|
||||
storeId: productInfo.storeId,
|
||||
unitShortNotation: units.shortNotation,
|
||||
incrementStep: productInfo.incrementStep,
|
||||
productQuantity: productInfo.productQuantity,
|
||||
isFlashAvailable: productInfo.isFlashAvailable,
|
||||
flashPrice: productInfo.flashPrice,
|
||||
})
|
||||
.from(productInfo)
|
||||
.innerJoin(units, eq(productInfo.unitId, units.id))
|
||||
}
|
||||
|
||||
export async function getAllStoresForCache(): Promise<StoreBasicData[]> {
|
||||
return db.query.storeInfo.findMany({
|
||||
columns: { id: true, name: true, description: true },
|
||||
})
|
||||
}
|
||||
|
||||
export async function getAllDeliverySlotsForCache(): Promise<DeliverySlotData[]> {
|
||||
return db
|
||||
.select({
|
||||
productId: productSlots.productId,
|
||||
id: deliverySlotInfo.id,
|
||||
deliveryTime: deliverySlotInfo.deliveryTime,
|
||||
freezeTime: deliverySlotInfo.freezeTime,
|
||||
isCapacityFull: deliverySlotInfo.isCapacityFull,
|
||||
})
|
||||
.from(productSlots)
|
||||
.innerJoin(deliverySlotInfo, eq(productSlots.slotId, deliverySlotInfo.id))
|
||||
.where(
|
||||
and(
|
||||
eq(deliverySlotInfo.isActive, true),
|
||||
eq(deliverySlotInfo.isCapacityFull, false),
|
||||
gt(deliverySlotInfo.deliveryTime, sql`NOW()`)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export async function getAllSpecialDealsForCache(): Promise<SpecialDealData[]> {
|
||||
return db
|
||||
.select({
|
||||
productId: specialDeals.productId,
|
||||
quantity: specialDeals.quantity,
|
||||
price: specialDeals.price,
|
||||
validTill: specialDeals.validTill,
|
||||
})
|
||||
.from(specialDeals)
|
||||
.where(gt(specialDeals.validTill, sql`NOW()`))
|
||||
}
|
||||
|
||||
export async function getAllProductTagsForCache(): Promise<ProductTagData[]> {
|
||||
return db
|
||||
.select({
|
||||
productId: productTags.productId,
|
||||
tagName: productTagInfo.tagName,
|
||||
})
|
||||
.from(productTags)
|
||||
.innerJoin(productTagInfo, eq(productTags.tagId, productTagInfo.id))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PRODUCT TAG STORE HELPERS
|
||||
// ============================================================================
|
||||
|
||||
export interface TagBasicData {
|
||||
id: number
|
||||
tagName: string
|
||||
tagDescription: string | null
|
||||
imageUrl: string | null
|
||||
isDashboardTag: boolean
|
||||
relatedStores: unknown
|
||||
}
|
||||
|
||||
export interface TagProductMapping {
|
||||
tagId: number
|
||||
productId: number
|
||||
}
|
||||
|
||||
export async function getAllTagsForCache(): Promise<TagBasicData[]> {
|
||||
return db
|
||||
.select({
|
||||
id: productTagInfo.id,
|
||||
tagName: productTagInfo.tagName,
|
||||
tagDescription: productTagInfo.tagDescription,
|
||||
imageUrl: productTagInfo.imageUrl,
|
||||
isDashboardTag: productTagInfo.isDashboardTag,
|
||||
relatedStores: productTagInfo.relatedStores,
|
||||
})
|
||||
.from(productTagInfo)
|
||||
}
|
||||
|
||||
export async function getAllTagProductMappings(): Promise<TagProductMapping[]> {
|
||||
return db
|
||||
.select({
|
||||
tagId: productTags.tagId,
|
||||
productId: productTags.productId,
|
||||
})
|
||||
.from(productTags)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SLOT STORE HELPERS
|
||||
// ============================================================================
|
||||
|
||||
export interface SlotWithProductsData {
|
||||
id: number
|
||||
deliveryTime: Date
|
||||
freezeTime: Date
|
||||
isActive: boolean
|
||||
isCapacityFull: boolean
|
||||
productSlots: Array<{
|
||||
product: {
|
||||
id: number
|
||||
name: string
|
||||
productQuantity: number
|
||||
shortDescription: string | null
|
||||
price: string
|
||||
marketPrice: string | null
|
||||
unit: { shortNotation: string } | null
|
||||
store: { id: number; name: string; description: string | null } | null
|
||||
images: unknown
|
||||
isOutOfStock: boolean
|
||||
storeId: number | null
|
||||
}
|
||||
}>
|
||||
}
|
||||
|
||||
export async function getAllSlotsWithProductsForCache(): Promise<SlotWithProductsData[]> {
|
||||
const now = new Date()
|
||||
|
||||
return db.query.deliverySlotInfo.findMany({
|
||||
where: and(
|
||||
eq(deliverySlotInfo.isActive, true),
|
||||
gt(deliverySlotInfo.deliveryTime, now),
|
||||
),
|
||||
with: {
|
||||
productSlots: {
|
||||
with: {
|
||||
product: {
|
||||
with: {
|
||||
unit: true,
|
||||
store: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: asc(deliverySlotInfo.deliveryTime),
|
||||
}) as Promise<SlotWithProductsData[]>
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// USER NEGATIVITY STORE HELPERS
|
||||
// ============================================================================
|
||||
|
||||
export interface UserNegativityData {
|
||||
userId: number
|
||||
totalNegativityScore: number
|
||||
}
|
||||
|
||||
export async function getAllUserNegativityScores(): Promise<UserNegativityData[]> {
|
||||
return db
|
||||
.select({
|
||||
userId: userIncidents.userId,
|
||||
totalNegativityScore: sum(userIncidents.negativityScore).mapWith(Number),
|
||||
})
|
||||
.from(userIncidents)
|
||||
.groupBy(userIncidents.userId)
|
||||
}
|
||||
|
||||
export async function getUserNegativityScore(userId: number): Promise<number> {
|
||||
const [result] = await db
|
||||
.select({
|
||||
totalNegativityScore: sum(userIncidents.negativityScore).mapWith(Number),
|
||||
})
|
||||
.from(userIncidents)
|
||||
.where(eq(userIncidents.userId, userId))
|
||||
.limit(1)
|
||||
|
||||
return result?.totalNegativityScore || 0
|
||||
}
|
||||
|
|
@ -45,6 +45,103 @@ export async function getUserDetails(userId: number) {
|
|||
return details || null
|
||||
}
|
||||
|
||||
export async function isUserSuspended(userId: number): Promise<boolean> {
|
||||
const details = await getUserDetails(userId)
|
||||
return details?.isSuspended ?? false
|
||||
}
|
||||
|
||||
export async function createUserWithProfile(input: {
|
||||
name: string
|
||||
email: string
|
||||
mobile: string
|
||||
hashedPassword: string
|
||||
profileImage?: string | null
|
||||
}) {
|
||||
return db.transaction(async (tx) => {
|
||||
// Create user
|
||||
const [user] = await tx.insert(users).values({
|
||||
name: input.name,
|
||||
email: input.email,
|
||||
mobile: input.mobile,
|
||||
}).returning()
|
||||
|
||||
// Create user credentials
|
||||
await tx.insert(userCreds).values({
|
||||
userId: user.id,
|
||||
userPassword: input.hashedPassword,
|
||||
})
|
||||
|
||||
// Create user details with profile image
|
||||
await tx.insert(userDetails).values({
|
||||
userId: user.id,
|
||||
profileImage: input.profileImage || null,
|
||||
})
|
||||
|
||||
return user
|
||||
})
|
||||
}
|
||||
|
||||
export async function getUserDetailsByUserId(userId: number) {
|
||||
const [details] = await db.select().from(userDetails).where(eq(userDetails.userId, userId)).limit(1)
|
||||
return details || null
|
||||
}
|
||||
|
||||
export async function updateUserProfile(userId: number, data: {
|
||||
name?: string
|
||||
email?: string
|
||||
mobile?: string
|
||||
hashedPassword?: string
|
||||
profileImage?: string
|
||||
bio?: string
|
||||
dateOfBirth?: Date | null
|
||||
gender?: string
|
||||
occupation?: string
|
||||
}) {
|
||||
return db.transaction(async (tx) => {
|
||||
// Update user table
|
||||
const userUpdate: any = {}
|
||||
if (data.name !== undefined) userUpdate.name = data.name
|
||||
if (data.email !== undefined) userUpdate.email = data.email
|
||||
if (data.mobile !== undefined) userUpdate.mobile = data.mobile
|
||||
|
||||
if (Object.keys(userUpdate).length > 0) {
|
||||
await tx.update(users).set(userUpdate).where(eq(users.id, userId))
|
||||
}
|
||||
|
||||
// Update password if provided
|
||||
if (data.hashedPassword) {
|
||||
await tx.update(userCreds).set({
|
||||
userPassword: data.hashedPassword,
|
||||
}).where(eq(userCreds.userId, userId))
|
||||
}
|
||||
|
||||
// Update or insert user details
|
||||
const detailsUpdate: any = {}
|
||||
if (data.bio !== undefined) detailsUpdate.bio = data.bio
|
||||
if (data.dateOfBirth !== undefined) detailsUpdate.dateOfBirth = data.dateOfBirth
|
||||
if (data.gender !== undefined) detailsUpdate.gender = data.gender
|
||||
if (data.occupation !== undefined) detailsUpdate.occupation = data.occupation
|
||||
if (data.profileImage !== undefined) detailsUpdate.profileImage = data.profileImage
|
||||
detailsUpdate.updatedAt = new Date()
|
||||
|
||||
const [existingDetails] = await tx.select().from(userDetails).where(eq(userDetails.userId, userId)).limit(1)
|
||||
|
||||
if (existingDetails) {
|
||||
await tx.update(userDetails).set(detailsUpdate).where(eq(userDetails.userId, userId))
|
||||
} else {
|
||||
await tx.insert(userDetails).values({
|
||||
userId,
|
||||
...detailsUpdate,
|
||||
createdAt: new Date(),
|
||||
})
|
||||
}
|
||||
|
||||
// Return updated user
|
||||
const [user] = await tx.select().from(users).where(eq(users.id, userId)).limit(1)
|
||||
return user
|
||||
})
|
||||
}
|
||||
|
||||
export async function createUserWithCreds(input: {
|
||||
name: string
|
||||
email: string
|
||||
|
|
|
|||
|
|
@ -30,10 +30,16 @@ export async function getUserComplaints(userId: number): Promise<UserComplaint[]
|
|||
}))
|
||||
}
|
||||
|
||||
export async function createComplaint(userId: number, orderId: number | null, complaintBody: string): Promise<void> {
|
||||
export async function createComplaint(
|
||||
userId: number,
|
||||
orderId: number | null,
|
||||
complaintBody: string,
|
||||
images?: string[] | null
|
||||
): Promise<void> {
|
||||
await db.insert(complaints).values({
|
||||
userId,
|
||||
orderId,
|
||||
complaintBody,
|
||||
images: images || null,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<OrderWithFullData[]> {
|
||||
return db.query.orders.findMany({
|
||||
where: inArray(orders.id, orderIds),
|
||||
with: {
|
||||
address: {
|
||||
columns: {
|
||||
name: true,
|
||||
addressLine1: true,
|
||||
addressLine2: true,
|
||||
city: true,
|
||||
state: true,
|
||||
pincode: true,
|
||||
phone: true,
|
||||
},
|
||||
},
|
||||
orderItems: {
|
||||
with: {
|
||||
product: {
|
||||
columns: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
slot: {
|
||||
columns: {
|
||||
deliveryTime: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}) as Promise<OrderWithFullData[]>
|
||||
}
|
||||
|
||||
export interface OrderWithCancellationData extends OrderWithFullData {
|
||||
refunds: Array<{
|
||||
refundStatus: string
|
||||
}>
|
||||
}
|
||||
|
||||
export async function getOrderByIdWithFullData(
|
||||
orderId: number
|
||||
): Promise<OrderWithCancellationData | null> {
|
||||
return db.query.orders.findFirst({
|
||||
where: eq(orders.id, orderId),
|
||||
with: {
|
||||
address: {
|
||||
columns: {
|
||||
name: true,
|
||||
addressLine1: true,
|
||||
addressLine2: true,
|
||||
city: true,
|
||||
state: true,
|
||||
pincode: true,
|
||||
phone: true,
|
||||
},
|
||||
},
|
||||
orderItems: {
|
||||
with: {
|
||||
product: {
|
||||
columns: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
slot: {
|
||||
columns: {
|
||||
deliveryTime: true,
|
||||
},
|
||||
},
|
||||
refunds: {
|
||||
columns: {
|
||||
refundStatus: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}) as Promise<OrderWithCancellationData | null>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ProductSummaryData[]> {
|
||||
let productIds: number[] | null = null
|
||||
|
||||
// If tagId is provided, get products that have this tag
|
||||
if (tagId) {
|
||||
const taggedProducts = await db
|
||||
.select({ productId: productTags.productId })
|
||||
.from(productTags)
|
||||
.where(eq(productTags.tagId, tagId))
|
||||
|
||||
productIds = taggedProducts.map(tp => tp.productId)
|
||||
}
|
||||
|
||||
let whereCondition = undefined
|
||||
|
||||
// Filter by product IDs if tag filtering is applied
|
||||
if (productIds && productIds.length > 0) {
|
||||
whereCondition = inArray(productInfo.id, productIds)
|
||||
}
|
||||
|
||||
return db
|
||||
.select({
|
||||
id: productInfo.id,
|
||||
name: productInfo.name,
|
||||
shortDescription: productInfo.shortDescription,
|
||||
price: productInfo.price,
|
||||
marketPrice: productInfo.marketPrice,
|
||||
images: productInfo.images,
|
||||
isOutOfStock: productInfo.isOutOfStock,
|
||||
unitShortNotation: units.shortNotation,
|
||||
productQuantity: productInfo.productQuantity,
|
||||
})
|
||||
.from(productInfo)
|
||||
.innerJoin(units, eq(productInfo.unitId, units.id))
|
||||
.where(whereCondition)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all suspended product IDs
|
||||
*/
|
||||
export async function getSuspendedProductIds(): Promise<number[]> {
|
||||
const suspendedProducts = await db
|
||||
.select({ id: productInfo.id })
|
||||
.from(productInfo)
|
||||
.where(eq(productInfo.isSuspended, true))
|
||||
|
||||
return suspendedProducts.map(sp => sp.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get next delivery date for a product (with capacity check)
|
||||
* This version filters by both isActive AND isCapacityFull
|
||||
*/
|
||||
export async function getNextDeliveryDateWithCapacity(productId: number): Promise<Date | null> {
|
||||
const result = await db
|
||||
.select({ deliveryTime: deliverySlotInfo.deliveryTime })
|
||||
.from(productSlots)
|
||||
.innerJoin(deliverySlotInfo, eq(productSlots.slotId, deliverySlotInfo.id))
|
||||
.where(
|
||||
and(
|
||||
eq(productSlots.productId, productId),
|
||||
eq(deliverySlotInfo.isActive, true),
|
||||
eq(deliverySlotInfo.isCapacityFull, false),
|
||||
gt(deliverySlotInfo.deliveryTime, sql`NOW()`)
|
||||
)
|
||||
)
|
||||
.orderBy(deliverySlotInfo.deliveryTime)
|
||||
.limit(1)
|
||||
|
||||
return result[0]?.deliveryTime || null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<typeof storeInfo>
|
||||
type StoreProductRow = {
|
||||
|
|
@ -125,3 +125,17 @@ export async function getStoreDetail(storeId: number): Promise<UserStoreDetailDa
|
|||
products,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get simple store summary (id, name, description only)
|
||||
* Used for common API endpoints
|
||||
*/
|
||||
export async function getStoresSummary(): Promise<StoreSummary[]> {
|
||||
return db.query.storeInfo.findMany({
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
description: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,3 +3,4 @@
|
|||
|
||||
export type * from './admin';
|
||||
export type * from './user';
|
||||
export type * from './store.types';
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue