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 { Request, Response } from "express";
|
||||||
import { db } from "@/src/db/db_index"
|
|
||||||
import { productInfo, units, productSlots, deliverySlotInfo, productTags } from "@/src/db/schema"
|
|
||||||
import { scaffoldAssetUrl } from "@/src/lib/s3-client"
|
import { scaffoldAssetUrl } from "@/src/lib/s3-client"
|
||||||
|
import { getNextDeliveryDate } from "@/src/trpc/apis/common-apis/common"
|
||||||
|
import {
|
||||||
|
getAllProductsWithUnits,
|
||||||
|
type ProductSummaryData,
|
||||||
|
} from "@/src/dbService"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get next delivery date for a product
|
* Get all products summary for dropdown
|
||||||
*/
|
*/
|
||||||
|
export const getAllProductsSummary = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { tagId } = req.query;
|
||||||
|
const tagIdNum = tagId ? parseInt(tagId as string) : undefined;
|
||||||
|
|
||||||
|
// If tagId is provided but no products found, return empty array
|
||||||
|
if (tagIdNum) {
|
||||||
|
const products = await getAllProductsWithUnits(tagIdNum);
|
||||||
|
if (products.length === 0) {
|
||||||
|
return res.status(200).json({
|
||||||
|
products: [],
|
||||||
|
count: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const productsWithUnits = await getAllProductsWithUnits(tagIdNum);
|
||||||
|
|
||||||
|
// Generate signed URLs for product images
|
||||||
|
const formattedProducts = await Promise.all(
|
||||||
|
productsWithUnits.map(async (product: ProductSummaryData) => {
|
||||||
|
const nextDeliveryDate = await getNextDeliveryDate(product.id);
|
||||||
|
return {
|
||||||
|
id: product.id,
|
||||||
|
name: product.name,
|
||||||
|
shortDescription: product.shortDescription,
|
||||||
|
price: product.price,
|
||||||
|
marketPrice: product.marketPrice,
|
||||||
|
unit: product.unitShortNotation,
|
||||||
|
productQuantity: product.productQuantity,
|
||||||
|
isOutOfStock: product.isOutOfStock,
|
||||||
|
nextDeliveryDate: nextDeliveryDate ? nextDeliveryDate.toISOString() : null,
|
||||||
|
images: scaffoldAssetUrl((product.images as string[]) || []),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
products: formattedProducts,
|
||||||
|
count: formattedProducts.length,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Get products summary error:", error);
|
||||||
|
return res.status(500).json({ error: "Failed to fetch products summary" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Old implementation - direct DB queries:
|
||||||
|
import { eq, gt, and, sql, inArray } from "drizzle-orm";
|
||||||
|
import { db } from "@/src/db/db_index"
|
||||||
|
import { productInfo, units, productSlots, deliverySlotInfo, productTags } from "@/src/db/schema"
|
||||||
|
|
||||||
const getNextDeliveryDate = async (productId: number): Promise<Date | null> => {
|
const getNextDeliveryDate = async (productId: number): Promise<Date | null> => {
|
||||||
const result = await db
|
const result = await db
|
||||||
.select({ deliveryTime: deliverySlotInfo.deliveryTime })
|
.select({ deliveryTime: deliverySlotInfo.deliveryTime })
|
||||||
|
|
@ -22,13 +77,9 @@ const getNextDeliveryDate = async (productId: number): Promise<Date | null> => {
|
||||||
.orderBy(deliverySlotInfo.deliveryTime)
|
.orderBy(deliverySlotInfo.deliveryTime)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
|
|
||||||
return result[0]?.deliveryTime || null;
|
return result[0]?.deliveryTime || null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all products summary for dropdown
|
|
||||||
*/
|
|
||||||
export const getAllProductsSummary = async (req: Request, res: Response) => {
|
export const getAllProductsSummary = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { tagId } = req.query;
|
const { tagId } = req.query;
|
||||||
|
|
@ -103,3 +154,4 @@ export const getAllProductsSummary = async (req: Request, res: Response) => {
|
||||||
return res.status(500).json({ error: "Failed to fetch products summary" });
|
return res.status(500).json({ error: "Failed to fetch products summary" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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 { db } from "@/src/db/db_index"
|
||||||
import { units, productInfo, deliverySlotInfo, productSlots, keyValStore, staffRoles, staffPermissions, staffRolePermissions } from "@/src/db/schema"
|
import { units, productInfo, deliverySlotInfo, productSlots, keyValStore, staffRoles, staffPermissions, staffRolePermissions } from "@/src/db/schema"
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { minOrderValue, deliveryCharge } from '@/src/lib/env-exporter'
|
|
||||||
import { CONST_KEYS } from '@/src/lib/const-keys'
|
|
||||||
|
|
||||||
export async function seed() {
|
export async function seed() {
|
||||||
console.log("Seeding database...");
|
console.log("Seeding database...");
|
||||||
|
|
@ -136,3 +207,4 @@ export async function seed() {
|
||||||
|
|
||||||
console.log("Seeding completed.");
|
console.log("Seeding completed.");
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
|
||||||
|
|
@ -155,6 +155,9 @@ export type {
|
||||||
UserUpdateNotesResponse,
|
UserUpdateNotesResponse,
|
||||||
UserRecentProduct,
|
UserRecentProduct,
|
||||||
UserRecentProductsResponse,
|
UserRecentProductsResponse,
|
||||||
|
// Store types
|
||||||
|
StoreSummary,
|
||||||
|
StoresSummaryResponse,
|
||||||
} from '@packages/shared';
|
} from '@packages/shared';
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,9 @@
|
||||||
import * as cron from 'node-cron';
|
import * as cron from 'node-cron';
|
||||||
import { db } from '@/src/db/db_index'
|
|
||||||
import { payments, orders, deliverySlotInfo, refunds } from '@/src/db/schema'
|
|
||||||
import { eq, and, gt, isNotNull } from 'drizzle-orm';
|
|
||||||
|
|
||||||
interface PendingPaymentRecord {
|
interface PendingPaymentRecord {
|
||||||
payment: typeof payments.$inferSelect;
|
payment: any;
|
||||||
order: typeof orders.$inferSelect;
|
order: any;
|
||||||
slot: typeof deliverySlotInfo.$inferSelect;
|
slot: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createPaymentNotification = (record: PendingPaymentRecord) => {
|
export const createPaymentNotification = (record: PendingPaymentRecord) => {
|
||||||
|
|
@ -19,34 +16,60 @@ export const createPaymentNotification = (record: PendingPaymentRecord) => {
|
||||||
|
|
||||||
export const checkRefundStatuses = async () => {
|
export const checkRefundStatuses = async () => {
|
||||||
try {
|
try {
|
||||||
// const initiatedRefunds = await db
|
// TODO: Reimplement with helpers from @/src/dbService
|
||||||
// .select()
|
// This function checks Razorpay refund status and updates database
|
||||||
// .from(refunds)
|
// Requires: getPendingRefunds(), updateRefundStatus()
|
||||||
// .where(and(
|
} catch (error) {
|
||||||
// eq(refunds.refundStatus, 'initiated'),
|
console.error('Error in checkRefundStatuses:', error);
|
||||||
// isNotNull(refunds.merchantRefundId)
|
}
|
||||||
// ));
|
};
|
||||||
//
|
|
||||||
// // Process refunds concurrently using Promise.allSettled
|
export const checkPendingPayments = async () => {
|
||||||
// const promises = initiatedRefunds.map(async (refund) => {
|
try {
|
||||||
// if (!refund.merchantRefundId) return;
|
// TODO: Reimplement with helpers from @/src/dbService
|
||||||
//
|
// This function finds pending payments and sends notifications
|
||||||
// try {
|
// Requires: getPendingPaymentsWithOrders()
|
||||||
// const razorpayRefund = await RazorpayPaymentService.fetchRefund(refund.merchantRefundId);
|
} catch (error) {
|
||||||
//
|
console.error('Error checking pending payments:', error);
|
||||||
// if (razorpayRefund.status === 'processed') {
|
}
|
||||||
// await db
|
};
|
||||||
// .update(refunds)
|
|
||||||
// .set({ refundStatus: 'success', refundProcessedAt: new Date() })
|
/*
|
||||||
// .where(eq(refunds.id, refund.id));
|
// Old implementation - direct DB queries:
|
||||||
// }
|
import { db } from '@/src/db/db_index'
|
||||||
// } catch (error) {
|
import { payments, orders, deliverySlotInfo, refunds } from '@/src/db/schema'
|
||||||
// console.error(`Error checking refund ${refund.id}:`, error);
|
import { eq, and, gt, isNotNull } from 'drizzle-orm';
|
||||||
// }
|
|
||||||
// });
|
export const checkRefundStatuses = async () => {
|
||||||
//
|
try {
|
||||||
// // Wait for all promises to complete
|
const initiatedRefunds = await db
|
||||||
// await Promise.allSettled(promises);
|
.select()
|
||||||
|
.from(refunds)
|
||||||
|
.where(and(
|
||||||
|
eq(refunds.refundStatus, 'initiated'),
|
||||||
|
isNotNull(refunds.merchantRefundId)
|
||||||
|
));
|
||||||
|
|
||||||
|
// Process refunds concurrently using Promise.allSettled
|
||||||
|
const promises = initiatedRefunds.map(async (refund) => {
|
||||||
|
if (!refund.merchantRefundId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const razorpayRefund = await RazorpayPaymentService.fetchRefund(refund.merchantRefundId);
|
||||||
|
|
||||||
|
if (razorpayRefund.status === 'processed') {
|
||||||
|
await db
|
||||||
|
.update(refunds)
|
||||||
|
.set({ refundStatus: 'success', refundProcessedAt: new Date() })
|
||||||
|
.where(eq(refunds.id, refund.id));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error checking refund ${refund.id}:`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for all promises to complete
|
||||||
|
await Promise.allSettled(promises);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in checkRefundStatuses:', error);
|
console.error('Error in checkRefundStatuses:', error);
|
||||||
}
|
}
|
||||||
|
|
@ -75,4 +98,4 @@ export const checkPendingPayments = async () => {
|
||||||
console.error('Error checking pending payments:', error);
|
console.error('Error checking pending payments:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
import * as cron from 'node-cron';
|
import * as cron from 'node-cron';
|
||||||
import { db } from '@/src/db/db_index'
|
import { toggleFlashDeliveryForItems, toggleKeyVal } from '@/src/dbService';
|
||||||
import { productInfo, keyValStore } from '@/src/db/schema'
|
|
||||||
import { inArray, eq } from 'drizzle-orm';
|
|
||||||
import { CONST_KEYS } from '@/src/lib/const-keys'
|
import { CONST_KEYS } from '@/src/lib/const-keys'
|
||||||
import { computeConstants } from '@/src/lib/const-store'
|
import { computeConstants } from '@/src/lib/const-store'
|
||||||
|
|
||||||
|
|
@ -20,6 +18,61 @@ const MUTTON_ITEMS = [
|
||||||
|
|
||||||
|
|
||||||
export const startAutomatedJobs = () => {
|
export const startAutomatedJobs = () => {
|
||||||
|
// Job to disable flash delivery for mutton at 12 PM daily
|
||||||
|
cron.schedule('0 12 * * *', async () => {
|
||||||
|
try {
|
||||||
|
console.log('Disabling flash delivery for products at 12 PM');
|
||||||
|
await toggleFlashDeliveryForItems(false, MUTTON_ITEMS);
|
||||||
|
console.log('Flash delivery disabled successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error disabling flash delivery:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Job to enable flash delivery for mutton at 6 AM daily
|
||||||
|
cron.schedule('0 6 * * *', async () => {
|
||||||
|
try {
|
||||||
|
console.log('Enabling flash delivery for products at 5 AM');
|
||||||
|
await toggleFlashDeliveryForItems(true, MUTTON_ITEMS);
|
||||||
|
console.log('Flash delivery enabled successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error enabling flash delivery:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Job to disable flash delivery feature at 9 PM daily
|
||||||
|
cron.schedule('0 21 * * *', async () => {
|
||||||
|
try {
|
||||||
|
console.log('Disabling flash delivery feature at 9 PM');
|
||||||
|
await toggleKeyVal(CONST_KEYS.isFlashDeliveryEnabled, false);
|
||||||
|
await computeConstants(); // Refresh Redis cache
|
||||||
|
console.log('Flash delivery feature disabled successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error disabling flash delivery feature:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Job to enable flash delivery feature at 6 AM daily
|
||||||
|
cron.schedule('0 6 * * *', async () => {
|
||||||
|
try {
|
||||||
|
console.log('Enabling flash delivery feature at 6 AM');
|
||||||
|
await toggleKeyVal(CONST_KEYS.isFlashDeliveryEnabled, true);
|
||||||
|
await computeConstants(); // Refresh Redis cache
|
||||||
|
console.log('Flash delivery feature enabled successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error enabling flash delivery feature:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Automated jobs scheduled');
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Old implementation - direct DB queries:
|
||||||
|
import { db } from '@/src/db/db_index'
|
||||||
|
import { productInfo, keyValStore } from '@/src/db/schema'
|
||||||
|
import { inArray, eq } from 'drizzle-orm';
|
||||||
|
|
||||||
// Job to disable flash delivery for mutton at 12 PM daily
|
// Job to disable flash delivery for mutton at 12 PM daily
|
||||||
cron.schedule('0 12 * * *', async () => {
|
cron.schedule('0 12 * * *', async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -77,9 +130,7 @@ export const startAutomatedJobs = () => {
|
||||||
console.error('Error enabling flash delivery feature:', error);
|
console.error('Error enabling flash delivery feature:', error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
console.log('Automated jobs scheduled');
|
|
||||||
};
|
|
||||||
|
|
||||||
// Optional: Call on import if desired, or export and call in main app
|
// Optional: Call on import if desired, or export and call in main app
|
||||||
// startAutomatedJobs();
|
// startAutomatedJobs();
|
||||||
|
|
@ -5,8 +5,7 @@ import { scaffoldStores } from '@/src/trpc/apis/user-apis/apis/stores'
|
||||||
import { scaffoldSlotsWithProducts } from '@/src/trpc/apis/user-apis/apis/slots'
|
import { scaffoldSlotsWithProducts } from '@/src/trpc/apis/user-apis/apis/slots'
|
||||||
import { scaffoldBanners } from '@/src/trpc/apis/user-apis/apis/banners'
|
import { scaffoldBanners } from '@/src/trpc/apis/user-apis/apis/banners'
|
||||||
import { scaffoldStoreWithProducts } from '@/src/trpc/apis/user-apis/apis/stores'
|
import { scaffoldStoreWithProducts } from '@/src/trpc/apis/user-apis/apis/stores'
|
||||||
import { storeInfo } from '@/src/db/schema'
|
import { getStoresSummary } from '@/src/dbService'
|
||||||
import { db } from '@/src/db/db_index'
|
|
||||||
import { imageUploadS3 } from '@/src/lib/s3-client'
|
import { imageUploadS3 } from '@/src/lib/s3-client'
|
||||||
import { apiCacheKey, cloudflareApiToken, cloudflareZoneId, assetsDomain } from '@/src/lib/env-exporter'
|
import { apiCacheKey, cloudflareApiToken, cloudflareZoneId, assetsDomain } from '@/src/lib/env-exporter'
|
||||||
import { CACHE_FILENAMES } from '@packages/shared'
|
import { CACHE_FILENAMES } from '@packages/shared'
|
||||||
|
|
@ -167,8 +166,16 @@ export async function createStoreFile(storeId: number): Promise<string> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createAllStoresFiles(): Promise<string[]> {
|
export async function createAllStoresFiles(): Promise<string[]> {
|
||||||
// Fetch all store IDs from database
|
/*
|
||||||
|
// Old implementation - direct DB queries:
|
||||||
|
import { db } from '@/src/db/db_index'
|
||||||
|
import { storeInfo } from '@/src/db/schema'
|
||||||
|
|
||||||
const stores = await db.select({ id: storeInfo.id }).from(storeInfo)
|
const stores = await db.select({ id: storeInfo.id }).from(storeInfo)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Fetch all store IDs from database using helper
|
||||||
|
const stores = await getStoresSummary()
|
||||||
|
|
||||||
// Create cache files for all stores and collect URLs
|
// Create cache files for all stores and collect URLs
|
||||||
const results: string[] = []
|
const results: string[] = []
|
||||||
|
|
@ -289,7 +296,15 @@ async function createBannersFileInternal(): Promise<string> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createAllStoresFilesInternal(): Promise<string[]> {
|
async function createAllStoresFilesInternal(): Promise<string[]> {
|
||||||
|
/*
|
||||||
|
// Old implementation - direct DB queries:
|
||||||
|
import { db } from '@/src/db/db_index'
|
||||||
|
import { storeInfo } from '@/src/db/schema'
|
||||||
|
|
||||||
const stores = await db.select({ id: storeInfo.id }).from(storeInfo)
|
const stores = await db.select({ id: storeInfo.id }).from(storeInfo)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const stores = await getStoresSummary()
|
||||||
const results: string[] = []
|
const results: string[] = []
|
||||||
|
|
||||||
for (const store of stores) {
|
for (const store of stores) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { db } from '@/src/db/db_index'
|
import { getAllKeyValStore } from '@/src/dbService'
|
||||||
import { keyValStore } from '@/src/db/schema'
|
|
||||||
import redisClient from '@/src/lib/redis-client'
|
import redisClient from '@/src/lib/redis-client'
|
||||||
import { CONST_KEYS, CONST_KEYS_ARRAY, type ConstKey } from '@/src/lib/const-keys'
|
import { CONST_KEYS, CONST_KEYS_ARRAY, type ConstKey } from '@/src/lib/const-keys'
|
||||||
|
|
||||||
|
|
@ -9,7 +8,15 @@ export const computeConstants = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
console.log('Computing constants from database...');
|
console.log('Computing constants from database...');
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Old implementation - direct DB queries:
|
||||||
|
import { db } from '@/src/db/db_index'
|
||||||
|
import { keyValStore } from '@/src/db/schema'
|
||||||
|
|
||||||
const constants = await db.select().from(keyValStore);
|
const constants = await db.select().from(keyValStore);
|
||||||
|
*/
|
||||||
|
|
||||||
|
const constants = await getAllKeyValStore();
|
||||||
|
|
||||||
for (const constant of constants) {
|
for (const constant of constants) {
|
||||||
const redisKey = `${CONST_REDIS_PREFIX}${constant.key}`;
|
const redisKey = `${CONST_REDIS_PREFIX}${constant.key}`;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
import { db } from '@/src/db/db_index'
|
import { deleteOrdersWithRelations } from '@/src/dbService'
|
||||||
import { orders, orderItems, orderStatus, payments, refunds, couponUsage, complaints } from '@/src/db/schema'
|
|
||||||
import { eq, inArray } from 'drizzle-orm';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete orders and all their related records
|
* Delete orders and all their related records
|
||||||
|
|
@ -8,6 +6,26 @@ import { eq, inArray } from 'drizzle-orm';
|
||||||
* @returns Promise<void>
|
* @returns Promise<void>
|
||||||
* @throws Error if deletion fails
|
* @throws Error if deletion fails
|
||||||
*/
|
*/
|
||||||
|
export const deleteOrders = async (orderIds: number[]): Promise<void> => {
|
||||||
|
if (orderIds.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteOrdersWithRelations(orderIds);
|
||||||
|
console.log(`Successfully deleted ${orderIds.length} orders and all related records`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to delete orders ${orderIds.join(', ')}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Old implementation - direct DB queries:
|
||||||
|
import { db } from '@/src/db/db_index'
|
||||||
|
import { orders, orderItems, orderStatus, payments, refunds, couponUsage, complaints } from '@/src/db/schema'
|
||||||
|
import { eq, inArray } from 'drizzle-orm';
|
||||||
|
|
||||||
export const deleteOrders = async (orderIds: number[]): Promise<void> => {
|
export const deleteOrders = async (orderIds: number[]): Promise<void> => {
|
||||||
if (orderIds.length === 0) {
|
if (orderIds.length === 0) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -43,3 +61,4 @@ export const deleteOrders = async (orderIds: number[]): Promise<void> => {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Queue, Worker } from 'bullmq';
|
import { Queue, Worker } from 'bullmq';
|
||||||
import { Expo } from 'expo-server-sdk';
|
import { Expo } from 'expo-server-sdk';
|
||||||
import { redisUrl } from '@/src/lib/env-exporter'
|
import { redisUrl } from '@/src/lib/env-exporter'
|
||||||
import { db } from '@/src/db/db_index'
|
// import { db } from '@/src/db/db_index'
|
||||||
import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client'
|
import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client'
|
||||||
import {
|
import {
|
||||||
NOTIFS_QUEUE,
|
NOTIFS_QUEUE,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { db } from '@/src/db/db_index'
|
import {
|
||||||
import { orders, orderStatus } from '@/src/db/schema'
|
getOrdersByIdsWithFullData,
|
||||||
|
getOrderByIdWithFullData,
|
||||||
|
} from '@/src/dbService'
|
||||||
import redisClient from '@/src/lib/redis-client'
|
import redisClient from '@/src/lib/redis-client'
|
||||||
import { sendTelegramMessage } from '@/src/lib/telegram-service'
|
import { sendTelegramMessage } from '@/src/lib/telegram-service'
|
||||||
import { inArray, eq } from 'drizzle-orm';
|
|
||||||
|
|
||||||
const ORDER_CHANNEL = 'orders:placed';
|
const ORDER_CHANNEL = 'orders:placed';
|
||||||
const CANCELLED_CHANNEL = 'orders:cancelled';
|
const CANCELLED_CHANNEL = 'orders:cancelled';
|
||||||
|
|
@ -98,6 +99,12 @@ export const startOrderHandler = async (): Promise<void> => {
|
||||||
const { orderIds }: OrderIdMessage = JSON.parse(message);
|
const { orderIds }: OrderIdMessage = JSON.parse(message);
|
||||||
console.log('New order received, sending to Telegram...');
|
console.log('New order received, sending to Telegram...');
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Old implementation - direct DB queries:
|
||||||
|
import { db } from '@/src/db/db_index'
|
||||||
|
import { orders } from '@/src/db/schema'
|
||||||
|
import { inArray } from 'drizzle-orm';
|
||||||
|
|
||||||
const ordersData = await db.query.orders.findMany({
|
const ordersData = await db.query.orders.findMany({
|
||||||
where: inArray(orders.id, orderIds),
|
where: inArray(orders.id, orderIds),
|
||||||
with: {
|
with: {
|
||||||
|
|
@ -106,6 +113,9 @@ export const startOrderHandler = async (): Promise<void> => {
|
||||||
slot: true,
|
slot: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
|
const ordersData = await getOrdersByIdsWithFullData(orderIds);
|
||||||
|
|
||||||
const telegramMessage = formatOrderMessageWithFullData(ordersData);
|
const telegramMessage = formatOrderMessageWithFullData(ordersData);
|
||||||
await sendTelegramMessage(telegramMessage);
|
await sendTelegramMessage(telegramMessage);
|
||||||
|
|
@ -143,6 +153,12 @@ export const startCancellationHandler = async (): Promise<void> => {
|
||||||
const cancellationData: CancellationMessage = JSON.parse(message);
|
const cancellationData: CancellationMessage = JSON.parse(message);
|
||||||
console.log('Order cancellation received, sending to Telegram...');
|
console.log('Order cancellation received, sending to Telegram...');
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Old implementation - direct DB queries:
|
||||||
|
import { db } from '@/src/db/db_index'
|
||||||
|
import { orders } from '@/src/db/schema'
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
|
||||||
const orderData = await db.query.orders.findFirst({
|
const orderData = await db.query.orders.findFirst({
|
||||||
where: eq(orders.id, cancellationData.orderId),
|
where: eq(orders.id, cancellationData.orderId),
|
||||||
with: {
|
with: {
|
||||||
|
|
@ -151,6 +167,9 @@ export const startCancellationHandler = async (): Promise<void> => {
|
||||||
refunds: true,
|
refunds: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
|
const orderData = await getOrderByIdWithFullData(cancellationData.orderId);
|
||||||
|
|
||||||
if (!orderData) {
|
if (!orderData) {
|
||||||
console.error('Order not found for cancellation:', cancellationData.orderId);
|
console.error('Order not found for cancellation:', cancellationData.orderId);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { db } from '@/src/db/db_index'
|
import { getStaffUserById, isUserSuspended } from '@/src/dbService';
|
||||||
import { staffUsers, userDetails } from '@/src/db/schema'
|
import { ApiError } from '@/src/lib/api-error';
|
||||||
import { eq } from 'drizzle-orm';
|
|
||||||
import { ApiError } from '@/src/lib/api-error'
|
|
||||||
|
|
||||||
interface AuthenticatedRequest extends Request {
|
interface AuthenticatedRequest extends Request {
|
||||||
user?: {
|
user?: {
|
||||||
|
|
@ -33,10 +31,19 @@ export const authenticateUser = async (req: AuthenticatedRequest, res: Response,
|
||||||
|
|
||||||
// Check if this is a staff token (has staffId)
|
// Check if this is a staff token (has staffId)
|
||||||
if (decoded.staffId) {
|
if (decoded.staffId) {
|
||||||
// This is a staff token, verify staff exists
|
/*
|
||||||
|
// Old implementation - direct DB queries:
|
||||||
|
import { db } from '@/src/db/db_index'
|
||||||
|
import { staffUsers } from '@/src/db/schema'
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
|
||||||
const staff = await db.query.staffUsers.findFirst({
|
const staff = await db.query.staffUsers.findFirst({
|
||||||
where: eq(staffUsers.id, decoded.staffId),
|
where: eq(staffUsers.id, decoded.staffId),
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
|
// This is a staff token, verify staff exists
|
||||||
|
const staff = await getStaffUserById(decoded.staffId);
|
||||||
|
|
||||||
if (!staff) {
|
if (!staff) {
|
||||||
throw new ApiError('Invalid staff token', 401);
|
throw new ApiError('Invalid staff token', 401);
|
||||||
|
|
@ -50,7 +57,12 @@ export const authenticateUser = async (req: AuthenticatedRequest, res: Response,
|
||||||
// This is a regular user token
|
// This is a regular user token
|
||||||
req.user = decoded;
|
req.user = decoded;
|
||||||
|
|
||||||
// Check if user is suspended
|
/*
|
||||||
|
// Old implementation - direct DB queries:
|
||||||
|
import { db } from '@/src/db/db_index'
|
||||||
|
import { userDetails } from '@/src/db/schema'
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
|
||||||
const details = await db.query.userDetails.findFirst({
|
const details = await db.query.userDetails.findFirst({
|
||||||
where: eq(userDetails.userId, decoded.userId),
|
where: eq(userDetails.userId, decoded.userId),
|
||||||
});
|
});
|
||||||
|
|
@ -58,6 +70,14 @@ export const authenticateUser = async (req: AuthenticatedRequest, res: Response,
|
||||||
if (details?.isSuspended) {
|
if (details?.isSuspended) {
|
||||||
throw new ApiError('Account suspended', 403);
|
throw new ApiError('Account suspended', 403);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Check if user is suspended
|
||||||
|
const suspended = await isUserSuspended(decoded.userId);
|
||||||
|
|
||||||
|
if (suspended) {
|
||||||
|
throw new ApiError('Account suspended', 403);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
next();
|
next();
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { db } from '@/src/db/db_index'
|
import { getStaffUserById } from '@/src/dbService';
|
||||||
import { staffUsers } from '@/src/db/schema'
|
import { ApiError } from '@/src/lib/api-error';
|
||||||
import { eq } from 'drizzle-orm';
|
|
||||||
import { ApiError } from '@/src/lib/api-error'
|
|
||||||
|
|
||||||
// Extend Request interface to include staffUser
|
// Extend Request interface to include staffUser
|
||||||
declare global {
|
declare global {
|
||||||
|
|
@ -54,10 +52,19 @@ export const authenticateStaff = async (req: Request, res: Response, next: NextF
|
||||||
throw new ApiError('Invalid staff token format', 401);
|
throw new ApiError('Invalid staff token format', 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch staff user from database
|
/*
|
||||||
|
// Old implementation - direct DB queries:
|
||||||
|
import { db } from '@/src/db/db_index'
|
||||||
|
import { staffUsers } from '@/src/db/schema'
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
|
||||||
const staff = await db.query.staffUsers.findFirst({
|
const staff = await db.query.staffUsers.findFirst({
|
||||||
where: eq(staffUsers.id, decoded.staffId),
|
where: eq(staffUsers.id, decoded.staffId),
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Fetch staff user from database
|
||||||
|
const staff = await getStaffUserById(decoded.staffId);
|
||||||
|
|
||||||
if (!staff) {
|
if (!staff) {
|
||||||
throw new ApiError('Staff user not found', 401);
|
throw new ApiError('Staff user not found', 401);
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,7 @@ export {
|
||||||
updateSlotDeliverySequence,
|
updateSlotDeliverySequence,
|
||||||
// Admin - Staff User
|
// Admin - Staff User
|
||||||
getStaffUserByName,
|
getStaffUserByName,
|
||||||
|
getStaffUserById,
|
||||||
getAllStaff,
|
getAllStaff,
|
||||||
getAllUsers,
|
getAllUsers,
|
||||||
getUserWithDetails,
|
getUserWithDetails,
|
||||||
|
|
@ -164,6 +165,8 @@ export {
|
||||||
getUserProductReviews,
|
getUserProductReviews,
|
||||||
getUserProductByIdBasic,
|
getUserProductByIdBasic,
|
||||||
createUserProductReview,
|
createUserProductReview,
|
||||||
|
getAllProductsWithUnits,
|
||||||
|
type ProductSummaryData,
|
||||||
// User - Slots
|
// User - Slots
|
||||||
getUserActiveSlotsList,
|
getUserActiveSlotsList,
|
||||||
getUserProductAvailability,
|
getUserProductAvailability,
|
||||||
|
|
@ -180,10 +183,15 @@ export {
|
||||||
getUserAuthById,
|
getUserAuthById,
|
||||||
getUserAuthCreds,
|
getUserAuthCreds,
|
||||||
getUserAuthDetails,
|
getUserAuthDetails,
|
||||||
|
isUserSuspended,
|
||||||
createUserAuthWithCreds,
|
createUserAuthWithCreds,
|
||||||
createUserAuthWithMobile,
|
createUserAuthWithMobile,
|
||||||
upsertUserAuthPassword,
|
upsertUserAuthPassword,
|
||||||
deleteUserAuthAccount,
|
deleteUserAuthAccount,
|
||||||
|
// UV API helpers
|
||||||
|
createUserWithProfile,
|
||||||
|
getUserDetailsByUserId,
|
||||||
|
updateUserProfile,
|
||||||
// User - Coupon
|
// User - Coupon
|
||||||
getUserActiveCouponsWithRelations,
|
getUserActiveCouponsWithRelations,
|
||||||
getUserAllCouponsWithRelations,
|
getUserAllCouponsWithRelations,
|
||||||
|
|
@ -218,4 +226,53 @@ export {
|
||||||
getUserRecentlyDeliveredOrderIds,
|
getUserRecentlyDeliveredOrderIds,
|
||||||
getUserProductIdsFromOrders,
|
getUserProductIdsFromOrders,
|
||||||
getUserProductsForRecentOrders,
|
getUserProductsForRecentOrders,
|
||||||
|
// Store Helpers
|
||||||
|
getAllBannersForCache,
|
||||||
|
getAllProductsForCache,
|
||||||
|
getAllStoresForCache,
|
||||||
|
getAllDeliverySlotsForCache,
|
||||||
|
getAllSpecialDealsForCache,
|
||||||
|
getAllProductTagsForCache,
|
||||||
|
getAllTagsForCache,
|
||||||
|
getAllTagProductMappings,
|
||||||
|
getAllSlotsWithProductsForCache,
|
||||||
|
getAllUserNegativityScores,
|
||||||
|
getUserNegativityScore,
|
||||||
|
type BannerData,
|
||||||
|
type ProductBasicData,
|
||||||
|
type StoreBasicData,
|
||||||
|
type DeliverySlotData,
|
||||||
|
type SpecialDealData,
|
||||||
|
type ProductTagData,
|
||||||
|
type TagBasicData,
|
||||||
|
type TagProductMapping,
|
||||||
|
type SlotWithProductsData,
|
||||||
|
type UserNegativityData,
|
||||||
|
// Automated Jobs
|
||||||
|
toggleFlashDeliveryForItems,
|
||||||
|
toggleKeyVal,
|
||||||
|
getAllKeyValStore,
|
||||||
|
// Post-order handler helpers
|
||||||
|
getOrdersByIdsWithFullData,
|
||||||
|
getOrderByIdWithFullData,
|
||||||
|
type OrderWithFullData,
|
||||||
|
type OrderWithCancellationData,
|
||||||
|
// Common API helpers
|
||||||
|
getSuspendedProductIds,
|
||||||
|
getNextDeliveryDateWithCapacity,
|
||||||
|
getStoresSummary,
|
||||||
|
healthCheck,
|
||||||
|
// Delete orders helper
|
||||||
|
deleteOrdersWithRelations,
|
||||||
|
// Seed helpers
|
||||||
|
seedUnits,
|
||||||
|
seedStaffRoles,
|
||||||
|
seedStaffPermissions,
|
||||||
|
seedRolePermissions,
|
||||||
|
seedKeyValStore,
|
||||||
|
type UnitSeedData,
|
||||||
|
type RolePermissionAssignment,
|
||||||
|
type KeyValSeedData,
|
||||||
|
type StaffRoleName,
|
||||||
|
type StaffPermissionName,
|
||||||
} from 'postgresService'
|
} from 'postgresService'
|
||||||
|
|
|
||||||
|
|
@ -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 redisClient from '@/src/lib/redis-client';
|
import {
|
||||||
import { db } from '@/src/db/db_index'
|
getAllBannersForCache,
|
||||||
import { homeBanners } from '@/src/db/schema'
|
type BannerData,
|
||||||
import { isNotNull, asc } from 'drizzle-orm';
|
} from '@/src/dbService'
|
||||||
import { scaffoldAssetUrl } from '@/src/lib/s3-client';
|
import { scaffoldAssetUrl } from '@/src/lib/s3-client'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Banner Type (matches getBanners return)
|
// Banner Type (matches getBanners return)
|
||||||
interface Banner {
|
interface Banner {
|
||||||
id: number;
|
id: number
|
||||||
name: string;
|
name: string
|
||||||
imageUrl: string | null;
|
imageUrl: string | null
|
||||||
serialNum: number | null;
|
serialNum: number | null
|
||||||
productIds: number[] | null;
|
productIds: number[] | null
|
||||||
createdAt: Date;
|
createdAt: Date
|
||||||
// updatedAt: Date;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function initializeBannerStore(): Promise<void> {
|
export async function initializeBannerStore(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log('Initializing banner store in Redis...');
|
console.log('Initializing banner store in Redis...')
|
||||||
|
|
||||||
|
const banners = await getAllBannersForCache()
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Old implementation - direct DB queries:
|
||||||
|
import { db } from '@/src/db/db_index'
|
||||||
|
import { homeBanners } from '@/src/db/schema'
|
||||||
|
import { isNotNull, asc } from 'drizzle-orm'
|
||||||
|
|
||||||
const banners = await db.query.homeBanners.findMany({
|
const banners = await db.query.homeBanners.findMany({
|
||||||
where: isNotNull(homeBanners.serialNum), // Only show assigned banners
|
where: isNotNull(homeBanners.serialNum),
|
||||||
orderBy: asc(homeBanners.serialNum), // Order by slot number 1-4
|
orderBy: asc(homeBanners.serialNum),
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
// Store each banner in Redis
|
// Store each banner in Redis
|
||||||
for (const banner of banners) {
|
for (const banner of banners) {
|
||||||
const signedImageUrl = banner.imageUrl ? scaffoldAssetUrl(banner.imageUrl) : banner.imageUrl;
|
const signedImageUrl = banner.imageUrl
|
||||||
|
? scaffoldAssetUrl(banner.imageUrl)
|
||||||
|
: banner.imageUrl
|
||||||
|
|
||||||
const bannerObj: Banner = {
|
const bannerObj: Banner = {
|
||||||
id: banner.id,
|
id: banner.id,
|
||||||
|
|
@ -40,53 +46,52 @@ export async function initializeBannerStore(): Promise<void> {
|
||||||
serialNum: banner.serialNum,
|
serialNum: banner.serialNum,
|
||||||
productIds: banner.productIds,
|
productIds: banner.productIds,
|
||||||
createdAt: banner.createdAt,
|
createdAt: banner.createdAt,
|
||||||
// updatedAt: banner.updatedAt,
|
|
||||||
};
|
|
||||||
|
|
||||||
await redisClient.set(`banner:${banner.id}`, JSON.stringify(bannerObj));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Banner store initialized successfully');
|
await redisClient.set(`banner:${banner.id}`, JSON.stringify(bannerObj))
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Banner store initialized successfully')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error initializing banner store:', error);
|
console.error('Error initializing banner store:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getBannerById(id: number): Promise<Banner | null> {
|
export async function getBannerById(id: number): Promise<Banner | null> {
|
||||||
try {
|
try {
|
||||||
const key = `banner:${id}`;
|
const key = `banner:${id}`
|
||||||
const data = await redisClient.get(key);
|
const data = await redisClient.get(key)
|
||||||
if (!data) return null;
|
if (!data) return null
|
||||||
return JSON.parse(data) as Banner;
|
return JSON.parse(data) as Banner
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error getting banner ${id}:`, error);
|
console.error(`Error getting banner ${id}:`, error)
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAllBanners(): Promise<Banner[]> {
|
export async function getAllBanners(): Promise<Banner[]> {
|
||||||
try {
|
try {
|
||||||
// Get all keys matching the pattern "banner:*"
|
// Get all keys matching the pattern "banner:*"
|
||||||
const keys = await redisClient.KEYS('banner:*');
|
const keys = await redisClient.KEYS('banner:*')
|
||||||
|
|
||||||
if (keys.length === 0) return [];
|
if (keys.length === 0) return []
|
||||||
|
|
||||||
// Get all banners using MGET for better performance
|
// Get all banners using MGET for better performance
|
||||||
const bannersData = await redisClient.MGET(keys);
|
const bannersData = await redisClient.MGET(keys)
|
||||||
|
|
||||||
const banners: Banner[] = [];
|
const banners: Banner[] = []
|
||||||
for (const bannerData of bannersData) {
|
for (const bannerData of bannersData) {
|
||||||
if (bannerData) {
|
if (bannerData) {
|
||||||
banners.push(JSON.parse(bannerData) as Banner);
|
banners.push(JSON.parse(bannerData) as Banner)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by serialNum to maintain the same order as the original query
|
// Sort by serialNum to maintain the same order as the original query
|
||||||
banners.sort((a, b) => (a.serialNum || 0) - (b.serialNum || 0));
|
banners.sort((a, b) => (a.serialNum || 0) - (b.serialNum || 0))
|
||||||
|
|
||||||
return banners;
|
return banners
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting all banners:', error);
|
console.error('Error getting all banners:', error)
|
||||||
return [];
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,51 @@
|
||||||
// import redisClient from '@/src/stores/redis-client';
|
import redisClient from '@/src/lib/redis-client'
|
||||||
import redisClient from '@/src/lib/redis-client';
|
import {
|
||||||
import { db } from '@/src/db/db_index'
|
getAllProductsForCache,
|
||||||
import { productInfo, units, productSlots, deliverySlotInfo, specialDeals, storeInfo, productTags, productTagInfo } from '@/src/db/schema'
|
getAllStoresForCache,
|
||||||
import { eq, and, gt, sql } from 'drizzle-orm';
|
getAllDeliverySlotsForCache,
|
||||||
import { generateSignedUrlsFromS3Urls, scaffoldAssetUrl } from '@/src/lib/s3-client';
|
getAllSpecialDealsForCache,
|
||||||
|
getAllProductTagsForCache,
|
||||||
|
type ProductBasicData,
|
||||||
|
type StoreBasicData,
|
||||||
|
type DeliverySlotData,
|
||||||
|
type SpecialDealData,
|
||||||
|
type ProductTagData,
|
||||||
|
} from '@/src/dbService'
|
||||||
|
import { scaffoldAssetUrl } from '@/src/lib/s3-client'
|
||||||
|
|
||||||
// Uniform Product Type (matches getProductDetails return)
|
// Uniform Product Type (matches getProductDetails return)
|
||||||
interface Product {
|
interface Product {
|
||||||
id: number;
|
id: number
|
||||||
name: string;
|
name: string
|
||||||
shortDescription: string | null;
|
shortDescription: string | null
|
||||||
longDescription: string | null;
|
longDescription: string | null
|
||||||
price: string;
|
price: string
|
||||||
marketPrice: string | null;
|
marketPrice: string | null
|
||||||
unitNotation: string;
|
unitNotation: string
|
||||||
images: string[];
|
images: string[]
|
||||||
isOutOfStock: boolean;
|
isOutOfStock: boolean
|
||||||
store: { id: number; name: string; description: string | null } | null;
|
store: { id: number; name: string; description: string | null } | null
|
||||||
incrementStep: number;
|
incrementStep: number
|
||||||
productQuantity: number;
|
productQuantity: number
|
||||||
isFlashAvailable: boolean;
|
isFlashAvailable: boolean
|
||||||
flashPrice: string | null;
|
flashPrice: string | null
|
||||||
deliverySlots: Array<{ id: number; deliveryTime: Date; freezeTime: Date; isCapacityFull: boolean }>;
|
deliverySlots: Array<{ id: number; deliveryTime: Date; freezeTime: Date; isCapacityFull: boolean }>
|
||||||
specialDeals: Array<{ quantity: string; price: string; validTill: Date }>;
|
specialDeals: Array<{ quantity: string; price: string; validTill: Date }>
|
||||||
productTags: string[];
|
productTags: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function initializeProducts(): Promise<void> {
|
export async function initializeProducts(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log('Initializing product store in Redis...');
|
console.log('Initializing product store in Redis...')
|
||||||
|
|
||||||
// Fetch all products with full details (similar to productMega logic)
|
// Fetch all products with full details (similar to productMega logic)
|
||||||
|
const productsData = await getAllProductsForCache()
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Old implementation - direct DB queries:
|
||||||
|
import { db } from '@/src/db/db_index'
|
||||||
|
import { productInfo, units } from '@/src/db/schema'
|
||||||
|
|
||||||
const productsData = await db
|
const productsData = await db
|
||||||
.select({
|
.select({
|
||||||
id: productInfo.id,
|
id: productInfo.id,
|
||||||
|
|
@ -50,74 +65,50 @@ export async function initializeProducts(): Promise<void> {
|
||||||
})
|
})
|
||||||
.from(productInfo)
|
.from(productInfo)
|
||||||
.innerJoin(units, eq(productInfo.unitId, units.id));
|
.innerJoin(units, eq(productInfo.unitId, units.id));
|
||||||
|
*/
|
||||||
|
|
||||||
// Fetch all stores
|
// Fetch all stores
|
||||||
const allStores = await db.query.storeInfo.findMany({
|
const allStores = await getAllStoresForCache()
|
||||||
columns: { id: true, name: true, description: true },
|
const storeMap = new Map(allStores.map((s) => [s.id, s]))
|
||||||
});
|
|
||||||
const storeMap = new Map(allStores.map(s => [s.id, s]));
|
|
||||||
|
|
||||||
// Fetch all delivery slots (excluding full capacity slots)
|
// Fetch all delivery slots (excluding full capacity slots)
|
||||||
const allDeliverySlots = await db
|
const allDeliverySlots = await getAllDeliverySlotsForCache()
|
||||||
.select({
|
const deliverySlotsMap = new Map<number, DeliverySlotData[]>()
|
||||||
productId: productSlots.productId,
|
|
||||||
id: deliverySlotInfo.id,
|
|
||||||
deliveryTime: deliverySlotInfo.deliveryTime,
|
|
||||||
freezeTime: deliverySlotInfo.freezeTime,
|
|
||||||
isCapacityFull: deliverySlotInfo.isCapacityFull,
|
|
||||||
})
|
|
||||||
.from(productSlots)
|
|
||||||
.innerJoin(deliverySlotInfo, eq(productSlots.slotId, deliverySlotInfo.id))
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(deliverySlotInfo.isActive, true),
|
|
||||||
eq(deliverySlotInfo.isCapacityFull, false),
|
|
||||||
gt(deliverySlotInfo.deliveryTime, sql`NOW()`)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const deliverySlotsMap = new Map<number, typeof allDeliverySlots>();
|
|
||||||
for (const slot of allDeliverySlots) {
|
for (const slot of allDeliverySlots) {
|
||||||
if (!deliverySlotsMap.has(slot.productId)) deliverySlotsMap.set(slot.productId, []);
|
if (!deliverySlotsMap.has(slot.productId))
|
||||||
deliverySlotsMap.get(slot.productId)!.push(slot);
|
deliverySlotsMap.set(slot.productId, [])
|
||||||
|
deliverySlotsMap.get(slot.productId)!.push(slot)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch all special deals
|
// Fetch all special deals
|
||||||
const allSpecialDeals = await db
|
const allSpecialDeals = await getAllSpecialDealsForCache()
|
||||||
.select({
|
const specialDealsMap = new Map<number, SpecialDealData[]>()
|
||||||
productId: specialDeals.productId,
|
|
||||||
quantity: specialDeals.quantity,
|
|
||||||
price: specialDeals.price,
|
|
||||||
validTill: specialDeals.validTill,
|
|
||||||
})
|
|
||||||
.from(specialDeals)
|
|
||||||
.where(gt(specialDeals.validTill, sql`NOW()`));
|
|
||||||
const specialDealsMap = new Map<number, typeof allSpecialDeals>();
|
|
||||||
for (const deal of allSpecialDeals) {
|
for (const deal of allSpecialDeals) {
|
||||||
if (!specialDealsMap.has(deal.productId)) specialDealsMap.set(deal.productId, []);
|
if (!specialDealsMap.has(deal.productId))
|
||||||
specialDealsMap.get(deal.productId)!.push(deal);
|
specialDealsMap.set(deal.productId, [])
|
||||||
|
specialDealsMap.get(deal.productId)!.push(deal)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch all product tags
|
// Fetch all product tags
|
||||||
const allProductTags = await db
|
const allProductTags = await getAllProductTagsForCache()
|
||||||
.select({
|
const productTagsMap = new Map<number, string[]>()
|
||||||
productId: productTags.productId,
|
|
||||||
tagName: productTagInfo.tagName,
|
|
||||||
})
|
|
||||||
.from(productTags)
|
|
||||||
.innerJoin(productTagInfo, eq(productTags.tagId, productTagInfo.id));
|
|
||||||
const productTagsMap = new Map<number, string[]>();
|
|
||||||
for (const tag of allProductTags) {
|
for (const tag of allProductTags) {
|
||||||
if (!productTagsMap.has(tag.productId)) productTagsMap.set(tag.productId, []);
|
if (!productTagsMap.has(tag.productId))
|
||||||
productTagsMap.get(tag.productId)!.push(tag.tagName);
|
productTagsMap.set(tag.productId, [])
|
||||||
|
productTagsMap.get(tag.productId)!.push(tag.tagName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store each product in Redis
|
// Store each product in Redis
|
||||||
for (const product of productsData) {
|
for (const product of productsData) {
|
||||||
const signedImages = scaffoldAssetUrl((product.images as string[]) || []);
|
const signedImages = scaffoldAssetUrl(
|
||||||
const store = product.storeId ? storeMap.get(product.storeId) || null : null;
|
(product.images as string[]) || []
|
||||||
const deliverySlots = deliverySlotsMap.get(product.id) || [];
|
)
|
||||||
const specialDeals = specialDealsMap.get(product.id) || [];
|
const store = product.storeId
|
||||||
const productTags = productTagsMap.get(product.id) || [];
|
? storeMap.get(product.storeId) || null
|
||||||
|
: null
|
||||||
|
const deliverySlots = deliverySlotsMap.get(product.id) || []
|
||||||
|
const specialDeals = specialDealsMap.get(product.id) || []
|
||||||
|
const productTags = productTagsMap.get(product.id) || []
|
||||||
|
|
||||||
const productObj: Product = {
|
const productObj: Product = {
|
||||||
id: product.id,
|
id: product.id,
|
||||||
|
|
@ -129,60 +120,70 @@ export async function initializeProducts(): Promise<void> {
|
||||||
unitNotation: product.unitShortNotation,
|
unitNotation: product.unitShortNotation,
|
||||||
images: signedImages,
|
images: signedImages,
|
||||||
isOutOfStock: product.isOutOfStock,
|
isOutOfStock: product.isOutOfStock,
|
||||||
store: store ? { id: store.id, name: store.name, description: store.description } : null,
|
store: store
|
||||||
|
? { id: store.id, name: store.name, description: store.description }
|
||||||
|
: null,
|
||||||
incrementStep: product.incrementStep,
|
incrementStep: product.incrementStep,
|
||||||
productQuantity: product.productQuantity,
|
productQuantity: product.productQuantity,
|
||||||
isFlashAvailable: product.isFlashAvailable,
|
isFlashAvailable: product.isFlashAvailable,
|
||||||
flashPrice: product.flashPrice?.toString() || null,
|
flashPrice: product.flashPrice?.toString() || null,
|
||||||
deliverySlots: deliverySlots.map(s => ({ id: s.id, deliveryTime: s.deliveryTime, freezeTime: s.freezeTime, isCapacityFull: s.isCapacityFull })),
|
deliverySlots: deliverySlots.map((s) => ({
|
||||||
specialDeals: specialDeals.map(d => ({ quantity: d.quantity.toString(), price: d.price.toString(), validTill: d.validTill })),
|
id: s.id,
|
||||||
|
deliveryTime: s.deliveryTime,
|
||||||
|
freezeTime: s.freezeTime,
|
||||||
|
isCapacityFull: s.isCapacityFull,
|
||||||
|
})),
|
||||||
|
specialDeals: specialDeals.map((d) => ({
|
||||||
|
quantity: d.quantity.toString(),
|
||||||
|
price: d.price.toString(),
|
||||||
|
validTill: d.validTill,
|
||||||
|
})),
|
||||||
productTags: productTags,
|
productTags: productTags,
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
await redisClient.set(`product:${product.id}`, JSON.stringify(productObj));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Product store initialized successfully');
|
await redisClient.set(`product:${product.id}`, JSON.stringify(productObj))
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Product store initialized successfully')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error initializing product store:', error);
|
console.error('Error initializing product store:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getProductById(id: number): Promise<Product | null> {
|
export async function getProductById(id: number): Promise<Product | null> {
|
||||||
try {
|
try {
|
||||||
const key = `product:${id}`;
|
const key = `product:${id}`
|
||||||
const data = await redisClient.get(key);
|
const data = await redisClient.get(key)
|
||||||
if (!data) return null;
|
if (!data) return null
|
||||||
return JSON.parse(data) as Product;
|
return JSON.parse(data) as Product
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error getting product ${id}:`, error);
|
console.error(`Error getting product ${id}:`, error)
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAllProducts(): Promise<Product[]> {
|
export async function getAllProducts(): Promise<Product[]> {
|
||||||
try {
|
try {
|
||||||
// Get all keys matching the pattern "product:*"
|
// Get all keys matching the pattern "product:*"
|
||||||
const keys = await redisClient.KEYS('product:*');
|
const keys = await redisClient.KEYS('product:*')
|
||||||
|
|
||||||
if (keys.length === 0) {
|
if (keys.length === 0) {
|
||||||
return [];
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all products using MGET for better performance
|
// Get all products using MGET for better performance
|
||||||
const productsData = await redisClient.MGET(keys);
|
const productsData = await redisClient.MGET(keys)
|
||||||
|
|
||||||
const products: Product[] = [];
|
const products: Product[] = []
|
||||||
for (const productData of productsData) {
|
for (const productData of productsData) {
|
||||||
if (productData) {
|
if (productData) {
|
||||||
products.push(JSON.parse(productData) as Product);
|
products.push(JSON.parse(productData) as Product)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return products;
|
return products
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting all products:', error);
|
console.error('Error getting all products:', error)
|
||||||
return [];
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,35 @@
|
||||||
// import redisClient from '@/src/stores/redis-client';
|
import redisClient from '@/src/lib/redis-client'
|
||||||
import redisClient from '@/src/lib/redis-client';
|
import {
|
||||||
import { db } from '@/src/db/db_index'
|
getAllTagsForCache,
|
||||||
import { productTagInfo, productTags } from '@/src/db/schema'
|
getAllTagProductMappings,
|
||||||
import { eq, inArray } from 'drizzle-orm';
|
type TagBasicData,
|
||||||
import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client';
|
type TagProductMapping,
|
||||||
|
} from '@/src/dbService'
|
||||||
|
import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client'
|
||||||
|
|
||||||
// Tag Type (matches getDashboardTags return)
|
// Tag Type (matches getDashboardTags return)
|
||||||
interface Tag {
|
interface Tag {
|
||||||
id: number;
|
id: number
|
||||||
tagName: string;
|
tagName: string
|
||||||
tagDescription: string | null;
|
tagDescription: string | null
|
||||||
imageUrl: string | null;
|
imageUrl: string | null
|
||||||
isDashboardTag: boolean;
|
isDashboardTag: boolean
|
||||||
relatedStores: number[];
|
relatedStores: number[]
|
||||||
productIds: number[];
|
productIds: number[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function initializeProductTagStore(): Promise<void> {
|
export async function initializeProductTagStore(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log('Initializing product tag store in Redis...');
|
console.log('Initializing product tag store in Redis...')
|
||||||
|
|
||||||
// Fetch all tags
|
// Fetch all tags
|
||||||
|
const tagsData = await getAllTagsForCache()
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Old implementation - direct DB queries:
|
||||||
|
import { db } from '@/src/db/db_index'
|
||||||
|
import { productTagInfo } from '@/src/db/schema'
|
||||||
|
|
||||||
const tagsData = await db
|
const tagsData = await db
|
||||||
.select({
|
.select({
|
||||||
id: productTagInfo.id,
|
id: productTagInfo.id,
|
||||||
|
|
@ -31,8 +40,16 @@ export async function initializeProductTagStore(): Promise<void> {
|
||||||
relatedStores: productTagInfo.relatedStores,
|
relatedStores: productTagInfo.relatedStores,
|
||||||
})
|
})
|
||||||
.from(productTagInfo);
|
.from(productTagInfo);
|
||||||
|
*/
|
||||||
|
|
||||||
// Fetch product IDs for each tag
|
// Fetch product IDs for each tag
|
||||||
|
const productTagsData = await getAllTagProductMappings()
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Old implementation - direct DB queries:
|
||||||
|
import { productTags } from '@/src/db/schema'
|
||||||
|
import { inArray } from 'drizzle-orm'
|
||||||
|
|
||||||
const tagIds = tagsData.map(t => t.id);
|
const tagIds = tagsData.map(t => t.id);
|
||||||
const productTagsData = await db
|
const productTagsData = await db
|
||||||
.select({
|
.select({
|
||||||
|
|
@ -41,19 +58,22 @@ export async function initializeProductTagStore(): Promise<void> {
|
||||||
})
|
})
|
||||||
.from(productTags)
|
.from(productTags)
|
||||||
.where(inArray(productTags.tagId, tagIds));
|
.where(inArray(productTags.tagId, tagIds));
|
||||||
|
*/
|
||||||
|
|
||||||
// Group product IDs by tag
|
// Group product IDs by tag
|
||||||
const productIdsByTag = new Map<number, number[]>();
|
const productIdsByTag = new Map<number, number[]>()
|
||||||
for (const pt of productTagsData) {
|
for (const pt of productTagsData) {
|
||||||
if (!productIdsByTag.has(pt.tagId)) {
|
if (!productIdsByTag.has(pt.tagId)) {
|
||||||
productIdsByTag.set(pt.tagId, []);
|
productIdsByTag.set(pt.tagId, [])
|
||||||
}
|
}
|
||||||
productIdsByTag.get(pt.tagId)!.push(pt.productId);
|
productIdsByTag.get(pt.tagId)!.push(pt.productId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store each tag in Redis
|
// Store each tag in Redis
|
||||||
for (const tag of tagsData) {
|
for (const tag of tagsData) {
|
||||||
const signedImageUrl = tag.imageUrl ? await generateSignedUrlFromS3Url(tag.imageUrl) : null;
|
const signedImageUrl = tag.imageUrl
|
||||||
|
? await generateSignedUrlFromS3Url(tag.imageUrl)
|
||||||
|
: null
|
||||||
|
|
||||||
const tagObj: Tag = {
|
const tagObj: Tag = {
|
||||||
id: tag.id,
|
id: tag.id,
|
||||||
|
|
@ -63,109 +83,109 @@ export async function initializeProductTagStore(): Promise<void> {
|
||||||
isDashboardTag: tag.isDashboardTag,
|
isDashboardTag: tag.isDashboardTag,
|
||||||
relatedStores: (tag.relatedStores as number[]) || [],
|
relatedStores: (tag.relatedStores as number[]) || [],
|
||||||
productIds: productIdsByTag.get(tag.id) || [],
|
productIds: productIdsByTag.get(tag.id) || [],
|
||||||
};
|
|
||||||
|
|
||||||
await redisClient.set(`tag:${tag.id}`, JSON.stringify(tagObj));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Product tag store initialized successfully');
|
await redisClient.set(`tag:${tag.id}`, JSON.stringify(tagObj))
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Product tag store initialized successfully')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error initializing product tag store:', error);
|
console.error('Error initializing product tag store:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTagById(id: number): Promise<Tag | null> {
|
export async function getTagById(id: number): Promise<Tag | null> {
|
||||||
try {
|
try {
|
||||||
const key = `tag:${id}`;
|
const key = `tag:${id}`
|
||||||
const data = await redisClient.get(key);
|
const data = await redisClient.get(key)
|
||||||
if (!data) return null;
|
if (!data) return null
|
||||||
return JSON.parse(data) as Tag;
|
return JSON.parse(data) as Tag
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error getting tag ${id}:`, error);
|
console.error(`Error getting tag ${id}:`, error)
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAllTags(): Promise<Tag[]> {
|
export async function getAllTags(): Promise<Tag[]> {
|
||||||
try {
|
try {
|
||||||
// Get all keys matching the pattern "tag:*"
|
// Get all keys matching the pattern "tag:*"
|
||||||
const keys = await redisClient.KEYS('tag:*');
|
const keys = await redisClient.KEYS('tag:*')
|
||||||
|
|
||||||
if (keys.length === 0) {
|
if (keys.length === 0) {
|
||||||
return [];
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all tags using MGET for better performance
|
// Get all tags using MGET for better performance
|
||||||
const tagsData = await redisClient.MGET(keys);
|
const tagsData = await redisClient.MGET(keys)
|
||||||
|
|
||||||
const tags: Tag[] = [];
|
const tags: Tag[] = []
|
||||||
for (const tagData of tagsData) {
|
for (const tagData of tagsData) {
|
||||||
if (tagData) {
|
if (tagData) {
|
||||||
tags.push(JSON.parse(tagData) as Tag);
|
tags.push(JSON.parse(tagData) as Tag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return tags;
|
return tags
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting all tags:', error);
|
console.error('Error getting all tags:', error)
|
||||||
return [];
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getDashboardTags(): Promise<Tag[]> {
|
export async function getDashboardTags(): Promise<Tag[]> {
|
||||||
try {
|
try {
|
||||||
// Get all keys matching the pattern "tag:*"
|
// Get all keys matching the pattern "tag:*"
|
||||||
const keys = await redisClient.KEYS('tag:*');
|
const keys = await redisClient.KEYS('tag:*')
|
||||||
|
|
||||||
if (keys.length === 0) {
|
if (keys.length === 0) {
|
||||||
return [];
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all tags using MGET for better performance
|
// Get all tags using MGET for better performance
|
||||||
const tagsData = await redisClient.MGET(keys);
|
const tagsData = await redisClient.MGET(keys)
|
||||||
|
|
||||||
const dashboardTags: Tag[] = [];
|
const dashboardTags: Tag[] = []
|
||||||
for (const tagData of tagsData) {
|
for (const tagData of tagsData) {
|
||||||
if (tagData) {
|
if (tagData) {
|
||||||
const tag = JSON.parse(tagData) as Tag;
|
const tag = JSON.parse(tagData) as Tag
|
||||||
if (tag.isDashboardTag) {
|
if (tag.isDashboardTag) {
|
||||||
dashboardTags.push(tag);
|
dashboardTags.push(tag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return dashboardTags;
|
return dashboardTags
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting dashboard tags:', error);
|
console.error('Error getting dashboard tags:', error)
|
||||||
return [];
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTagsByStoreId(storeId: number): Promise<Tag[]> {
|
export async function getTagsByStoreId(storeId: number): Promise<Tag[]> {
|
||||||
try {
|
try {
|
||||||
// Get all keys matching the pattern "tag:*"
|
// Get all keys matching the pattern "tag:*"
|
||||||
const keys = await redisClient.KEYS('tag:*');
|
const keys = await redisClient.KEYS('tag:*')
|
||||||
|
|
||||||
if (keys.length === 0) {
|
if (keys.length === 0) {
|
||||||
return [];
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all tags using MGET for better performance
|
// Get all tags using MGET for better performance
|
||||||
const tagsData = await redisClient.MGET(keys);
|
const tagsData = await redisClient.MGET(keys)
|
||||||
|
|
||||||
const storeTags: Tag[] = [];
|
const storeTags: Tag[] = []
|
||||||
for (const tagData of tagsData) {
|
for (const tagData of tagsData) {
|
||||||
if (tagData) {
|
if (tagData) {
|
||||||
const tag = JSON.parse(tagData) as Tag;
|
const tag = JSON.parse(tagData) as Tag
|
||||||
if (tag.relatedStores.includes(storeId)) {
|
if (tag.relatedStores.includes(storeId)) {
|
||||||
storeTags.push(tag);
|
storeTags.push(tag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return storeTags;
|
return storeTags
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error getting tags for store ${storeId}:`, error);
|
console.error(`Error getting tags for store ${storeId}:`, error)
|
||||||
return [];
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,50 +1,58 @@
|
||||||
import redisClient from '@/src/lib/redis-client';
|
import redisClient from '@/src/lib/redis-client'
|
||||||
import { db } from '@/src/db/db_index'
|
import {
|
||||||
import { deliverySlotInfo, productSlots, productInfo, units } from '@/src/db/schema'
|
getAllSlotsWithProductsForCache,
|
||||||
import { eq, and, gt, asc } from 'drizzle-orm';
|
type SlotWithProductsData,
|
||||||
import { generateSignedUrlsFromS3Urls, scaffoldAssetUrl } from '@/src/lib/s3-client';
|
} from '@/src/dbService'
|
||||||
import dayjs from 'dayjs';
|
import { scaffoldAssetUrl } from '@/src/lib/s3-client'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
// Define the structure for slot with products
|
// Define the structure for slot with products
|
||||||
interface SlotWithProducts {
|
interface SlotWithProducts {
|
||||||
id: number;
|
id: number
|
||||||
deliveryTime: Date;
|
deliveryTime: Date
|
||||||
freezeTime: Date;
|
freezeTime: Date
|
||||||
isActive: boolean;
|
isActive: boolean
|
||||||
isCapacityFull: boolean;
|
isCapacityFull: boolean
|
||||||
products: Array<{
|
products: Array<{
|
||||||
id: number;
|
id: number
|
||||||
name: string;
|
name: string
|
||||||
shortDescription: string | null;
|
shortDescription: string | null
|
||||||
productQuantity: number;
|
productQuantity: number
|
||||||
price: string;
|
price: string
|
||||||
marketPrice: string | null;
|
marketPrice: string | null
|
||||||
unit: string | null;
|
unit: string | null
|
||||||
images: string[];
|
images: string[]
|
||||||
isOutOfStock: boolean;
|
isOutOfStock: boolean
|
||||||
storeId: number | null;
|
storeId: number | null
|
||||||
nextDeliveryDate: Date;
|
nextDeliveryDate: Date
|
||||||
}>;
|
}>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SlotInfo {
|
interface SlotInfo {
|
||||||
id: number;
|
id: number
|
||||||
deliveryTime: Date;
|
deliveryTime: Date
|
||||||
freezeTime: Date;
|
freezeTime: Date
|
||||||
isCapacityFull: boolean;
|
isCapacityFull: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function initializeSlotStore(): Promise<void> {
|
export async function initializeSlotStore(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log('Initializing slot store in Redis...');
|
console.log('Initializing slot store in Redis...')
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
|
|
||||||
// Fetch active delivery slots with future delivery times
|
// Fetch active delivery slots with future delivery times
|
||||||
|
const slots = await getAllSlotsWithProductsForCache()
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Old implementation - direct DB queries:
|
||||||
|
import { db } from '@/src/db/db_index'
|
||||||
|
import { deliverySlotInfo } from '@/src/db/schema'
|
||||||
|
import { eq, gt, and, asc } from 'drizzle-orm'
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
const slots = await db.query.deliverySlotInfo.findMany({
|
const slots = await db.query.deliverySlotInfo.findMany({
|
||||||
where: and(
|
where: and(
|
||||||
eq(deliverySlotInfo.isActive, true),
|
eq(deliverySlotInfo.isActive, true),
|
||||||
gt(deliverySlotInfo.deliveryTime, now), // Only future slots
|
gt(deliverySlotInfo.deliveryTime, now),
|
||||||
),
|
),
|
||||||
with: {
|
with: {
|
||||||
productSlots: {
|
productSlots: {
|
||||||
|
|
@ -60,6 +68,7 @@ export async function initializeSlotStore(): Promise<void> {
|
||||||
},
|
},
|
||||||
orderBy: asc(deliverySlotInfo.deliveryTime),
|
orderBy: asc(deliverySlotInfo.deliveryTime),
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
// Transform data for storage
|
// Transform data for storage
|
||||||
const slotsWithProducts = await Promise.all(
|
const slotsWithProducts = await Promise.all(
|
||||||
|
|
@ -79,151 +88,156 @@ export async function initializeSlotStore(): Promise<void> {
|
||||||
marketPrice: productSlot.product.marketPrice?.toString() || null,
|
marketPrice: productSlot.product.marketPrice?.toString() || null,
|
||||||
unit: productSlot.product.unit?.shortNotation || null,
|
unit: productSlot.product.unit?.shortNotation || null,
|
||||||
images: scaffoldAssetUrl(
|
images: scaffoldAssetUrl(
|
||||||
(productSlot.product.images as string[]) || [],
|
(productSlot.product.images as string[]) || []
|
||||||
),
|
),
|
||||||
isOutOfStock: productSlot.product.isOutOfStock,
|
isOutOfStock: productSlot.product.isOutOfStock,
|
||||||
storeId: productSlot.product.storeId,
|
storeId: productSlot.product.storeId,
|
||||||
nextDeliveryDate: slot.deliveryTime,
|
nextDeliveryDate: slot.deliveryTime,
|
||||||
})),
|
}))
|
||||||
),
|
),
|
||||||
})),
|
}))
|
||||||
);
|
)
|
||||||
|
|
||||||
// Store each slot in Redis with key pattern "slot:{id}"
|
// Store each slot in Redis with key pattern "slot:{id}"
|
||||||
for (const slot of slotsWithProducts) {
|
for (const slot of slotsWithProducts) {
|
||||||
await redisClient.set(`slot:${slot.id}`, JSON.stringify(slot));
|
await redisClient.set(`slot:${slot.id}`, JSON.stringify(slot))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build and store product-slots map
|
// Build and store product-slots map
|
||||||
// Group slots by productId
|
// Group slots by productId
|
||||||
const productSlotsMap: Record<number, SlotInfo[]> = {};
|
const productSlotsMap: Record<number, SlotInfo[]> = {}
|
||||||
|
|
||||||
for (const slot of slotsWithProducts) {
|
for (const slot of slotsWithProducts) {
|
||||||
for (const product of slot.products) {
|
for (const product of slot.products) {
|
||||||
if (!productSlotsMap[product.id]) {
|
if (!productSlotsMap[product.id]) {
|
||||||
productSlotsMap[product.id] = [];
|
productSlotsMap[product.id] = []
|
||||||
}
|
}
|
||||||
productSlotsMap[product.id].push({
|
productSlotsMap[product.id].push({
|
||||||
id: slot.id,
|
id: slot.id,
|
||||||
deliveryTime: slot.deliveryTime,
|
deliveryTime: slot.deliveryTime,
|
||||||
freezeTime: slot.freezeTime,
|
freezeTime: slot.freezeTime,
|
||||||
isCapacityFull: slot.isCapacityFull,
|
isCapacityFull: slot.isCapacityFull,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store each product's slots in Redis with key pattern "product:{id}:slots"
|
// Store each product's slots in Redis with key pattern "product:{id}:slots"
|
||||||
for (const [productId, slotInfos] of Object.entries(productSlotsMap)) {
|
for (const [productId, slotInfos] of Object.entries(productSlotsMap)) {
|
||||||
await redisClient.set(`product:${productId}:slots`, JSON.stringify(slotInfos));
|
await redisClient.set(
|
||||||
|
`product:${productId}:slots`,
|
||||||
|
JSON.stringify(slotInfos)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Slot store initialized successfully');
|
console.log('Slot store initialized successfully')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error initializing slot store:', error);
|
console.error('Error initializing slot store:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSlotById(slotId: number): Promise<SlotWithProducts | null> {
|
export async function getSlotById(slotId: number): Promise<SlotWithProducts | null> {
|
||||||
try {
|
try {
|
||||||
const key = `slot:${slotId}`;
|
const key = `slot:${slotId}`
|
||||||
const data = await redisClient.get(key);
|
const data = await redisClient.get(key)
|
||||||
if (!data) return null;
|
if (!data) return null
|
||||||
return JSON.parse(data) as SlotWithProducts;
|
return JSON.parse(data) as SlotWithProducts
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error getting slot ${slotId}:`, error);
|
console.error(`Error getting slot ${slotId}:`, error)
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAllSlots(): Promise<SlotWithProducts[]> {
|
export async function getAllSlots(): Promise<SlotWithProducts[]> {
|
||||||
try {
|
try {
|
||||||
// Get all keys matching the pattern "slot:*"
|
// Get all keys matching the pattern "slot:*"
|
||||||
const keys = await redisClient.KEYS('slot:*');
|
const keys = await redisClient.KEYS('slot:*')
|
||||||
|
|
||||||
if (keys.length === 0) return [];
|
if (keys.length === 0) return []
|
||||||
|
|
||||||
// Get all slots using MGET for better performance
|
// Get all slots using MGET for better performance
|
||||||
const slotsData = await redisClient.MGET(keys);
|
const slotsData = await redisClient.MGET(keys)
|
||||||
|
|
||||||
const slots: SlotWithProducts[] = [];
|
const slots: SlotWithProducts[] = []
|
||||||
for (const slotData of slotsData) {
|
for (const slotData of slotsData) {
|
||||||
if (slotData) {
|
if (slotData) {
|
||||||
slots.push(JSON.parse(slotData) as SlotWithProducts);
|
slots.push(JSON.parse(slotData) as SlotWithProducts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return slots;
|
return slots
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting all slots:', error);
|
console.error('Error getting all slots:', error)
|
||||||
return [];
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getProductSlots(productId: number): Promise<SlotInfo[]> {
|
export async function getProductSlots(productId: number): Promise<SlotInfo[]> {
|
||||||
try {
|
try {
|
||||||
const key = `product:${productId}:slots`;
|
const key = `product:${productId}:slots`
|
||||||
const data = await redisClient.get(key);
|
const data = await redisClient.get(key)
|
||||||
if (!data) return [];
|
if (!data) return []
|
||||||
return JSON.parse(data) as SlotInfo[];
|
return JSON.parse(data) as SlotInfo[]
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error getting slots for product ${productId}:`, error);
|
console.error(`Error getting slots for product ${productId}:`, error)
|
||||||
return [];
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAllProductsSlots(): Promise<Record<number, SlotInfo[]>> {
|
export async function getAllProductsSlots(): Promise<Record<number, SlotInfo[]>> {
|
||||||
try {
|
try {
|
||||||
// Get all keys matching the pattern "product:*:slots"
|
// Get all keys matching the pattern "product:*:slots"
|
||||||
const keys = await redisClient.KEYS('product:*:slots');
|
const keys = await redisClient.KEYS('product:*:slots')
|
||||||
|
|
||||||
if (keys.length === 0) return {};
|
if (keys.length === 0) return {}
|
||||||
|
|
||||||
// Get all product slots using MGET for better performance
|
// Get all product slots using MGET for better performance
|
||||||
const productsData = await redisClient.MGET(keys);
|
const productsData = await redisClient.MGET(keys)
|
||||||
|
|
||||||
const result: Record<number, SlotInfo[]> = {};
|
const result: Record<number, SlotInfo[]> = {}
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
// Extract productId from key "product:{id}:slots"
|
// Extract productId from key "product:{id}:slots"
|
||||||
const match = key.match(/product:(\d+):slots/);
|
const match = key.match(/product:(\d+):slots/)
|
||||||
if (match) {
|
if (match) {
|
||||||
const productId = parseInt(match[1], 10);
|
const productId = parseInt(match[1], 10)
|
||||||
const dataIndex = keys.indexOf(key);
|
const dataIndex = keys.indexOf(key)
|
||||||
if (productsData[dataIndex]) {
|
if (productsData[dataIndex]) {
|
||||||
result[productId] = JSON.parse(productsData[dataIndex]) as SlotInfo[];
|
result[productId] = JSON.parse(productsData[dataIndex]) as SlotInfo[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting all products slots:', error);
|
console.error('Error getting all products slots:', error)
|
||||||
return {};
|
return {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMultipleProductsSlots(productIds: number[]): Promise<Record<number, SlotInfo[]>> {
|
export async function getMultipleProductsSlots(
|
||||||
|
productIds: number[]
|
||||||
|
): Promise<Record<number, SlotInfo[]>> {
|
||||||
try {
|
try {
|
||||||
if (productIds.length === 0) return {};
|
if (productIds.length === 0) return {}
|
||||||
|
|
||||||
// Build keys for all productIds
|
// Build keys for all productIds
|
||||||
const keys = productIds.map(id => `product:${id}:slots`);
|
const keys = productIds.map((id) => `product:${id}:slots`)
|
||||||
|
|
||||||
// Use MGET for batch retrieval
|
// Use MGET for batch retrieval
|
||||||
const productsData = await redisClient.MGET(keys);
|
const productsData = await redisClient.MGET(keys)
|
||||||
|
|
||||||
const result: Record<number, SlotInfo[]> = {};
|
const result: Record<number, SlotInfo[]> = {}
|
||||||
for (let i = 0; i < productIds.length; i++) {
|
for (let i = 0; i < productIds.length; i++) {
|
||||||
const data = productsData[i];
|
const data = productsData[i]
|
||||||
if (data) {
|
if (data) {
|
||||||
const slots = JSON.parse(data) as SlotInfo[];
|
const slots = JSON.parse(data) as SlotInfo[]
|
||||||
// Filter out slots that are at full capacity
|
// Filter out slots that are at full capacity
|
||||||
result[productIds[i]] = slots.filter(slot => !slot.isCapacityFull);
|
result[productIds[i]] = slots.filter((slot) => !slot.isCapacityFull)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting products slots:', error);
|
console.error('Error getting products slots:', error)
|
||||||
return {};
|
return {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,21 @@
|
||||||
import redisClient from '@/src/lib/redis-client';
|
import redisClient from '@/src/lib/redis-client'
|
||||||
import { db } from '@/src/db/db_index'
|
import {
|
||||||
import { userIncidents } from '@/src/db/schema'
|
getAllUserNegativityScores as getAllUserNegativityScoresFromDb,
|
||||||
import { eq, sum } from 'drizzle-orm';
|
getUserNegativityScore as getUserNegativityScoreFromDb,
|
||||||
|
type UserNegativityData,
|
||||||
|
} from '@/src/dbService'
|
||||||
|
|
||||||
export async function initializeUserNegativityStore(): Promise<void> {
|
export async function initializeUserNegativityStore(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log('Initializing user negativity store in Redis...');
|
console.log('Initializing user negativity store in Redis...')
|
||||||
|
|
||||||
|
const results = await getAllUserNegativityScoresFromDb()
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Old implementation - direct DB queries:
|
||||||
|
import { db } from '@/src/db/db_index'
|
||||||
|
import { userIncidents } from '@/src/db/schema'
|
||||||
|
import { sum } from 'drizzle-orm'
|
||||||
|
|
||||||
const results = await db
|
const results = await db
|
||||||
.select({
|
.select({
|
||||||
|
|
@ -14,91 +24,102 @@ export async function initializeUserNegativityStore(): Promise<void> {
|
||||||
})
|
})
|
||||||
.from(userIncidents)
|
.from(userIncidents)
|
||||||
.groupBy(userIncidents.userId);
|
.groupBy(userIncidents.userId);
|
||||||
|
*/
|
||||||
|
|
||||||
for (const { userId, totalNegativityScore } of results) {
|
for (const { userId, totalNegativityScore } of results) {
|
||||||
await redisClient.set(
|
await redisClient.set(
|
||||||
`user:negativity:${userId}`,
|
`user:negativity:${userId}`,
|
||||||
totalNegativityScore.toString(),
|
totalNegativityScore.toString()
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`User negativity store initialized for ${results.length} users`);
|
console.log(`User negativity store initialized for ${results.length} users`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error initializing user negativity store:', error);
|
console.error('Error initializing user negativity store:', error)
|
||||||
throw error;
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUserNegativity(userId: number): Promise<number> {
|
export async function getUserNegativity(userId: number): Promise<number> {
|
||||||
try {
|
try {
|
||||||
const key = `user:negativity:${userId}`;
|
const key = `user:negativity:${userId}`
|
||||||
const data = await redisClient.get(key);
|
const data = await redisClient.get(key)
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return 0;
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
return parseInt(data, 10);
|
return parseInt(data, 10)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error getting negativity score for user ${userId}:`, error);
|
console.error(`Error getting negativity score for user ${userId}:`, error)
|
||||||
return 0;
|
return 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAllUserNegativityScores(): Promise<Record<number, number>> {
|
export async function getAllUserNegativityScores(): Promise<Record<number, number>> {
|
||||||
try {
|
try {
|
||||||
const keys = await redisClient.KEYS('user:negativity:*');
|
const keys = await redisClient.KEYS('user:negativity:*')
|
||||||
|
|
||||||
if (keys.length === 0) return {};
|
if (keys.length === 0) return {}
|
||||||
|
|
||||||
const values = await redisClient.MGET(keys);
|
const values = await redisClient.MGET(keys)
|
||||||
|
|
||||||
const result: Record<number, number> = {};
|
const result: Record<number, number> = {}
|
||||||
for (let i = 0; i < keys.length; i++) {
|
for (let i = 0; i < keys.length; i++) {
|
||||||
const key = keys[i];
|
const key = keys[i]
|
||||||
const value = values[i];
|
const value = values[i]
|
||||||
|
|
||||||
const match = key.match(/user:negativity:(\d+)/);
|
const match = key.match(/user:negativity:(\d+)/)
|
||||||
if (match && value) {
|
if (match && value) {
|
||||||
const userId = parseInt(match[1], 10);
|
const userId = parseInt(match[1], 10)
|
||||||
result[userId] = parseInt(value, 10);
|
result[userId] = parseInt(value, 10)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting all user negativity scores:', error);
|
console.error('Error getting all user negativity scores:', error)
|
||||||
return {};
|
return {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMultipleUserNegativityScores(userIds: number[]): Promise<Record<number, number>> {
|
export async function getMultipleUserNegativityScores(
|
||||||
|
userIds: number[]
|
||||||
|
): Promise<Record<number, number>> {
|
||||||
try {
|
try {
|
||||||
if (userIds.length === 0) return {};
|
if (userIds.length === 0) return {}
|
||||||
|
|
||||||
const keys = userIds.map(id => `user:negativity:${id}`);
|
const keys = userIds.map((id) => `user:negativity:${id}`)
|
||||||
|
|
||||||
const values = await redisClient.MGET(keys);
|
const values = await redisClient.MGET(keys)
|
||||||
|
|
||||||
const result: Record<number, number> = {};
|
const result: Record<number, number> = {}
|
||||||
for (let i = 0; i < userIds.length; i++) {
|
for (let i = 0; i < userIds.length; i++) {
|
||||||
const value = values[i];
|
const value = values[i]
|
||||||
if (value) {
|
if (value) {
|
||||||
result[userIds[i]] = parseInt(value, 10);
|
result[userIds[i]] = parseInt(value, 10)
|
||||||
} else {
|
} else {
|
||||||
result[userIds[i]] = 0;
|
result[userIds[i]] = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting multiple user negativity scores:', error);
|
console.error('Error getting multiple user negativity scores:', error)
|
||||||
return {};
|
return {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function recomputeUserNegativityScore(userId: number): Promise<void> {
|
export async function recomputeUserNegativityScore(userId: number): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
const totalScore = await getUserNegativityScoreFromDb(userId)
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Old implementation - direct DB queries:
|
||||||
|
import { db } from '@/src/db/db_index'
|
||||||
|
import { userIncidents } from '@/src/db/schema'
|
||||||
|
import { eq, sum } from 'drizzle-orm'
|
||||||
|
|
||||||
const [result] = await db
|
const [result] = await db
|
||||||
.select({
|
.select({
|
||||||
totalNegativityScore: sum(userIncidents.negativityScore).mapWith(Number),
|
totalNegativityScore: sum(userIncidents.negativityScore).mapWith(Number),
|
||||||
|
|
@ -108,11 +129,12 @@ export async function recomputeUserNegativityScore(userId: number): Promise<void
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
const totalScore = result?.totalNegativityScore || 0;
|
const totalScore = result?.totalNegativityScore || 0;
|
||||||
|
*/
|
||||||
|
|
||||||
const key = `user:negativity:${userId}`;
|
const key = `user:negativity:${userId}`
|
||||||
await redisClient.set(key, totalScore.toString());
|
await redisClient.set(key, totalScore.toString())
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error recomputing negativity score for user ${userId}:`, error);
|
console.error(`Error recomputing negativity score for user ${userId}:`, error)
|
||||||
throw error;
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index'
|
import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index'
|
||||||
import { commonRouter } from '@/src/trpc/apis/common-apis/common'
|
import { commonRouter } from '@/src/trpc/apis/common-apis/common'
|
||||||
import { db } from '@/src/db/db_index'
|
import {
|
||||||
import { keyValStore, productInfo, storeInfo } from '@/src/db/schema'
|
getStoresSummary,
|
||||||
|
healthCheck,
|
||||||
|
} from '@/src/dbService'
|
||||||
|
import type { StoresSummaryResponse } from '@packages/shared'
|
||||||
import * as turf from '@turf/turf';
|
import * as turf from '@turf/turf';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { mbnrGeoJson } from '@/src/lib/mbnr-geojson'
|
import { mbnrGeoJson } from '@/src/lib/mbnr-geojson'
|
||||||
|
|
@ -37,8 +40,14 @@ export async function scaffoldEssentialConsts() {
|
||||||
|
|
||||||
export const commonApiRouter = router({
|
export const commonApiRouter = router({
|
||||||
product: commonRouter,
|
product: commonRouter,
|
||||||
|
|
||||||
getStoresSummary: publicProcedure
|
getStoresSummary: publicProcedure
|
||||||
.query(async () => {
|
.query(async (): Promise<StoresSummaryResponse> => {
|
||||||
|
/*
|
||||||
|
// Old implementation - direct DB queries:
|
||||||
|
import { db } from '@/src/db/db_index'
|
||||||
|
import { storeInfo } from '@/src/db/schema'
|
||||||
|
|
||||||
const stores = await db.query.storeInfo.findMany({
|
const stores = await db.query.storeInfo.findMany({
|
||||||
columns: {
|
columns: {
|
||||||
id: true,
|
id: true,
|
||||||
|
|
@ -46,11 +55,15 @@ export const commonApiRouter = router({
|
||||||
description: true,
|
description: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
|
const stores = await getStoresSummary();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
stores,
|
stores,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
checkLocationInPolygon: publicProcedure
|
checkLocationInPolygon: publicProcedure
|
||||||
.input(z.object({
|
.input(z.object({
|
||||||
lat: z.number().min(-90).max(90),
|
lat: z.number().min(-90).max(90),
|
||||||
|
|
@ -110,16 +123,23 @@ export const commonApiRouter = router({
|
||||||
}
|
}
|
||||||
return { uploadUrls };
|
return { uploadUrls };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
healthCheck: publicProcedure
|
healthCheck: publicProcedure
|
||||||
.query(async () => {
|
.query(async () => {
|
||||||
|
/*
|
||||||
|
// Old implementation - direct DB queries:
|
||||||
|
import { db } from '@/src/db/db_index'
|
||||||
|
import { keyValStore, productInfo } from '@/src/db/schema'
|
||||||
|
|
||||||
// Test DB connection by selecting product names
|
// Test DB connection by selecting product names
|
||||||
// await db.select({ name: productInfo.name }).from(productInfo).limit(1);
|
// await db.select({ name: productInfo.name }).from(productInfo).limit(1);
|
||||||
await db.select({ key: keyValStore.key }).from(keyValStore).limit(1);
|
await db.select({ key: keyValStore.key }).from(keyValStore).limit(1);
|
||||||
|
*/
|
||||||
|
|
||||||
return {
|
const result = await healthCheck();
|
||||||
status: "ok",
|
return result;
|
||||||
};
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
essentialConsts: publicProcedure
|
essentialConsts: publicProcedure
|
||||||
.query(async () => {
|
.query(async () => {
|
||||||
const response = await scaffoldEssentialConsts();
|
const response = await scaffoldEssentialConsts();
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,35 @@
|
||||||
import { router, publicProcedure } from '@/src/trpc/trpc-index'
|
import { router, publicProcedure } from '@/src/trpc/trpc-index'
|
||||||
import { db } from '@/src/db/db_index'
|
import {
|
||||||
import { productInfo, units, productSlots, deliverySlotInfo, storeInfo } from '@/src/db/schema'
|
getSuspendedProductIds,
|
||||||
import { eq, gt, and, sql, inArray } from 'drizzle-orm';
|
getNextDeliveryDateWithCapacity,
|
||||||
|
getStoresSummary,
|
||||||
|
} from '@/src/dbService'
|
||||||
import { generateSignedUrlsFromS3Urls, generateSignedUrlFromS3Url } from '@/src/lib/s3-client'
|
import { generateSignedUrlsFromS3Urls, generateSignedUrlFromS3Url } from '@/src/lib/s3-client'
|
||||||
import { getAllProducts as getAllProductsFromCache } from '@/src/stores/product-store'
|
import { getAllProducts as getAllProductsFromCache } from '@/src/stores/product-store'
|
||||||
import { getDashboardTags as getDashboardTagsFromCache } from '@/src/stores/product-tag-store'
|
import { getDashboardTags as getDashboardTagsFromCache } from '@/src/stores/product-tag-store'
|
||||||
|
|
||||||
export const getNextDeliveryDate = async (productId: number): Promise<Date | null> => {
|
// Re-export with original name for backwards compatibility
|
||||||
const result = await db
|
export const getNextDeliveryDate = getNextDeliveryDateWithCapacity
|
||||||
.select({ deliveryTime: deliverySlotInfo.deliveryTime })
|
|
||||||
.from(productSlots)
|
|
||||||
.innerJoin(deliverySlotInfo, eq(productSlots.slotId, deliverySlotInfo.id))
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(productSlots.productId, productId),
|
|
||||||
eq(deliverySlotInfo.isActive, true),
|
|
||||||
eq(deliverySlotInfo.isCapacityFull, false),
|
|
||||||
gt(deliverySlotInfo.deliveryTime, sql`NOW()`)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.orderBy(deliverySlotInfo.deliveryTime)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
|
|
||||||
return result[0]?.deliveryTime || null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function scaffoldProducts() {
|
export async function scaffoldProducts() {
|
||||||
// Get all products from cache
|
// Get all products from cache
|
||||||
let products = await getAllProductsFromCache();
|
let products = await getAllProductsFromCache();
|
||||||
products = products.filter(item => Boolean(item.id))
|
products = products.filter(item => Boolean(item.id))
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Old implementation - direct DB queries:
|
||||||
|
import { db } from '@/src/db/db_index'
|
||||||
|
import { productInfo } from '@/src/db/schema'
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
|
||||||
// Get suspended product IDs to filter them out
|
// Get suspended product IDs to filter them out
|
||||||
const suspendedProducts = await db
|
const suspendedProducts = await db
|
||||||
.select({ id: productInfo.id })
|
.select({ id: productInfo.id })
|
||||||
.from(productInfo)
|
.from(productInfo)
|
||||||
.where(eq(productInfo.isSuspended, true));
|
.where(eq(productInfo.isSuspended, true));
|
||||||
|
*/
|
||||||
|
|
||||||
const suspendedProductIds = new Set(suspendedProducts.map(sp => sp.id));
|
const suspendedProductIds = new Set(await getSuspendedProductIds());
|
||||||
|
|
||||||
// Filter out suspended products
|
// Filter out suspended products
|
||||||
products = products.filter(product => !suspendedProductIds.has(product.id));
|
products = products.filter(product => !suspendedProductIds.has(product.id));
|
||||||
|
|
@ -45,7 +37,7 @@ export async function scaffoldProducts() {
|
||||||
// Format products to match the expected response structure
|
// Format products to match the expected response structure
|
||||||
const formattedProducts = await Promise.all(
|
const formattedProducts = await Promise.all(
|
||||||
products.map(async (product) => {
|
products.map(async (product) => {
|
||||||
const nextDeliveryDate = await getNextDeliveryDate(product.id);
|
const nextDeliveryDate = await getNextDeliveryDateWithCapacity(product.id);
|
||||||
return {
|
return {
|
||||||
id: product.id,
|
id: product.id,
|
||||||
name: product.name,
|
name: product.name,
|
||||||
|
|
@ -89,28 +81,18 @@ export const commonRouter = router({
|
||||||
return response;
|
return response;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Old implementation - moved to common-trpc-index.ts:
|
||||||
getStoresSummary: publicProcedure
|
getStoresSummary: publicProcedure
|
||||||
.query(async () => {
|
.query(async () => {
|
||||||
const stores = await db.query.storeInfo.findMany({
|
const stores = await getStoresSummary();
|
||||||
columns: {
|
return { stores };
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
description: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
stores,
|
|
||||||
};
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
healthCheck: publicProcedure
|
healthCheck: publicProcedure
|
||||||
.query(async () => {
|
.query(async () => {
|
||||||
// Test DB connection by selecting product names
|
const result = await healthCheck();
|
||||||
await db.select({ name: productInfo.name }).from(productInfo).limit(1);
|
return result;
|
||||||
|
|
||||||
return {
|
|
||||||
status: "ok",
|
|
||||||
};
|
|
||||||
}),
|
}),
|
||||||
|
*/
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { eq } from 'drizzle-orm';
|
import {
|
||||||
import { db } from '@/src/db/db_index'
|
getUserAuthByEmail,
|
||||||
import { users, userCreds, userDetails } from '@/src/db/schema'
|
getUserAuthByMobile,
|
||||||
|
createUserWithProfile,
|
||||||
|
getUserAuthById,
|
||||||
|
getUserDetailsByUserId,
|
||||||
|
updateUserProfile,
|
||||||
|
} from '@/src/dbService';
|
||||||
import { ApiError } from '@/src/lib/api-error'
|
import { ApiError } from '@/src/lib/api-error'
|
||||||
import catchAsync from '@/src/lib/catch-async'
|
import catchAsync from '@/src/lib/catch-async'
|
||||||
import { jwtSecret } from '@/src/lib/env-exporter';
|
import { jwtSecret } from '@/src/lib/env-exporter';
|
||||||
|
|
@ -79,24 +84,36 @@ export const register = catchAsync(async (req: Request, res: Response, next: Nex
|
||||||
throw new ApiError('Invalid mobile number', 400);
|
throw new ApiError('Invalid mobile number', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if email already exists
|
/*
|
||||||
|
// Old implementation - direct DB queries:
|
||||||
|
import { db } from '@/src/db/db_index'
|
||||||
|
import { users } from '@/src/db/schema'
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
|
||||||
const [existingEmail] = await db
|
const [existingEmail] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(users)
|
.from(users)
|
||||||
.where(eq(users.email, email.toLowerCase()))
|
.where(eq(users.email, email.toLowerCase()))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Check if email already exists
|
||||||
|
const existingEmail = await getUserAuthByEmail(email.toLowerCase());
|
||||||
if (existingEmail) {
|
if (existingEmail) {
|
||||||
throw new ApiError('Email already registered', 409);
|
throw new ApiError('Email already registered', 409);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if mobile already exists
|
/*
|
||||||
|
// Old implementation - direct DB queries:
|
||||||
const [existingMobile] = await db
|
const [existingMobile] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(users)
|
.from(users)
|
||||||
.where(eq(users.mobile, cleanMobile))
|
.where(eq(users.mobile, cleanMobile))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Check if mobile already exists
|
||||||
|
const existingMobile = await getUserAuthByMobile(cleanMobile);
|
||||||
if (existingMobile) {
|
if (existingMobile) {
|
||||||
throw new ApiError('Mobile number already registered', 409);
|
throw new ApiError('Mobile number already registered', 409);
|
||||||
}
|
}
|
||||||
|
|
@ -104,9 +121,11 @@ export const register = catchAsync(async (req: Request, res: Response, next: Nex
|
||||||
// Hash password
|
// Hash password
|
||||||
const hashedPassword = await bcrypt.hash(password, 12);
|
const hashedPassword = await bcrypt.hash(password, 12);
|
||||||
|
|
||||||
// Create user and credentials in a transaction
|
/*
|
||||||
|
// Old implementation - direct DB queries:
|
||||||
|
import { userCreds, userDetails } from '@/src/db/schema'
|
||||||
|
|
||||||
const newUser = await db.transaction(async (tx) => {
|
const newUser = await db.transaction(async (tx) => {
|
||||||
// Create user
|
|
||||||
const [user] = await tx
|
const [user] = await tx
|
||||||
.insert(users)
|
.insert(users)
|
||||||
.values({
|
.values({
|
||||||
|
|
@ -116,24 +135,28 @@ export const register = catchAsync(async (req: Request, res: Response, next: Nex
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
// Create user credentials
|
await tx.insert(userCreds).values({
|
||||||
await tx
|
|
||||||
.insert(userCreds)
|
|
||||||
.values({
|
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
userPassword: hashedPassword,
|
userPassword: hashedPassword,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create user details with profile image
|
await tx.insert(userDetails).values({
|
||||||
await tx
|
|
||||||
.insert(userDetails)
|
|
||||||
.values({
|
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
profileImage: profileImageUrl,
|
profileImage: profileImageUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Create user with profile in transaction
|
||||||
|
const newUser = await createUserWithProfile({
|
||||||
|
name: name.trim(),
|
||||||
|
email: email.toLowerCase().trim(),
|
||||||
|
mobile: cleanMobile,
|
||||||
|
hashedPassword,
|
||||||
|
profileImage: profileImageUrl,
|
||||||
|
});
|
||||||
|
|
||||||
const token = generateToken(newUser.id);
|
const token = generateToken(newUser.id);
|
||||||
|
|
||||||
|
|
@ -195,7 +218,12 @@ export const updateProfile = catchAsync(async (req: Request, res: Response, next
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if email already exists (if changing email)
|
/*
|
||||||
|
// Old implementation - direct DB queries:
|
||||||
|
import { db } from '@/src/db/db_index'
|
||||||
|
import { users, userCreds, userDetails } from '@/src/db/schema'
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
|
||||||
if (email) {
|
if (email) {
|
||||||
const [existingEmail] = await db
|
const [existingEmail] = await db
|
||||||
.select()
|
.select()
|
||||||
|
|
@ -207,8 +235,18 @@ export const updateProfile = catchAsync(async (req: Request, res: Response, next
|
||||||
throw new ApiError('Email already registered', 409);
|
throw new ApiError('Email already registered', 409);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
// Check if mobile already exists (if changing mobile)
|
// Check if email already exists (if changing email)
|
||||||
|
if (email) {
|
||||||
|
const existingEmail = await getUserAuthByEmail(email.toLowerCase());
|
||||||
|
if (existingEmail && existingEmail.id !== userId) {
|
||||||
|
throw new ApiError('Email already registered', 409);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Old implementation - direct DB queries:
|
||||||
if (mobile) {
|
if (mobile) {
|
||||||
const cleanMobile = mobile.replace(/\D/g, '');
|
const cleanMobile = mobile.replace(/\D/g, '');
|
||||||
const [existingMobile] = await db
|
const [existingMobile] = await db
|
||||||
|
|
@ -221,8 +259,25 @@ export const updateProfile = catchAsync(async (req: Request, res: Response, next
|
||||||
throw new ApiError('Mobile number already registered', 409);
|
throw new ApiError('Mobile number already registered', 409);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
// Update user and user details in a transaction
|
// Check if mobile already exists (if changing mobile)
|
||||||
|
if (mobile) {
|
||||||
|
const cleanMobile = mobile.replace(/\D/g, '');
|
||||||
|
const existingMobile = await getUserAuthByMobile(cleanMobile);
|
||||||
|
if (existingMobile && existingMobile.id !== userId) {
|
||||||
|
throw new ApiError('Mobile number already registered', 409);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash password if provided
|
||||||
|
let hashedPassword: string | undefined;
|
||||||
|
if (password) {
|
||||||
|
hashedPassword = await bcrypt.hash(password, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Old implementation - direct DB queries:
|
||||||
const updatedUser = await db.transaction(async (tx) => {
|
const updatedUser = await db.transaction(async (tx) => {
|
||||||
// Update user table
|
// Update user table
|
||||||
const updateData: any = {};
|
const updateData: any = {};
|
||||||
|
|
@ -231,19 +286,13 @@ export const updateProfile = catchAsync(async (req: Request, res: Response, next
|
||||||
if (mobile) updateData.mobile = mobile.replace(/\D/g, '');
|
if (mobile) updateData.mobile = mobile.replace(/\D/g, '');
|
||||||
|
|
||||||
if (Object.keys(updateData).length > 0) {
|
if (Object.keys(updateData).length > 0) {
|
||||||
await tx
|
await tx.update(users).set(updateData).where(eq(users.id, userId));
|
||||||
.update(users)
|
|
||||||
.set(updateData)
|
|
||||||
.where(eq(users.id, userId));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update password if provided
|
// Update password if provided
|
||||||
if (password) {
|
if (password) {
|
||||||
const hashedPassword = await bcrypt.hash(password, 12);
|
const hashedPassword = await bcrypt.hash(password, 12);
|
||||||
await tx
|
await tx.update(userCreds).set({ userPassword: hashedPassword }).where(eq(userCreds.userId, userId));
|
||||||
.update(userCreds)
|
|
||||||
.set({ userPassword: hashedPassword })
|
|
||||||
.where(eq(userCreds.userId, userId));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update or insert user details
|
// Update or insert user details
|
||||||
|
|
@ -255,44 +304,41 @@ export const updateProfile = catchAsync(async (req: Request, res: Response, next
|
||||||
if (profileImageUrl) userDetailsUpdate.profileImage = profileImageUrl;
|
if (profileImageUrl) userDetailsUpdate.profileImage = profileImageUrl;
|
||||||
userDetailsUpdate.updatedAt = new Date();
|
userDetailsUpdate.updatedAt = new Date();
|
||||||
|
|
||||||
// Check if user details record exists
|
const [existingDetails] = await tx.select().from(userDetails).where(eq(userDetails.userId, userId)).limit(1);
|
||||||
const [existingDetails] = await tx
|
|
||||||
.select()
|
|
||||||
.from(userDetails)
|
|
||||||
.where(eq(userDetails.userId, userId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (existingDetails) {
|
if (existingDetails) {
|
||||||
// Update existing record
|
await tx.update(userDetails).set(userDetailsUpdate).where(eq(userDetails.userId, userId));
|
||||||
await tx
|
|
||||||
.update(userDetails)
|
|
||||||
.set(userDetailsUpdate)
|
|
||||||
.where(eq(userDetails.userId, userId));
|
|
||||||
} else {
|
} else {
|
||||||
// Create new record
|
|
||||||
userDetailsUpdate.userId = userId;
|
userDetailsUpdate.userId = userId;
|
||||||
userDetailsUpdate.createdAt = new Date();
|
userDetailsUpdate.createdAt = new Date();
|
||||||
await tx
|
await tx.insert(userDetails).values(userDetailsUpdate);
|
||||||
.insert(userDetails)
|
|
||||||
.values(userDetailsUpdate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return updated user data
|
const [user] = await tx.select().from(users).where(eq(users.id, userId)).limit(1);
|
||||||
const [user] = await tx
|
|
||||||
.select()
|
|
||||||
.from(users)
|
|
||||||
.where(eq(users.id, userId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Update user profile in transaction
|
||||||
|
const updatedUser = await updateUserProfile(userId, {
|
||||||
|
name: name?.trim(),
|
||||||
|
email: email?.toLowerCase().trim(),
|
||||||
|
mobile: mobile?.replace(/\D/g, ''),
|
||||||
|
hashedPassword,
|
||||||
|
profileImage: profileImageUrl,
|
||||||
|
bio,
|
||||||
|
dateOfBirth: dateOfBirth ? new Date(dateOfBirth) : undefined,
|
||||||
|
gender,
|
||||||
|
occupation,
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Old implementation - direct DB queries:
|
||||||
|
const [userDetail] = await db.select().from(userDetails).where(eq(userDetails.userId, userId)).limit(1);
|
||||||
|
*/
|
||||||
|
|
||||||
// Get updated user details for response
|
// Get updated user details for response
|
||||||
const [userDetail] = await db
|
const userDetail = await getUserDetailsByUserId(userId);
|
||||||
.select()
|
|
||||||
.from(userDetails)
|
|
||||||
.where(eq(userDetails.userId, userId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
// Generate signed URL for profile image if it exists
|
// Generate signed URL for profile image if it exists
|
||||||
const profileImageSignedUrl = userDetail?.profileImage
|
const profileImageSignedUrl = userDetail?.profileImage
|
||||||
|
|
@ -319,3 +365,10 @@ export const updateProfile = catchAsync(async (req: Request, res: Response, next
|
||||||
data: response,
|
data: response,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Old implementation - direct DB queries:
|
||||||
|
import { db } from '@/src/db/db_index'
|
||||||
|
import { users, userCreds, userDetails } from '@/src/db/schema'
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { db } from '@/src/db/db_index'
|
import { createUserComplaint } from '@/src/dbService';
|
||||||
import { complaints } from '@/src/db/schema'
|
|
||||||
import { ApiError } from '@/src/lib/api-error'
|
import { ApiError } from '@/src/lib/api-error'
|
||||||
import catchAsync from '@/src/lib/catch-async'
|
import catchAsync from '@/src/lib/catch-async'
|
||||||
import { imageUploadS3 } from '@/src/lib/s3-client'
|
import { imageUploadS3 } from '@/src/lib/s3-client'
|
||||||
|
|
@ -43,15 +42,34 @@ export const raiseComplaint = catchAsync(async (req: Request, res: Response, nex
|
||||||
uploadedImageUrls = await Promise.all(imageUploadPromises);
|
uploadedImageUrls = await Promise.all(imageUploadPromises);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Old implementation - direct DB queries:
|
||||||
|
import { db } from '@/src/db/db_index'
|
||||||
|
import { complaints } from '@/src/db/schema'
|
||||||
|
|
||||||
await db.insert(complaints).values({
|
await db.insert(complaints).values({
|
||||||
userId,
|
userId,
|
||||||
orderId: orderIdNum,
|
orderId: orderIdNum,
|
||||||
complaintBody: complaintBody.trim(),
|
complaintBody: complaintBody.trim(),
|
||||||
images: uploadedImageUrls.length > 0 ? uploadedImageUrls : null,
|
images: uploadedImageUrls.length > 0 ? uploadedImageUrls : null,
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
|
await createUserComplaint(
|
||||||
|
userId,
|
||||||
|
orderIdNum,
|
||||||
|
complaintBody.trim(),
|
||||||
|
uploadedImageUrls.length > 0 ? uploadedImageUrls : null
|
||||||
|
);
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Complaint raised successfully'
|
message: 'Complaint raised successfully'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Old implementation - direct DB queries:
|
||||||
|
import { db } from '@/src/db/db_index'
|
||||||
|
import { complaints } from '@/src/db/schema'
|
||||||
|
*/
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@ export { db } from './src/db/db_index';
|
||||||
// Re-export schema
|
// Re-export schema
|
||||||
export * from './src/db/schema';
|
export * from './src/db/schema';
|
||||||
|
|
||||||
|
// Export enum types for type safety
|
||||||
|
export { staffRoleEnum, staffPermissionEnum } from './src/db/schema';
|
||||||
|
|
||||||
// Admin API helpers - explicitly namespaced exports to avoid duplicates
|
// Admin API helpers - explicitly namespaced exports to avoid duplicates
|
||||||
export {
|
export {
|
||||||
// Banner
|
// Banner
|
||||||
|
|
@ -105,6 +108,7 @@ export {
|
||||||
export {
|
export {
|
||||||
// Staff User
|
// Staff User
|
||||||
getStaffUserByName,
|
getStaffUserByName,
|
||||||
|
getStaffUserById,
|
||||||
getAllStaff,
|
getAllStaff,
|
||||||
getAllUsers,
|
getAllUsers,
|
||||||
getUserWithDetails,
|
getUserWithDetails,
|
||||||
|
|
@ -212,6 +216,8 @@ export {
|
||||||
getProductReviews as getUserProductReviews,
|
getProductReviews as getUserProductReviews,
|
||||||
getProductById as getUserProductByIdBasic,
|
getProductById as getUserProductByIdBasic,
|
||||||
createProductReview as createUserProductReview,
|
createProductReview as createUserProductReview,
|
||||||
|
getAllProductsWithUnits,
|
||||||
|
type ProductSummaryData,
|
||||||
} from './src/user-apis/product';
|
} from './src/user-apis/product';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
@ -237,10 +243,15 @@ export {
|
||||||
getUserById as getUserAuthById,
|
getUserById as getUserAuthById,
|
||||||
getUserCreds as getUserAuthCreds,
|
getUserCreds as getUserAuthCreds,
|
||||||
getUserDetails as getUserAuthDetails,
|
getUserDetails as getUserAuthDetails,
|
||||||
|
isUserSuspended,
|
||||||
createUserWithCreds as createUserAuthWithCreds,
|
createUserWithCreds as createUserAuthWithCreds,
|
||||||
createUserWithMobile as createUserAuthWithMobile,
|
createUserWithMobile as createUserAuthWithMobile,
|
||||||
upsertUserPassword as upsertUserAuthPassword,
|
upsertUserPassword as upsertUserAuthPassword,
|
||||||
deleteUserAccount as deleteUserAuthAccount,
|
deleteUserAccount as deleteUserAuthAccount,
|
||||||
|
// UV API helpers
|
||||||
|
createUserWithProfile,
|
||||||
|
getUserDetailsByUserId,
|
||||||
|
updateUserProfile,
|
||||||
} from './src/user-apis/auth';
|
} from './src/user-apis/auth';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
@ -284,4 +295,80 @@ export {
|
||||||
getRecentlyDeliveredOrderIds as getUserRecentlyDeliveredOrderIds,
|
getRecentlyDeliveredOrderIds as getUserRecentlyDeliveredOrderIds,
|
||||||
getProductIdsFromOrders as getUserProductIdsFromOrders,
|
getProductIdsFromOrders as getUserProductIdsFromOrders,
|
||||||
getProductsForRecentOrders as getUserProductsForRecentOrders,
|
getProductsForRecentOrders as getUserProductsForRecentOrders,
|
||||||
|
// Post-order handler helpers
|
||||||
|
getOrdersByIdsWithFullData,
|
||||||
|
getOrderByIdWithFullData,
|
||||||
|
type OrderWithFullData,
|
||||||
|
type OrderWithCancellationData,
|
||||||
} from './src/user-apis/order';
|
} from './src/user-apis/order';
|
||||||
|
|
||||||
|
// Store Helpers (for cache initialization)
|
||||||
|
export {
|
||||||
|
// Banner Store
|
||||||
|
getAllBannersForCache,
|
||||||
|
type BannerData,
|
||||||
|
// Product Store
|
||||||
|
getAllProductsForCache,
|
||||||
|
getAllStoresForCache,
|
||||||
|
getAllDeliverySlotsForCache,
|
||||||
|
getAllSpecialDealsForCache,
|
||||||
|
getAllProductTagsForCache,
|
||||||
|
type ProductBasicData,
|
||||||
|
type StoreBasicData,
|
||||||
|
type DeliverySlotData,
|
||||||
|
type SpecialDealData,
|
||||||
|
type ProductTagData,
|
||||||
|
// Product Tag Store
|
||||||
|
getAllTagsForCache,
|
||||||
|
getAllTagProductMappings,
|
||||||
|
type TagBasicData,
|
||||||
|
type TagProductMapping,
|
||||||
|
// Slot Store
|
||||||
|
getAllSlotsWithProductsForCache,
|
||||||
|
type SlotWithProductsData,
|
||||||
|
// User Negativity Store
|
||||||
|
getAllUserNegativityScores,
|
||||||
|
getUserNegativityScore,
|
||||||
|
type UserNegativityData,
|
||||||
|
} from './src/stores/store-helpers';
|
||||||
|
|
||||||
|
// Automated Jobs Helpers
|
||||||
|
export {
|
||||||
|
toggleFlashDeliveryForItems,
|
||||||
|
toggleKeyVal,
|
||||||
|
getAllKeyValStore,
|
||||||
|
} from './src/lib/automated-jobs';
|
||||||
|
|
||||||
|
// Health Check
|
||||||
|
export {
|
||||||
|
healthCheck,
|
||||||
|
} from './src/lib/health-check';
|
||||||
|
|
||||||
|
// Common API Helpers
|
||||||
|
export {
|
||||||
|
getSuspendedProductIds,
|
||||||
|
getNextDeliveryDateWithCapacity,
|
||||||
|
} from './src/user-apis/product';
|
||||||
|
|
||||||
|
export {
|
||||||
|
getStoresSummary,
|
||||||
|
} from './src/user-apis/stores';
|
||||||
|
|
||||||
|
// Delete Orders Helper
|
||||||
|
export {
|
||||||
|
deleteOrdersWithRelations,
|
||||||
|
} from './src/lib/delete-orders';
|
||||||
|
|
||||||
|
// Seed Helpers
|
||||||
|
export {
|
||||||
|
seedUnits,
|
||||||
|
seedStaffRoles,
|
||||||
|
seedStaffPermissions,
|
||||||
|
seedRolePermissions,
|
||||||
|
seedKeyValStore,
|
||||||
|
type UnitSeedData,
|
||||||
|
type RolePermissionAssignment,
|
||||||
|
type KeyValSeedData,
|
||||||
|
type StaffRoleName,
|
||||||
|
type StaffPermissionName,
|
||||||
|
} from './src/lib/seed';
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,14 @@ export async function getStaffUserByName(name: string): Promise<StaffUser | null
|
||||||
return staff || null;
|
return staff || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getStaffUserById(staffId: number): Promise<StaffUser | null> {
|
||||||
|
const staff = await db.query.staffUsers.findFirst({
|
||||||
|
where: eq(staffUsers.id, staffId),
|
||||||
|
});
|
||||||
|
|
||||||
|
return staff || null;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getAllStaff(): Promise<any[]> {
|
export async function getAllStaff(): Promise<any[]> {
|
||||||
const staff = await db.query.staffUsers.findMany({
|
const staff = await db.query.staffUsers.findMany({
|
||||||
columns: {
|
columns: {
|
||||||
|
|
|
||||||
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
|
return details || null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function isUserSuspended(userId: number): Promise<boolean> {
|
||||||
|
const details = await getUserDetails(userId)
|
||||||
|
return details?.isSuspended ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createUserWithProfile(input: {
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
mobile: string
|
||||||
|
hashedPassword: string
|
||||||
|
profileImage?: string | null
|
||||||
|
}) {
|
||||||
|
return db.transaction(async (tx) => {
|
||||||
|
// Create user
|
||||||
|
const [user] = await tx.insert(users).values({
|
||||||
|
name: input.name,
|
||||||
|
email: input.email,
|
||||||
|
mobile: input.mobile,
|
||||||
|
}).returning()
|
||||||
|
|
||||||
|
// Create user credentials
|
||||||
|
await tx.insert(userCreds).values({
|
||||||
|
userId: user.id,
|
||||||
|
userPassword: input.hashedPassword,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create user details with profile image
|
||||||
|
await tx.insert(userDetails).values({
|
||||||
|
userId: user.id,
|
||||||
|
profileImage: input.profileImage || null,
|
||||||
|
})
|
||||||
|
|
||||||
|
return user
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUserDetailsByUserId(userId: number) {
|
||||||
|
const [details] = await db.select().from(userDetails).where(eq(userDetails.userId, userId)).limit(1)
|
||||||
|
return details || null
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateUserProfile(userId: number, data: {
|
||||||
|
name?: string
|
||||||
|
email?: string
|
||||||
|
mobile?: string
|
||||||
|
hashedPassword?: string
|
||||||
|
profileImage?: string
|
||||||
|
bio?: string
|
||||||
|
dateOfBirth?: Date | null
|
||||||
|
gender?: string
|
||||||
|
occupation?: string
|
||||||
|
}) {
|
||||||
|
return db.transaction(async (tx) => {
|
||||||
|
// Update user table
|
||||||
|
const userUpdate: any = {}
|
||||||
|
if (data.name !== undefined) userUpdate.name = data.name
|
||||||
|
if (data.email !== undefined) userUpdate.email = data.email
|
||||||
|
if (data.mobile !== undefined) userUpdate.mobile = data.mobile
|
||||||
|
|
||||||
|
if (Object.keys(userUpdate).length > 0) {
|
||||||
|
await tx.update(users).set(userUpdate).where(eq(users.id, userId))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update password if provided
|
||||||
|
if (data.hashedPassword) {
|
||||||
|
await tx.update(userCreds).set({
|
||||||
|
userPassword: data.hashedPassword,
|
||||||
|
}).where(eq(userCreds.userId, userId))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update or insert user details
|
||||||
|
const detailsUpdate: any = {}
|
||||||
|
if (data.bio !== undefined) detailsUpdate.bio = data.bio
|
||||||
|
if (data.dateOfBirth !== undefined) detailsUpdate.dateOfBirth = data.dateOfBirth
|
||||||
|
if (data.gender !== undefined) detailsUpdate.gender = data.gender
|
||||||
|
if (data.occupation !== undefined) detailsUpdate.occupation = data.occupation
|
||||||
|
if (data.profileImage !== undefined) detailsUpdate.profileImage = data.profileImage
|
||||||
|
detailsUpdate.updatedAt = new Date()
|
||||||
|
|
||||||
|
const [existingDetails] = await tx.select().from(userDetails).where(eq(userDetails.userId, userId)).limit(1)
|
||||||
|
|
||||||
|
if (existingDetails) {
|
||||||
|
await tx.update(userDetails).set(detailsUpdate).where(eq(userDetails.userId, userId))
|
||||||
|
} else {
|
||||||
|
await tx.insert(userDetails).values({
|
||||||
|
userId,
|
||||||
|
...detailsUpdate,
|
||||||
|
createdAt: new Date(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return updated user
|
||||||
|
const [user] = await tx.select().from(users).where(eq(users.id, userId)).limit(1)
|
||||||
|
return user
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export async function createUserWithCreds(input: {
|
export async function createUserWithCreds(input: {
|
||||||
name: string
|
name: string
|
||||||
email: string
|
email: string
|
||||||
|
|
|
||||||
|
|
@ -30,10 +30,16 @@ export async function getUserComplaints(userId: number): Promise<UserComplaint[]
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createComplaint(userId: number, orderId: number | null, complaintBody: string): Promise<void> {
|
export async function createComplaint(
|
||||||
|
userId: number,
|
||||||
|
orderId: number | null,
|
||||||
|
complaintBody: string,
|
||||||
|
images?: string[] | null
|
||||||
|
): Promise<void> {
|
||||||
await db.insert(complaints).values({
|
await db.insert(complaints).values({
|
||||||
userId,
|
userId,
|
||||||
orderId,
|
orderId,
|
||||||
complaintBody,
|
complaintBody,
|
||||||
|
images: images || null,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -622,3 +622,112 @@ export async function getProductsForRecentOrders(
|
||||||
.orderBy(desc(productInfo.createdAt))
|
.orderBy(desc(productInfo.createdAt))
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Post-Order Handler Helpers (for Telegram notifications)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface OrderWithFullData {
|
||||||
|
id: number
|
||||||
|
totalAmount: string
|
||||||
|
isFlashDelivery: boolean
|
||||||
|
address: {
|
||||||
|
name: string | null
|
||||||
|
addressLine1: string | null
|
||||||
|
addressLine2: string | null
|
||||||
|
city: string | null
|
||||||
|
state: string | null
|
||||||
|
pincode: string | null
|
||||||
|
phone: string | null
|
||||||
|
} | null
|
||||||
|
orderItems: Array<{
|
||||||
|
quantity: string
|
||||||
|
product: {
|
||||||
|
name: string
|
||||||
|
} | null
|
||||||
|
}>
|
||||||
|
slot: {
|
||||||
|
deliveryTime: Date
|
||||||
|
} | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getOrdersByIdsWithFullData(
|
||||||
|
orderIds: number[]
|
||||||
|
): Promise<OrderWithFullData[]> {
|
||||||
|
return db.query.orders.findMany({
|
||||||
|
where: inArray(orders.id, orderIds),
|
||||||
|
with: {
|
||||||
|
address: {
|
||||||
|
columns: {
|
||||||
|
name: true,
|
||||||
|
addressLine1: true,
|
||||||
|
addressLine2: true,
|
||||||
|
city: true,
|
||||||
|
state: true,
|
||||||
|
pincode: true,
|
||||||
|
phone: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderItems: {
|
||||||
|
with: {
|
||||||
|
product: {
|
||||||
|
columns: {
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
slot: {
|
||||||
|
columns: {
|
||||||
|
deliveryTime: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}) as Promise<OrderWithFullData[]>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrderWithCancellationData extends OrderWithFullData {
|
||||||
|
refunds: Array<{
|
||||||
|
refundStatus: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getOrderByIdWithFullData(
|
||||||
|
orderId: number
|
||||||
|
): Promise<OrderWithCancellationData | null> {
|
||||||
|
return db.query.orders.findFirst({
|
||||||
|
where: eq(orders.id, orderId),
|
||||||
|
with: {
|
||||||
|
address: {
|
||||||
|
columns: {
|
||||||
|
name: true,
|
||||||
|
addressLine1: true,
|
||||||
|
addressLine2: true,
|
||||||
|
city: true,
|
||||||
|
state: true,
|
||||||
|
pincode: true,
|
||||||
|
phone: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderItems: {
|
||||||
|
with: {
|
||||||
|
product: {
|
||||||
|
columns: {
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
slot: {
|
||||||
|
columns: {
|
||||||
|
deliveryTime: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
refunds: {
|
||||||
|
columns: {
|
||||||
|
refundStatus: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}) as Promise<OrderWithCancellationData | null>
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { db } from '../db/db_index'
|
import { db } from '../db/db_index'
|
||||||
import { deliverySlotInfo, productInfo, productReviews, productSlots, specialDeals, storeInfo, units, users } from '../db/schema'
|
import { deliverySlotInfo, productInfo, productReviews, productSlots, productTags, specialDeals, storeInfo, units, users } from '../db/schema'
|
||||||
import { and, desc, eq, gt, sql } from 'drizzle-orm'
|
import { and, desc, eq, gt, inArray, sql } from 'drizzle-orm'
|
||||||
import type { UserProductDetailData, UserProductReview } from '@packages/shared'
|
import type { UserProductDetailData, UserProductReview } from '@packages/shared'
|
||||||
|
|
||||||
const getStringArray = (value: unknown): string[] | null => {
|
const getStringArray = (value: unknown): string[] | null => {
|
||||||
|
|
@ -179,3 +179,87 @@ export async function createProductReview(
|
||||||
userName: null,
|
userName: null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProductSummaryData {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
shortDescription: string | null
|
||||||
|
price: string
|
||||||
|
marketPrice: string | null
|
||||||
|
images: unknown
|
||||||
|
isOutOfStock: boolean
|
||||||
|
unitShortNotation: string
|
||||||
|
productQuantity: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAllProductsWithUnits(tagId?: number): Promise<ProductSummaryData[]> {
|
||||||
|
let productIds: number[] | null = null
|
||||||
|
|
||||||
|
// If tagId is provided, get products that have this tag
|
||||||
|
if (tagId) {
|
||||||
|
const taggedProducts = await db
|
||||||
|
.select({ productId: productTags.productId })
|
||||||
|
.from(productTags)
|
||||||
|
.where(eq(productTags.tagId, tagId))
|
||||||
|
|
||||||
|
productIds = taggedProducts.map(tp => tp.productId)
|
||||||
|
}
|
||||||
|
|
||||||
|
let whereCondition = undefined
|
||||||
|
|
||||||
|
// Filter by product IDs if tag filtering is applied
|
||||||
|
if (productIds && productIds.length > 0) {
|
||||||
|
whereCondition = inArray(productInfo.id, productIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
return db
|
||||||
|
.select({
|
||||||
|
id: productInfo.id,
|
||||||
|
name: productInfo.name,
|
||||||
|
shortDescription: productInfo.shortDescription,
|
||||||
|
price: productInfo.price,
|
||||||
|
marketPrice: productInfo.marketPrice,
|
||||||
|
images: productInfo.images,
|
||||||
|
isOutOfStock: productInfo.isOutOfStock,
|
||||||
|
unitShortNotation: units.shortNotation,
|
||||||
|
productQuantity: productInfo.productQuantity,
|
||||||
|
})
|
||||||
|
.from(productInfo)
|
||||||
|
.innerJoin(units, eq(productInfo.unitId, units.id))
|
||||||
|
.where(whereCondition)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all suspended product IDs
|
||||||
|
*/
|
||||||
|
export async function getSuspendedProductIds(): Promise<number[]> {
|
||||||
|
const suspendedProducts = await db
|
||||||
|
.select({ id: productInfo.id })
|
||||||
|
.from(productInfo)
|
||||||
|
.where(eq(productInfo.isSuspended, true))
|
||||||
|
|
||||||
|
return suspendedProducts.map(sp => sp.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get next delivery date for a product (with capacity check)
|
||||||
|
* This version filters by both isActive AND isCapacityFull
|
||||||
|
*/
|
||||||
|
export async function getNextDeliveryDateWithCapacity(productId: number): Promise<Date | null> {
|
||||||
|
const result = await db
|
||||||
|
.select({ deliveryTime: deliverySlotInfo.deliveryTime })
|
||||||
|
.from(productSlots)
|
||||||
|
.innerJoin(deliverySlotInfo, eq(productSlots.slotId, deliverySlotInfo.id))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(productSlots.productId, productId),
|
||||||
|
eq(deliverySlotInfo.isActive, true),
|
||||||
|
eq(deliverySlotInfo.isCapacityFull, false),
|
||||||
|
gt(deliverySlotInfo.deliveryTime, sql`NOW()`)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.orderBy(deliverySlotInfo.deliveryTime)
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
return result[0]?.deliveryTime || null
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { db } from '../db/db_index'
|
||||||
import { productInfo, storeInfo, units } from '../db/schema'
|
import { productInfo, storeInfo, units } from '../db/schema'
|
||||||
import { and, eq, sql } from 'drizzle-orm'
|
import { and, eq, sql } from 'drizzle-orm'
|
||||||
import type { InferSelectModel } from 'drizzle-orm'
|
import type { InferSelectModel } from 'drizzle-orm'
|
||||||
import type { UserStoreDetailData, UserStoreProductData, UserStoreSummaryData } from '@packages/shared'
|
import type { UserStoreDetailData, UserStoreProductData, UserStoreSummaryData, StoreSummary } from '@packages/shared'
|
||||||
|
|
||||||
type StoreRow = InferSelectModel<typeof storeInfo>
|
type StoreRow = InferSelectModel<typeof storeInfo>
|
||||||
type StoreProductRow = {
|
type StoreProductRow = {
|
||||||
|
|
@ -125,3 +125,17 @@ export async function getStoreDetail(storeId: number): Promise<UserStoreDetailDa
|
||||||
products,
|
products,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get simple store summary (id, name, description only)
|
||||||
|
* Used for common API endpoints
|
||||||
|
*/
|
||||||
|
export async function getStoresSummary(): Promise<StoreSummary[]> {
|
||||||
|
return db.query.storeInfo.findMany({
|
||||||
|
columns: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
description: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,3 +3,4 @@
|
||||||
|
|
||||||
export type * from './admin';
|
export type * from './admin';
|
||||||
export type * from './user';
|
export type * from './user';
|
||||||
|
export type * from './store.types';
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,22 @@
|
||||||
/**
|
/**
|
||||||
* Store Types
|
* Store Types
|
||||||
* Central type definitions for store-related data structures
|
* Central type definitions for store-related data structures
|
||||||
|
* Note: Store interface is defined in admin.ts to avoid duplication
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface Store {
|
/**
|
||||||
|
* Store summary for dropdowns/forms
|
||||||
|
* Minimal data for store selection UI
|
||||||
|
*/
|
||||||
|
export interface StoreSummary {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
imageUrl: string | null;
|
}
|
||||||
owner: number;
|
|
||||||
createdAt: Date;
|
/**
|
||||||
updatedAt: Date;
|
* Response type for getStoresSummary endpoint
|
||||||
|
*/
|
||||||
|
export interface StoresSummaryResponse {
|
||||||
|
stores: StoreSummary[];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue