This commit is contained in:
shafi54 2026-03-22 20:20:18 +05:30
parent cd5ab79f44
commit 56b606ebcf
156 changed files with 19095 additions and 4311 deletions

File diff suppressed because one or more lines are too long

View file

@ -227,7 +227,6 @@ export default function Layout() {
<Drawer.Screen name="slots" options={{ title: "Slots" }} />
<Drawer.Screen name="vendor-snippets" options={{ title: "Vendor Snippets" }} />
<Drawer.Screen name="stores" options={{ title: "Stores" }} />
<Drawer.Screen name="address-management" options={{ title: "Address Management" }} />
<Drawer.Screen name="product-tags" options={{ title: "Product Tags" }} />
<Drawer.Screen name="rebalance-orders" options={{ title: "Rebalance Orders" }} />
<Drawer.Screen name="user-management" options={{ title: "User Management" }} />

View file

@ -1,108 +0,0 @@
import React, { useState } from 'react'
import { View, Text, TouchableOpacity, ScrollView } from 'react-native'
import { BottomDialog , tw } from 'common-ui'
import { trpc } from '@/src/trpc-client'
import AddressZoneForm from '@/components/AddressZoneForm'
import AddressPlaceForm from '@/components/AddressPlaceForm'
import MaterialIcons from '@expo/vector-icons/MaterialIcons'
const AddressManagement: React.FC = () => {
const [dialogOpen, setDialogOpen] = useState(false)
const [dialogType, setDialogType] = useState<'zone' | 'place' | null>(null)
const [expandedZones, setExpandedZones] = useState<Set<number>>(new Set())
const { data: zones, refetch: refetchZones } = trpc.admin.address.getZones.useQuery()
const { data: areas, refetch: refetchAreas } = trpc.admin.address.getAreas.useQuery()
const createZone = trpc.admin.address.createZone.useMutation({
onSuccess: () => {
refetchZones()
setDialogOpen(false)
},
})
const createArea = trpc.admin.address.createArea.useMutation({
onSuccess: () => {
refetchAreas()
setDialogOpen(false)
},
})
const handleAddZone = () => {
setDialogType('zone')
setDialogOpen(true)
}
const handleAddPlace = () => {
setDialogType('place')
setDialogOpen(true)
}
const toggleZone = (zoneId: number) => {
setExpandedZones(prev => {
const newSet = new Set(prev)
if (newSet.has(zoneId)) {
newSet.delete(zoneId)
} else {
newSet.add(zoneId)
}
return newSet
})
}
const groupedAreas = areas?.reduce((acc, area) => {
if (area.zoneId) {
if (!acc[area.zoneId]) acc[area.zoneId] = []
acc[area.zoneId].push(area)
}
return acc
}, {} as Record<number, typeof areas[0][]>) || {}
const unzonedAreas = areas?.filter(a => !a.zoneId) || []
return (
<View style={tw`flex-1 bg-white`}>
<View style={tw`flex-row justify-between p-4`}>
<TouchableOpacity style={tw`bg-blue1 px-4 py-2 rounded`} onPress={handleAddZone}>
<Text style={tw`text-white`}>Add Zone</Text>
</TouchableOpacity>
<TouchableOpacity style={tw`bg-green1 px-4 py-2 rounded`} onPress={handleAddPlace}>
<Text style={tw`text-white`}>Add Place</Text>
</TouchableOpacity>
</View>
<ScrollView style={tw`flex-1 p-4`}>
{zones?.map(zone => (
<View key={zone.id} style={tw`mb-4 border border-gray-300 rounded`}>
<TouchableOpacity style={tw`flex-row items-center p-3 bg-gray-100`} onPress={() => toggleZone(zone.id)}>
<Text style={tw`flex-1 text-lg font-semibold`}>{zone.zoneName}</Text>
<MaterialIcons name={expandedZones.has(zone.id) ? 'expand-less' : 'expand-more'} size={24} />
</TouchableOpacity>
{expandedZones.has(zone.id) && (
<View style={tw`p-3`}>
{groupedAreas[zone.id]?.map(area => (
<Text key={area.id} style={tw`text-base mb-1`}>- {area.placeName}</Text>
)) || <Text style={tw`text-gray-500`}>No places in this zone</Text>}
</View>
)}
</View>
))}
<View style={tw`mt-6`}>
<Text style={tw`text-xl font-bold mb-2`}>Unzoned Places</Text>
{unzonedAreas.map(area => (
<Text key={area.id} style={tw`text-base mb-1`}>- {area.placeName}</Text>
))}
{unzonedAreas.length === 0 && <Text style={tw`text-gray-500`}>No unzoned places</Text>}
</View>
</ScrollView>
<BottomDialog open={dialogOpen} onClose={() => setDialogOpen(false)}>
{dialogType === 'zone' && <AddressZoneForm onSubmit={createZone.mutate} onClose={() => setDialogOpen(false)} />}
{dialogType === 'place' && <AddressPlaceForm onSubmit={createArea.mutate} onClose={() => setDialogOpen(false)} />}
</BottomDialog>
</View>
)
}
export default AddressManagement

View file

@ -184,15 +184,6 @@ export default function Dashboard() {
category: 'marketing',
iconColor: '#F97316',
iconBg: '#FFEDD5',
},
{
title: 'Address Management',
icon: 'location-on',
description: 'Manage service areas',
route: '/(drawer)/address-management',
category: 'settings',
iconColor: '#EAB308',
iconBg: '#FEF9C3',
},
{
title: 'App Constants',

View file

@ -1,64 +0,0 @@
import React from 'react'
import { Formik } from 'formik'
import * as Yup from 'yup'
import { View, Text, TouchableOpacity } from 'react-native'
import { MyTextInput, BottomDropdown, tw } from 'common-ui'
import { trpc } from '@/src/trpc-client'
interface AddressPlaceFormProps {
onSubmit: (values: { placeName: string; zoneId: number | null }) => void
onClose: () => void
}
const AddressPlaceForm: React.FC<AddressPlaceFormProps> = ({ onSubmit, onClose }) => {
const { data: zones } = trpc.admin.address.getZones.useQuery()
const validationSchema = Yup.object({
placeName: Yup.string().required('Place name is required'),
zoneId: Yup.number().optional(),
})
const zoneOptions = zones?.map(z => ({ label: z.zoneName, value: z.id })) || []
return (
<View style={tw`p-4`}>
<Text style={tw`text-lg font-semibold mb-4`}>Add Place</Text>
<Formik
initialValues={{ placeName: '', zoneId: null as number | null }}
validationSchema={validationSchema}
onSubmit={(values) => {
onSubmit(values)
onClose()
}}
>
{({ handleChange, setFieldValue, handleSubmit, values, errors, touched }) => (
<View>
<MyTextInput
label="Place Name"
value={values.placeName}
onChangeText={handleChange('placeName')}
error={!!(touched.placeName && errors.placeName)}
/>
<BottomDropdown
label="Zone (Optional)"
value={values.zoneId as any}
options={zoneOptions}
onValueChange={(value) => setFieldValue('zoneId', value as number | undefined)}
placeholder="Select Zone"
/>
<View style={tw`flex-row justify-between mt-4`}>
<TouchableOpacity style={tw`bg-gray2 px-4 py-2 rounded`} onPress={onClose}>
<Text style={tw`text-gray-900`}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity style={tw`bg-blue1 px-4 py-2 rounded`} onPress={() => handleSubmit()}>
<Text style={tw`text-white`}>Create</Text>
</TouchableOpacity>
</View>
</View>
)}
</Formik>
</View>
)
}
export default AddressPlaceForm

View file

@ -1,51 +0,0 @@
import React from 'react'
import { Formik } from 'formik'
import * as Yup from 'yup'
import { View, Text, TouchableOpacity } from 'react-native'
import { MyTextInput, tw } from 'common-ui'
interface AddressZoneFormProps {
onSubmit: (values: { zoneName: string }) => void
onClose: () => void
}
const AddressZoneForm: React.FC<AddressZoneFormProps> = ({ onSubmit, onClose }) => {
const validationSchema = Yup.object({
zoneName: Yup.string().required('Zone name is required'),
})
return (
<View style={tw`p-4`}>
<Text style={tw`text-lg font-semibold mb-4`}>Add Zone</Text>
<Formik
initialValues={{ zoneName: '' }}
validationSchema={validationSchema}
onSubmit={(values) => {
onSubmit(values)
onClose()
}}
>
{({ handleChange, handleSubmit, values, errors, touched }) => (
<View>
<MyTextInput
label="Zone Name"
value={values.zoneName}
onChangeText={handleChange('zoneName')}
error={!!(touched.zoneName && errors.zoneName)}
/>
<View style={tw`flex-row justify-between mt-4`}>
<TouchableOpacity style={tw`bg-gray2 px-4 py-2 rounded`} onPress={onClose}>
<Text style={tw`text-gray-900`}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity style={tw`bg-blue1 px-4 py-2 rounded`} onPress={() => handleSubmit()}>
<Text style={tw`text-white`}>Create</Text>
</TouchableOpacity>
</View>
</View>
)}
</Formik>
</View>
)
}
export default AddressZoneForm

View file

@ -19,7 +19,6 @@ import { startAutomatedJobs } from '@/src/lib/automatedJobs'
seed()
initFunc()
startAutomatedJobs()
signedUrlCache.loadFromDisk()
const app = new Hono()

View file

@ -40,7 +40,6 @@
"jose": "^5.10.0",
"node-cron": "^4.2.1",
"pg": "^8.16.3",
"razorpay": "^2.9.6",
"redis": "^5.9.0",
"zod": "^4.1.12"
},

View file

@ -1,11 +0,0 @@
import { Router } from "express";
import { authenticateStaff } from "@/src/middleware/staff-auth";
const router = Router();
// Apply staff authentication to all admin routes
router.use(authenticateStaff);
const avRouter = router;
export default avRouter;

View file

@ -1,5 +1,5 @@
import { eq, gt, and, sql, inArray } from "drizzle-orm";
import { Request, Response } from "express";
import { Context } from "hono";
import { db } from "@/src/db/db_index"
import { productInfo, units, productSlots, deliverySlotInfo, productTags } from "@/src/db/schema"
import { scaffoldAssetUrl } from "@/src/lib/s3-client"
@ -29,10 +29,10 @@ const getNextDeliveryDate = async (productId: number): Promise<Date | null> => {
/**
* Get all products summary for dropdown
*/
export const getAllProductsSummary = async (req: Request, res: Response) => {
export const getAllProductsSummary = async (c: Context) => {
try {
const { tagId } = req.query;
const tagIdNum = tagId ? parseInt(tagId as string) : null;
const tagId = c.req.query('tagId');
const tagIdNum = tagId ? parseInt(tagId) : null;
let productIds: number[] | null = null;
@ -53,7 +53,7 @@ export const getAllProductsSummary = async (req: Request, res: Response) => {
whereCondition = inArray(productInfo.id, productIds);
} else if (tagIdNum) {
// If tagId was provided but no products found, return empty array
return res.status(200).json({
return c.json({
products: [],
count: 0,
});
@ -94,12 +94,12 @@ export const getAllProductsSummary = async (req: Request, res: Response) => {
})
);
return res.status(200).json({
return c.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" });
return c.json({ error: "Failed to fetch products summary" }, 500);
}
};

View file

@ -1,10 +1,9 @@
import { Router } from "express";
import { getAllProductsSummary } from "@/src/apis/common-apis/apis/common-product.controller"
import { Hono } from 'hono'
import { getAllProductsSummary } from '@/src/apis/common-apis/apis/common-product.controller'
const router = Router();
const app = new Hono()
router.get("/summary", getAllProductsSummary);
// GET /summary - Get all products summary
app.get('/summary', getAllProductsSummary)
const commonProductsRouter= router;
export default commonProductsRouter;
export default app

View file

@ -1,10 +1,9 @@
import { Router } from "express";
import commonProductsRouter from "@/src/apis/common-apis/apis/common-product.router"
import { Hono } from 'hono'
import commonProductsRouter from '@/src/apis/common-apis/apis/common-product.router'
const router = Router();
const app = new Hono()
router.use('/products', commonProductsRouter)
// Mount product routes at /products
app.route('/products', commonProductsRouter)
const commonRouter = router;
export default commonRouter;
export default app

View file

@ -1,17 +1,9 @@
import * as cron from 'node-cron';
import { checkPendingPayments, checkRefundStatuses } from '@/src/jobs/payment-status-checker'
const runCombinedJob = async () => {
const start = Date.now();
try {
console.log('Starting combined job: payments and refunds check');
// Run payment check
// await checkPendingPayments();
// Run refund check
// await checkRefundStatuses();
console.log('Starting combined job');
console.log('Combined job completed successfully');
} catch (error) {
console.error('Error in combined job:', error);

View file

@ -1,79 +0,0 @@
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';
import { RazorpayPaymentService } from '@/src/lib/payments-utils'
interface PendingPaymentRecord {
payment: typeof payments.$inferSelect;
order: typeof orders.$inferSelect;
slot: typeof deliverySlotInfo.$inferSelect;
}
export const createPaymentNotification = (record: PendingPaymentRecord) => {
// Construct message from record data
const message = `Payment pending for order ORD${record.order.id}. Please complete before orders close time.`;
// TODO: Implement notification sending logic using record.order.userId, record.order.id, message
console.log(`Sending notification to user ${record.order.userId} for order ${record.order.id}: ${message}`);
};
export const checkRefundStatuses = async () => {
try {
const initiatedRefunds = await db
.select()
.from(refunds)
.where(and(
eq(refunds.refundStatus, 'initiated'),
isNotNull(refunds.merchantRefundId)
));
// Process refunds concurrently using Promise.allSettled
const promises = initiatedRefunds.map(async (refund) => {
if (!refund.merchantRefundId) return;
try {
const razorpayRefund = await RazorpayPaymentService.fetchRefund(refund.merchantRefundId);
if (razorpayRefund.status === 'processed') {
await db
.update(refunds)
.set({ refundStatus: 'success', refundProcessedAt: new Date() })
.where(eq(refunds.id, refund.id));
}
} catch (error) {
console.error(`Error checking refund ${refund.id}:`, error);
}
});
// Wait for all promises to complete
await Promise.allSettled(promises);
} catch (error) {
console.error('Error in checkRefundStatuses:', error);
}
};
export const checkPendingPayments = async () => {
try {
const pendingPayments = await db
.select({
payment: payments,
order: orders,
slot: deliverySlotInfo,
})
.from(payments)
.innerJoin(orders, eq(payments.orderId, orders.id))
.innerJoin(deliverySlotInfo, eq(orders.slotId, deliverySlotInfo.id))
.where(and(
eq(payments.status, 'pending'),
gt(deliverySlotInfo.freezeTime, new Date()) // Freeze time not passed
));
for (const record of pendingPayments) {
createPaymentNotification(record);
}
} catch (error) {
console.error('Error checking pending payments:', error);
}
};

View file

@ -1,6 +0,0 @@
import express from 'express';
const catchAsync =
(fn: express.RequestHandler) =>
(req: express.Request, res: express.Response, next: express.NextFunction) =>
Promise.resolve(fn(req, res, next)).catch(next);
export default catchAsync;

View file

@ -1,59 +0,0 @@
import Razorpay from "razorpay";
import { razorpayId, razorpaySecret } from "@/src/lib/env-exporter"
import { db } from "@/src/db/db_index"
import { payments } from "@/src/db/schema"
type Tx = Parameters<Parameters<typeof db.transaction>[0]>[0];
export class RazorpayPaymentService {
private static instance = new Razorpay({
key_id: razorpayId,
key_secret: razorpaySecret,
});
static async createOrder(orderId: number, amount: string) {
// Create Razorpay order
const razorpayOrder = await this.instance.orders.create({
amount: parseFloat(amount) * 100, // Convert to paisa
currency: 'INR',
receipt: `order_${orderId}`,
notes: {
customerOrderId: orderId.toString(),
},
});
return razorpayOrder;
}
static async insertPaymentRecord(orderId: number, razorpayOrder: any, tx?: Tx) {
// Use transaction if provided, otherwise use db
const dbInstance = tx || db;
// Insert payment record
const [payment] = await dbInstance
.insert(payments)
.values({
status: 'pending',
gateway: 'razorpay',
orderId,
token: orderId.toString(),
merchantOrderId: razorpayOrder.id,
payload: razorpayOrder,
})
.returning();
return payment;
}
static async initiateRefund(paymentId: string, amount: number) {
const refund = await this.instance.payments.refund(paymentId, {
amount,
});
return refund;
}
static async fetchRefund(refundId: string) {
const refund = await this.instance.refunds.fetch(refundId);
return refund;
}
}

132
apps/backend/src/lib/signed-url-cache.ts Executable file → Normal file
View file

@ -1,8 +1,3 @@
import fs from 'fs';
import path from 'path';
const CACHE_FILE_PATH = path.join('.', 'assets', 'signed-url-cache.json');
// Interface for cache entries with TTL
interface CacheEntry {
value: string;
@ -16,18 +11,7 @@ class SignedURLCache {
constructor() {
this.originalToSignedCache = new Map();
this.signedToOriginalCache = new Map();
// Create cache directory if it doesn't exist
const cacheDir = path.dirname(CACHE_FILE_PATH);
if (!fs.existsSync(cacheDir)) {
console.log('creating the directory')
fs.mkdirSync(cacheDir, { recursive: true });
}
else {
console.log('the directory is already present')
}
console.log('SignedURLCache: Initialized (in-memory only)');
}
/**
@ -110,7 +94,7 @@ class SignedURLCache {
clear(): void {
this.originalToSignedCache.clear();
this.signedToOriginalCache.clear();
this.saveToDisk();
console.log('SignedURLCache: Cleared all entries');
}
/**
@ -145,119 +129,27 @@ class SignedURLCache {
}
/**
* Save the cache to disk
* Get cache statistics
*/
saveToDisk(): void {
try {
// Remove expired entries before saving
const removedCount = this.clearExpired();
// Convert Maps to serializable objects
const serializedOriginalToSigned: Record<string, { value: string; expiresAt: number }> = {};
const serializedSignedToOriginal: Record<string, { value: string; expiresAt: number }> = {};
for (const [originalUrl, entry] of this.originalToSignedCache.entries()) {
serializedOriginalToSigned[originalUrl] = {
value: entry.value,
expiresAt: entry.expiresAt
getStats(): { totalEntries: number } {
return {
totalEntries: this.originalToSignedCache.size
};
}
for (const [signedUrl, entry] of this.signedToOriginalCache.entries()) {
serializedSignedToOriginal[signedUrl] = {
value: entry.value,
expiresAt: entry.expiresAt
};
}
const serializedCache = {
originalToSigned: serializedOriginalToSigned,
signedToOriginal: serializedSignedToOriginal
};
// Write to file
fs.writeFileSync(
CACHE_FILE_PATH,
JSON.stringify(serializedCache),
'utf8'
);
console.log(`SignedURLCache: Saved ${this.originalToSignedCache.size} entries to disk`);
} catch (error) {
console.error('Error saving SignedURLCache to disk:', error);
}
}
/**
* Load the cache from disk
* Stub methods for backward compatibility - do nothing in in-memory mode
*/
saveToDisk(): void {
// No-op: In-memory cache only
}
loadFromDisk(): void {
try {
if (fs.existsSync(CACHE_FILE_PATH)) {
// Read from file
const data = fs.readFileSync(CACHE_FILE_PATH, 'utf8');
// Parse the data
const parsedData = JSON.parse(data) as {
originalToSigned: Record<string, { value: string; expiresAt: number }>,
signedToOriginal: Record<string, { value: string; expiresAt: number }>
};
// Only load entries that haven't expired yet
const now = Date.now();
let loadedCount = 0;
let expiredCount = 0;
// Load original to signed mappings
if (parsedData.originalToSigned) {
for (const [originalUrl, entry] of Object.entries(parsedData.originalToSigned)) {
if (now <= entry.expiresAt) {
this.originalToSignedCache.set(originalUrl, entry);
loadedCount++;
} else {
expiredCount++;
}
}
}
// Load signed to original mappings
if (parsedData.signedToOriginal) {
for (const [signedUrl, entry] of Object.entries(parsedData.signedToOriginal)) {
if (now <= entry.expiresAt) {
this.signedToOriginalCache.set(signedUrl, entry);
// Don't increment loadedCount as these are pairs of what we already counted
} else {
// Don't increment expiredCount as these are pairs of what we already counted
}
}
}
console.log(`SignedURLCache: Loaded ${loadedCount} valid entries from disk (skipped ${expiredCount} expired entries)`);
} else {
console.log('SignedURLCache: No cache file found, starting with empty cache');
}
} catch (error) {
console.error('Error loading SignedURLCache from disk:', error);
// Start with empty caches if loading fails
this.originalToSignedCache = new Map();
this.signedToOriginalCache = new Map();
}
// No-op: In-memory cache only
}
}
// Create a singleton instance to be used throughout the application
const signedUrlCache = new SignedURLCache();
process.on('SIGINT', () => {
console.log('SignedURLCache: Saving cache before shutdown...');
signedUrlCache.saveToDisk();
process.exit(0);
});
process.on('SIGTERM', () => {
console.log('SignedURLCache: Saving cache before shutdown...');
signedUrlCache.saveToDisk();
process.exit(0);
});
export default signedUrlCache;

View file

@ -0,0 +1,263 @@
import fs from 'fs';
import path from 'path';
const CACHE_FILE_PATH = path.join('.', 'assets', 'signed-url-cache.json');
// Interface for cache entries with TTL
interface CacheEntry {
value: string;
expiresAt: number; // Timestamp when this entry expires
}
class SignedURLCache {
private originalToSignedCache: Map<string, CacheEntry>;
private signedToOriginalCache: Map<string, CacheEntry>;
constructor() {
this.originalToSignedCache = new Map();
this.signedToOriginalCache = new Map();
// Create cache directory if it doesn't exist
const cacheDir = path.dirname(CACHE_FILE_PATH);
if (!fs.existsSync(cacheDir)) {
console.log('creating the directory')
fs.mkdirSync(cacheDir, { recursive: true });
}
else {
console.log('the directory is already present')
}
}
/**
* Get a signed URL from the cache using an original URL as the key
*/
get(originalUrl: string): string | undefined {
const entry = this.originalToSignedCache.get(originalUrl);
// If no entry or entry has expired, return undefined
if (!entry || Date.now() > entry.expiresAt) {
if (entry) {
// Remove expired entry
this.originalToSignedCache.delete(originalUrl);
// Also remove from reverse mapping if it exists
this.signedToOriginalCache.delete(entry.value);
}
return undefined;
}
return entry.value;
}
/**
* Get the original URL from the cache using a signed URL as the key
*/
getOriginalUrl(signedUrl: string): string | undefined {
const entry = this.signedToOriginalCache.get(signedUrl);
// If no entry or entry has expired, return undefined
if (!entry || Date.now() > entry.expiresAt) {
if (entry) {
// Remove expired entry
this.signedToOriginalCache.delete(signedUrl);
// Also remove from primary mapping if it exists
this.originalToSignedCache.delete(entry.value);
}
return undefined;
}
return entry.value;
}
/**
* Set a value in the cache with a TTL (Time To Live)
* @param originalUrl The original S3 URL
* @param signedUrl The signed URL
* @param ttlMs Time to live in milliseconds (default: 3 days)
*/
set(originalUrl: string, signedUrl: string, ttlMs: number = 259200000): void {
const expiresAt = Date.now() + ttlMs;
const entry: CacheEntry = {
value: signedUrl,
expiresAt
};
const reverseEntry: CacheEntry = {
value: originalUrl,
expiresAt
};
this.originalToSignedCache.set(originalUrl, entry);
this.signedToOriginalCache.set(signedUrl, reverseEntry);
}
has(originalUrl: string): boolean {
const entry = this.originalToSignedCache.get(originalUrl);
// Entry exists and hasn't expired
return !!entry && Date.now() <= entry.expiresAt;
}
hasSignedUrl(signedUrl: string): boolean {
const entry = this.signedToOriginalCache.get(signedUrl);
// Entry exists and hasn't expired
return !!entry && Date.now() <= entry.expiresAt;
}
clear(): void {
this.originalToSignedCache.clear();
this.signedToOriginalCache.clear();
this.saveToDisk();
}
/**
* Remove all expired entries from the cache
* @returns The number of expired entries that were removed
*/
clearExpired(): number {
const now = Date.now();
let removedCount = 0;
// Clear expired entries from original to signed cache
for (const [originalUrl, entry] of this.originalToSignedCache.entries()) {
if (now > entry.expiresAt) {
this.originalToSignedCache.delete(originalUrl);
removedCount++;
}
}
// Clear expired entries from signed to original cache
for (const [signedUrl, entry] of this.signedToOriginalCache.entries()) {
if (now > entry.expiresAt) {
this.signedToOriginalCache.delete(signedUrl);
// No need to increment removedCount as we've already counted these in the first loop
}
}
if (removedCount > 0) {
console.log(`SignedURLCache: Cleared ${removedCount} expired entries`);
}
return removedCount;
}
/**
* Save the cache to disk
*/
saveToDisk(): void {
try {
// Remove expired entries before saving
const removedCount = this.clearExpired();
// Convert Maps to serializable objects
const serializedOriginalToSigned: Record<string, { value: string; expiresAt: number }> = {};
const serializedSignedToOriginal: Record<string, { value: string; expiresAt: number }> = {};
for (const [originalUrl, entry] of this.originalToSignedCache.entries()) {
serializedOriginalToSigned[originalUrl] = {
value: entry.value,
expiresAt: entry.expiresAt
};
}
for (const [signedUrl, entry] of this.signedToOriginalCache.entries()) {
serializedSignedToOriginal[signedUrl] = {
value: entry.value,
expiresAt: entry.expiresAt
};
}
const serializedCache = {
originalToSigned: serializedOriginalToSigned,
signedToOriginal: serializedSignedToOriginal
};
// Write to file
fs.writeFileSync(
CACHE_FILE_PATH,
JSON.stringify(serializedCache),
'utf8'
);
console.log(`SignedURLCache: Saved ${this.originalToSignedCache.size} entries to disk`);
} catch (error) {
console.error('Error saving SignedURLCache to disk:', error);
}
}
/**
* Load the cache from disk
*/
loadFromDisk(): void {
try {
if (fs.existsSync(CACHE_FILE_PATH)) {
// Read from file
const data = fs.readFileSync(CACHE_FILE_PATH, 'utf8');
// Parse the data
const parsedData = JSON.parse(data) as {
originalToSigned: Record<string, { value: string; expiresAt: number }>,
signedToOriginal: Record<string, { value: string; expiresAt: number }>
};
// Only load entries that haven't expired yet
const now = Date.now();
let loadedCount = 0;
let expiredCount = 0;
// Load original to signed mappings
if (parsedData.originalToSigned) {
for (const [originalUrl, entry] of Object.entries(parsedData.originalToSigned)) {
if (now <= entry.expiresAt) {
this.originalToSignedCache.set(originalUrl, entry);
loadedCount++;
} else {
expiredCount++;
}
}
}
// Load signed to original mappings
if (parsedData.signedToOriginal) {
for (const [signedUrl, entry] of Object.entries(parsedData.signedToOriginal)) {
if (now <= entry.expiresAt) {
this.signedToOriginalCache.set(signedUrl, entry);
// Don't increment loadedCount as these are pairs of what we already counted
} else {
// Don't increment expiredCount as these are pairs of what we already counted
}
}
}
console.log(`SignedURLCache: Loaded ${loadedCount} valid entries from disk (skipped ${expiredCount} expired entries)`);
} else {
console.log('SignedURLCache: No cache file found, starting with empty cache');
}
} catch (error) {
console.error('Error loading SignedURLCache from disk:', error);
// Start with empty caches if loading fails
this.originalToSignedCache = new Map();
this.signedToOriginalCache = new Map();
}
}
}
// Create a singleton instance to be used throughout the application
const signedUrlCache = new SignedURLCache();
process.on('SIGINT', () => {
console.log('SignedURLCache: Saving cache before shutdown...');
signedUrlCache.saveToDisk();
process.exit(0);
});
process.on('SIGTERM', () => {
console.log('SignedURLCache: Saving cache before shutdown...');
signedUrlCache.saveToDisk();
process.exit(0);
});
export default signedUrlCache;

View file

@ -1,5 +1,6 @@
import { Hono } from 'hono'
import { authenticateUser } from '@/src/middleware/auth.middleware'
import v1Router from '@/src/v1-router'
// Note: This router is kept for compatibility during migration
// Most routes have been moved to tRPC
@ -24,10 +25,10 @@ router.get('/seed', (c) => {
})
})
// Mount v1 routes (REST API)
router.route('/v1', v1Router)
// Apply authentication middleware to all subsequent routes
router.use('*', authenticateUser)
// Legacy routes - most functionality moved to tRPC
// router.route('/v1', v1Router) // Uncomment if needed during transition
export default router

View file

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

View file

@ -1,13 +0,0 @@
import { Router, Request, Response } from 'express';
const router = Router();
router.get('/', (req: Request, res: Response) => {
res.json({
status: 'ok',
message: 'Health check passed',
timestamp: new Date().toISOString(),
});
});
export default router;

View file

@ -2,7 +2,6 @@
import { router } from '@/src/trpc/trpc-index'
import { complaintRouter } from '@/src/trpc/apis/admin-apis/apis/complaint'
import { couponRouter } from '@/src/trpc/apis/admin-apis/apis/coupon'
import { cancelledOrdersRouter } from '@/src/trpc/apis/admin-apis/apis/cancelled-orders'
import { orderRouter } from '@/src/trpc/apis/admin-apis/apis/order'
import { vendorSnippetsRouter } from '@/src/trpc/apis/admin-apis/apis/vendor-snippets'
import { slotsRouter } from '@/src/trpc/apis/admin-apis/apis/slots'
@ -10,7 +9,6 @@ import { productRouter } from '@/src/trpc/apis/admin-apis/apis/product'
import { staffUserRouter } from '@/src/trpc/apis/admin-apis/apis/staff-user'
import { storeRouter } from '@/src/trpc/apis/admin-apis/apis/store'
import { adminPaymentsRouter } from '@/src/trpc/apis/admin-apis/apis/payments'
import addressRouter from '@/src/trpc/apis/admin-apis/apis/address'
import { bannerRouter } from '@/src/trpc/apis/admin-apis/apis/banner'
import { userRouter } from '@/src/trpc/apis/admin-apis/apis/user'
import { constRouter } from '@/src/trpc/apis/admin-apis/apis/const'
@ -20,7 +18,6 @@ import { tagRouter } from '@/src/trpc/apis/admin-apis/apis/tag'
export const adminRouter = router({
complaint: complaintRouter,
coupon: couponRouter,
cancelledOrders: cancelledOrdersRouter,
order: orderRouter,
vendorSnippets: vendorSnippetsRouter,
slots: slotsRouter,
@ -28,7 +25,6 @@ export const adminRouter = router({
staffUser: staffUserRouter,
store: storeRouter,
payments: adminPaymentsRouter,
address: addressRouter,
banner: bannerRouter,
user: userRouter,
const: constRouter,

View file

@ -1,23 +1,16 @@
import { z } from 'zod';
import { db } from '@/src/db/db_index'
import { homeBanners } from '@/src/db/schema'
import { eq, and, desc, sql } from 'drizzle-orm';
import { protectedProcedure, router } from '@/src/trpc/trpc-index'
import { extractKeyFromPresignedUrl, scaffoldAssetUrl } from '@/src/lib/s3-client'
import { scaffoldAssetUrl, extractKeyFromPresignedUrl } from '@/src/lib/s3-client'
import { ApiError } from '@/src/lib/api-error';
import { scheduleStoreInitialization } from '@/src/stores/store-initializer'
import { bannerDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/main'
export const bannerRouter = router({
// Get all banners
getBanners: protectedProcedure
.query(async () => {
try {
const banners = await db.query.homeBanners.findMany({
orderBy: desc(homeBanners.createdAt), // Order by creation date instead
// Removed product relationship since we now use productIds array
});
const banners = await bannerDbService.getAllBanners()
// Convert S3 keys to signed URLs for client
const bannersWithSignedUrls = await Promise.all(
@ -26,15 +19,13 @@ export const bannerRouter = router({
return {
...banner,
imageUrl: banner.imageUrl ? scaffoldAssetUrl(banner.imageUrl) : banner.imageUrl,
// Ensure productIds is always an array
productIds: banner.productIds || [],
};
} catch (error) {
console.error(`Failed to generate signed URL for banner ${banner.id}:`, error);
return {
...banner,
imageUrl: banner.imageUrl, // Keep original on error
// Ensure productIds is always an array
imageUrl: banner.imageUrl,
productIds: banner.productIds || [],
};
}
@ -44,10 +35,8 @@ export const bannerRouter = router({
return {
banners: bannersWithSignedUrls,
};
}
catch(e:any) {
} catch (e: any) {
console.log(e)
throw new ApiError(e.message);
}
}),
@ -56,23 +45,17 @@ export const bannerRouter = router({
getBanner: protectedProcedure
.input(z.object({ id: z.number() }))
.query(async ({ input }) => {
const banner = await db.query.homeBanners.findFirst({
where: eq(homeBanners.id, input.id),
// Removed product relationship since we now use productIds array
});
const banner = await bannerDbService.getBannerById(input.id)
if (banner) {
try {
// Convert S3 key to signed URL for client
if (banner.imageUrl) {
banner.imageUrl = scaffoldAssetUrl(banner.imageUrl);
}
} catch (error) {
console.error(`Failed to generate signed URL for banner ${banner.id}:`, error);
// Keep original imageUrl on error
}
// Ensure productIds is always an array (handle migration compatibility)
if (!banner.productIds) {
banner.productIds = [];
}
@ -89,29 +72,27 @@ export const bannerRouter = router({
description: z.string().optional(),
productIds: z.array(z.number()).optional(),
redirectUrl: z.string().url().optional(),
// serialNum removed completely
}))
.mutation(async ({ input }) => {
try {
const imageUrl = extractKeyFromPresignedUrl(input.imageUrl)
// const imageUrl = input.imageUrl
const [banner] = await db.insert(homeBanners).values({
const banner = await bannerDbService.createBanner({
name: input.name,
imageUrl: imageUrl,
description: input.description,
productIds: input.productIds || [],
redirectUrl: input.redirectUrl,
serialNum: 999, // Default value, not used
isActive: false, // Default to inactive
}).returning();
serialNum: 999,
isActive: false,
})
// Reinitialize stores to reflect changes
scheduleStoreInitialization()
return banner;
} catch (error) {
console.error('Error creating banner:', error);
throw error; // Re-throw to maintain tRPC error handling
throw error;
}
}),
@ -129,30 +110,20 @@ export const bannerRouter = router({
}))
.mutation(async ({ input }) => {
try {
const { id, ...updateData } = input;
const incomingProductIds = input.productIds;
// Extract S3 key from presigned URL if imageUrl is provided
const processedData = {
...updateData,
...(updateData.imageUrl && {
imageUrl: extractKeyFromPresignedUrl(updateData.imageUrl)
}),
};
// Handle serialNum null case
const finalData: any = { ...processedData };
if ('serialNum' in finalData && finalData.serialNum === null) {
// Set to null explicitly
finalData.serialNum = null;
const processedData: any = { ...updateData }
if (updateData.imageUrl) {
processedData.imageUrl = extractKeyFromPresignedUrl(updateData.imageUrl)
}
const [banner] = await db.update(homeBanners)
.set({ ...finalData, lastUpdated: new Date(), })
.where(eq(homeBanners.id, id))
.returning();
if ('serialNum' in processedData && processedData.serialNum === null) {
processedData.serialNum = null;
}
const banner = await bannerDbService.updateBannerById(id, processedData)
// Reinitialize stores to reflect changes
scheduleStoreInitialization()
return banner;
@ -166,9 +137,8 @@ export const bannerRouter = router({
deleteBanner: protectedProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input }) => {
await db.delete(homeBanners).where(eq(homeBanners.id, input.id));
await bannerDbService.deleteBannerById(input.id)
// Reinitialize stores to reflect changes
scheduleStoreInitialization()
return { success: true };

View file

@ -1,9 +1,7 @@
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod';
import { db } from '@/src/db/db_index'
import { complaints, users } from '@/src/db/schema'
import { eq, desc, lt, and } from 'drizzle-orm';
import { scaffoldAssetUrl } from '@/src/lib/s3-client'
import { complaintDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/main'
export const complaintRouter = router({
getAll: protectedProcedure
@ -14,27 +12,7 @@ export const complaintRouter = router({
.query(async ({ input }) => {
const { cursor, limit } = input;
let whereCondition = cursor
? lt(complaints.id, cursor)
: undefined;
const complaintsData = await db
.select({
id: complaints.id,
complaintBody: complaints.complaintBody,
userId: complaints.userId,
orderId: complaints.orderId,
isResolved: complaints.isResolved,
createdAt: complaints.createdAt,
userName: users.name,
userMobile: users.mobile,
images: complaints.images,
})
.from(complaints)
.leftJoin(users, eq(complaints.userId, users.id))
.where(whereCondition)
.orderBy(desc(complaints.id))
.limit(limit + 1);
const complaintsData = await complaintDbService.getComplaints(cursor, limit);
const hasMore = complaintsData.length > limit;
const complaintsToReturn = hasMore ? complaintsData.slice(0, limit) : complaintsData;
@ -70,10 +48,7 @@ export const complaintRouter = router({
resolve: protectedProcedure
.input(z.object({ id: z.string(), response: z.string().optional() }))
.mutation(async ({ input }) => {
await db
.update(complaints)
.set({ isResolved: true, response: input.response })
.where(eq(complaints.id, parseInt(input.id)));
await complaintDbService.resolveComplaint(parseInt(input.id), input.response);
return { message: 'Complaint resolved successfully' };
}),

View file

@ -1,15 +1,13 @@
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod';
import { db } from '@/src/db/db_index'
import { keyValStore } from '@/src/db/schema'
import { computeConstants } from '@/src/lib/const-store'
import { CONST_KEYS } from '@/src/lib/const-keys'
import { constantDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/main'
export const constRouter = router({
getConstants: protectedProcedure
.query(async () => {
const constants = await db.select().from(keyValStore);
const constants = await constantDbService.getAllConstants();
const resp = constants.map(c => ({
key: c.key,
@ -38,23 +36,14 @@ export const constRouter = router({
throw new Error(`Invalid constant keys: ${invalidKeys.join(', ')}`);
}
await db.transaction(async (tx) => {
for (const { key, value } of constants) {
await tx.insert(keyValStore)
.values({ key, value })
.onConflictDoUpdate({
target: keyValStore.key,
set: { value },
});
}
});
const updatedCount = await constantDbService.upsertConstants(constants);
// Refresh all constants in Redis after database update
await computeConstants();
return {
success: true,
updatedCount: constants.length,
updatedCount,
keys: constants.map(c => c.key),
};
}),

View file

@ -1,9 +1,7 @@
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod';
import { db } from '@/src/db/db_index'
import { coupons, users, staffUsers, orders, couponApplicableUsers, couponApplicableProducts, orderStatus, reservedCoupons } from '@/src/db/schema'
import { eq, and, like, or, inArray, lt } from 'drizzle-orm';
import dayjs from 'dayjs';
import { couponDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/main'
const createCouponBodySchema = z.object({
couponCode: z.string().optional(),
@ -51,10 +49,7 @@ export const couponRouter = router({
// If applicableUsers is provided, verify users exist
if (applicableUsers && applicableUsers.length > 0) {
const existingUsers = await db.query.users.findMany({
where: inArray(users.id, applicableUsers),
columns: { id: true },
});
const existingUsers = await couponDbService.getUsersByIds(applicableUsers);
if (existingUsers.length !== applicableUsers.length) {
throw new Error("Some applicable users not found");
}
@ -69,56 +64,40 @@ export const couponRouter = router({
// Generate coupon code if not provided
let finalCouponCode = couponCode;
if (!finalCouponCode) {
// Generate a unique coupon code
const timestamp = Date.now().toString().slice(-6);
const random = Math.random().toString(36).substring(2, 8).toUpperCase();
finalCouponCode = `MF${timestamp}${random}`;
}
// Check if coupon code already exists
const existingCoupon = await db.query.coupons.findFirst({
where: eq(coupons.couponCode, finalCouponCode),
});
const existingCoupon = await couponDbService.getCouponByCode(finalCouponCode);
if (existingCoupon) {
throw new Error("Coupon code already exists");
}
const result = await db.insert(coupons).values({
const coupon = await couponDbService.createCoupon({
couponCode: finalCouponCode,
isUserBased: isUserBased || false,
discountPercent: discountPercent?.toString(),
flatDiscount: flatDiscount?.toString(),
minOrder: minOrder?.toString(),
discountPercent: discountPercent?.toString() || null,
flatDiscount: flatDiscount?.toString() || null,
minOrder: minOrder?.toString() || null,
productIds: productIds || null,
createdBy: staffUserId,
maxValue: maxValue?.toString(),
maxValue: maxValue?.toString() || null,
isApplyForAll: isApplyForAll || false,
validTill: validTill ? dayjs(validTill).toDate() : undefined,
maxLimitForUser: maxLimitForUser,
validTill: validTill ? dayjs(validTill).toDate() : null,
maxLimitForUser: maxLimitForUser || null,
exclusiveApply: exclusiveApply || false,
}).returning();
const coupon = result[0];
});
// Insert applicable users
if (applicableUsers && applicableUsers.length > 0) {
await db.insert(couponApplicableUsers).values(
applicableUsers.map(userId => ({
couponId: coupon.id,
userId,
}))
);
await couponDbService.addApplicableUsers(coupon.id, applicableUsers);
}
// Insert applicable products
if (applicableProducts && applicableProducts.length > 0) {
await db.insert(couponApplicableProducts).values(
applicableProducts.map(productId => ({
couponId: coupon.id,
productId,
}))
);
await couponDbService.addApplicableProducts(coupon.id, applicableProducts);
}
return coupon;
@ -133,39 +112,7 @@ export const couponRouter = router({
.query(async ({ input }) => {
const { cursor, limit, search } = input;
let whereCondition = undefined;
const conditions = [];
if (cursor) {
conditions.push(lt(coupons.id, cursor));
}
if (search && search.trim()) {
conditions.push(like(coupons.couponCode, `%${search}%`));
}
if (conditions.length > 0) {
whereCondition = and(...conditions);
}
const result = await db.query.coupons.findMany({
where: whereCondition,
with: {
creator: true,
applicableUsers: {
with: {
user: true,
},
},
applicableProducts: {
with: {
product: true,
},
},
},
orderBy: (coupons, { desc }) => [desc(coupons.createdAt)],
limit: limit + 1,
});
const result = await couponDbService.getAllCoupons({ cursor, limit, search });
const hasMore = result.length > limit;
const couponsList = hasMore ? result.slice(0, limit) : result;
@ -177,24 +124,7 @@ export const couponRouter = router({
getById: protectedProcedure
.input(z.object({ id: z.number() }))
.query(async ({ input }) => {
const couponId = input.id;
const result = await db.query.coupons.findFirst({
where: eq(coupons.id, couponId),
with: {
creator: true,
applicableUsers: {
with: {
user: true,
},
},
applicableProducts: {
with: {
product: true,
},
},
},
});
const result = await couponDbService.getCouponById(input.id);
if (!result) {
throw new Error("Coupon not found");
@ -227,7 +157,7 @@ export const couponRouter = router({
// If updating to user-based, applicableUsers is required
if (updates.isUserBased && (!updates.applicableUsers || updates.applicableUsers.length === 0)) {
const existingCount = await db.$count(couponApplicableUsers, eq(couponApplicableUsers.couponId, id));
const existingCount = await couponDbService.countApplicableUsers(id);
if (existingCount === 0) {
throw new Error("applicableUsers is required for user-based coupons");
}
@ -235,17 +165,14 @@ export const couponRouter = router({
// If applicableUsers is provided, verify users exist
if (updates.applicableUsers && updates.applicableUsers.length > 0) {
const existingUsers = await db.query.users.findMany({
where: inArray(users.id, updates.applicableUsers),
columns: { id: true },
});
const existingUsers = await couponDbService.getUsersByIds(updates.applicableUsers);
if (existingUsers.length !== updates.applicableUsers.length) {
throw new Error("Some applicable users not found");
}
}
const updateData: any = { ...updates };
delete updateData.applicableUsers; // Remove since we use couponApplicableUsers table
delete updateData.applicableUsers;
if (updates.discountPercent !== undefined) {
updateData.discountPercent = updates.discountPercent?.toString();
}
@ -262,60 +189,31 @@ export const couponRouter = router({
updateData.validTill = updates.validTill ? dayjs(updates.validTill).toDate() : null;
}
const result = await db.update(coupons)
.set(updateData)
.where(eq(coupons.id, id))
.returning();
if (result.length === 0) {
throw new Error("Coupon not found");
}
console.log('updated coupon successfully')
const result = await couponDbService.updateCoupon(id, updateData);
// Update applicable users: delete existing and insert new
if (updates.applicableUsers !== undefined) {
await db.delete(couponApplicableUsers).where(eq(couponApplicableUsers.couponId, id));
await couponDbService.removeAllApplicableUsers(id);
if (updates.applicableUsers.length > 0) {
await db.insert(couponApplicableUsers).values(
updates.applicableUsers.map(userId => ({
couponId: id,
userId,
}))
);
await couponDbService.addApplicableUsers(id, updates.applicableUsers);
}
}
// Update applicable products: delete existing and insert new
if (updates.applicableProducts !== undefined) {
await db.delete(couponApplicableProducts).where(eq(couponApplicableProducts.couponId, id));
await couponDbService.removeAllApplicableProducts(id);
if (updates.applicableProducts.length > 0) {
await db.insert(couponApplicableProducts).values(
updates.applicableProducts.map(productId => ({
couponId: id,
productId,
}))
);
await couponDbService.addApplicableProducts(id, updates.applicableProducts);
}
}
return result[0];
return result;
}),
delete: protectedProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input }) => {
const { id } = input;
const result = await db.update(coupons)
.set({ isInvalidated: true })
.where(eq(coupons.id, id))
.returning();
if (result.length === 0) {
throw new Error("Coupon not found");
}
await couponDbService.invalidateCoupon(input.id);
return { message: "Coupon invalidated successfully" };
}),
@ -328,14 +226,9 @@ export const couponRouter = router({
return { valid: false, message: "Invalid coupon code" };
}
const coupon = await db.query.coupons.findFirst({
where: and(
eq(coupons.couponCode, code.toUpperCase()),
eq(coupons.isInvalidated, false)
),
});
const coupon = await couponDbService.getCouponByCode(code.toUpperCase());
if (!coupon) {
if (!coupon || coupon.isInvalidated) {
return { valid: false, message: "Coupon not found or invalidated" };
}
@ -383,73 +276,39 @@ export const couponRouter = router({
}),
generateCancellationCoupon: protectedProcedure
.input(
z.object({
orderId: z.number(),
})
)
.input(z.object({ orderId: z.number() }))
.mutation(async ({ input, ctx }) => {
const { orderId } = input;
// Get staff user ID from auth middleware
const staffUserId = ctx.staffUser?.id;
if (!staffUserId) {
throw new Error("Unauthorized");
}
// Find the order with user and order status information
const order = await db.query.orders.findFirst({
where: eq(orders.id, orderId),
with: {
user: true,
orderStatus: true,
},
});
const order = await couponDbService.getOrderByIdWithUserAndStatus(orderId);
if (!order) {
throw new Error("Order not found");
}
// Check if order is cancelled (check if any status entry has isCancelled: true)
// const isOrderCancelled = order.orderStatus?.some(status => status.isCancelled) || false;
// if (!isOrderCancelled) {
// throw new Error("Order is not cancelled");
// }
// // Check if payment method is COD
// if (order.isCod) {
// throw new Error("Can't generate refund coupon for CoD Order");
// }
// Verify user exists
if (!order.user) {
throw new Error("User not found for this order");
}
// Generate coupon code: first 3 letters of user name or mobile + orderId
const userNamePrefix = (order.user.name || order.user.mobile || 'USR').substring(0, 3).toUpperCase();
const couponCode = `${userNamePrefix}${orderId}`;
// Check if coupon code already exists
const existingCoupon = await db.query.coupons.findFirst({
where: eq(coupons.couponCode, couponCode),
});
const existingCoupon = await couponDbService.getCouponByCode(couponCode);
if (existingCoupon) {
throw new Error("Coupon code already exists");
}
// Get order total amount
const orderAmount = parseFloat(order.totalAmount);
// Calculate expiry date (30 days from now)
const expiryDate = new Date();
expiryDate.setDate(expiryDate.getDate() + 30);
// Create the coupon and update order status in a transaction
const coupon = await db.transaction(async (tx) => {
// Create the coupon
const result = await tx.insert(coupons).values({
const coupon = await couponDbService.withTransaction(async (tx) => {
const newCoupon = await couponDbService.createCoupon({
couponCode,
isUserBased: true,
flatDiscount: orderAmount.toString(),
@ -459,22 +318,12 @@ export const couponRouter = router({
maxLimitForUser: 1,
createdBy: staffUserId,
isApplyForAll: false,
}).returning();
const coupon = result[0];
// Insert applicable users
await tx.insert(couponApplicableUsers).values({
couponId: coupon.id,
userId: order.userId,
});
// Update order_status with refund coupon ID
await tx.update(orderStatus)
.set({ refundCouponId: coupon.id })
.where(eq(orderStatus.orderId, orderId));
await couponDbService.addApplicableUsers(newCoupon.id, [order.userId]);
await couponDbService.updateOrderStatusRefundCoupon(orderId, newCoupon.id);
return coupon;
return newCoupon;
});
return coupon;
@ -487,100 +336,52 @@ export const couponRouter = router({
search: z.string().optional(),
}))
.query(async ({ input }) => {
const { cursor, limit, search } = input;
const result = await couponDbService.getReservedCoupons(input);
let whereCondition = undefined;
const conditions = [];
if (cursor) {
conditions.push(lt(reservedCoupons.id, cursor));
}
if (search && search.trim()) {
conditions.push(or(
like(reservedCoupons.secretCode, `%${search}%`),
like(reservedCoupons.couponCode, `%${search}%`)
));
}
if (conditions.length > 0) {
whereCondition = and(...conditions);
}
const result = await db.query.reservedCoupons.findMany({
where: whereCondition,
with: {
redeemedUser: true,
creator: true,
},
orderBy: (reservedCoupons, { desc }) => [desc(reservedCoupons.createdAt)],
limit: limit + 1, // Fetch one extra to check if there's more
});
const hasMore = result.length > limit;
const coupons = hasMore ? result.slice(0, limit) : result;
const hasMore = result.length > input.limit;
const coupons = hasMore ? result.slice(0, input.limit) : result;
const nextCursor = hasMore ? result[result.length - 1].id : undefined;
return {
coupons,
nextCursor,
};
return { coupons, nextCursor };
}),
createReservedCoupon: protectedProcedure
.input(createCouponBodySchema)
.mutation(async ({ input, ctx }) => {
const { couponCode, isUserBased, discountPercent, flatDiscount, minOrder, productIds, applicableUsers, applicableProducts, maxValue, isApplyForAll, validTill, maxLimitForUser, exclusiveApply } = input;
const { couponCode, discountPercent, flatDiscount, minOrder, productIds, applicableProducts, maxValue, validTill, maxLimitForUser, exclusiveApply } = input;
// Validation: ensure at least one discount type is provided
if ((!discountPercent && !flatDiscount) || (discountPercent && flatDiscount)) {
throw new Error("Either discountPercent or flatDiscount must be provided (but not both)");
}
// For reserved coupons, applicableUsers is not used, as it's redeemed by one user
// Get staff user ID from auth middleware
const staffUserId = ctx.staffUser?.id;
if (!staffUserId) {
throw new Error("Unauthorized");
}
// Generate secret code if not provided (use couponCode as base)
let secretCode = couponCode || `SECRET${Date.now().toString().slice(-6)}${Math.random().toString(36).substring(2, 8).toUpperCase()}`;
// Check if secret code already exists
const existing = await db.query.reservedCoupons.findFirst({
where: eq(reservedCoupons.secretCode, secretCode),
});
const existing = await couponDbService.getCouponByCode(secretCode);
if (existing) {
throw new Error("Secret code already exists");
}
const result = await db.insert(reservedCoupons).values({
const coupon = await couponDbService.createReservedCoupon({
secretCode,
couponCode: couponCode || `RESERVED${Date.now().toString().slice(-6)}`,
discountPercent: discountPercent?.toString(),
flatDiscount: flatDiscount?.toString(),
minOrder: minOrder?.toString(),
productIds,
maxValue: maxValue?.toString(),
validTill: validTill ? dayjs(validTill).toDate() : undefined,
maxLimitForUser,
discountPercent: discountPercent?.toString() || null,
flatDiscount: flatDiscount?.toString() || null,
minOrder: minOrder?.toString() || null,
productIds: productIds || null,
maxValue: maxValue?.toString() || null,
validTill: validTill ? dayjs(validTill).toDate() : null,
maxLimitForUser: maxLimitForUser || null,
exclusiveApply: exclusiveApply || false,
createdBy: staffUserId,
}).returning();
});
const coupon = result[0];
// Insert applicable products if provided
if (applicableProducts && applicableProducts.length > 0) {
await db.insert(couponApplicableProducts).values(
applicableProducts.map(productId => ({
couponId: coupon.id,
productId,
}))
);
await couponDbService.addApplicableProducts(coupon.id, applicableProducts);
}
return coupon;
@ -593,27 +394,11 @@ export const couponRouter = router({
offset: z.number().min(0).default(0),
}))
.query(async ({ input }) => {
const { search, limit } = input;
const { search, limit, offset } = input;
let whereCondition = undefined;
if (search && search.trim()) {
whereCondition = or(
like(users.name, `%${search}%`),
like(users.mobile, `%${search}%`)
);
}
const userList = await db.query.users.findMany({
where: whereCondition,
columns: {
id: true,
name: true,
mobile: true,
},
limit: limit,
offset: input.offset,
orderBy: (users, { asc }) => [asc(users.name)],
});
const userList = search
? await couponDbService.getUsersBySearch(search, limit, offset)
: await couponDbService.getUsersByIds([]);
return {
users: userList.map(user => ({
@ -625,75 +410,55 @@ export const couponRouter = router({
}),
createCoupon: protectedProcedure
.input(z.object({
mobile: z.string().min(1, 'Mobile number is required'),
}))
.input(z.object({ mobile: z.string().min(1, 'Mobile number is required') }))
.mutation(async ({ input, ctx }) => {
const { mobile } = input;
// Get staff user ID from auth middleware
const staffUserId = ctx.staffUser?.id;
if (!staffUserId) {
throw new Error("Unauthorized");
}
// Clean mobile number (remove non-digits)
const cleanMobile = mobile.replace(/\D/g, '');
// Validate: exactly 10 digits
if (cleanMobile.length !== 10) {
throw new Error("Mobile number must be exactly 10 digits");
}
// Check if user exists, create if not
let user = await db.query.users.findFirst({
where: eq(users.mobile, cleanMobile),
});
let user = await couponDbService.getUserByMobile(cleanMobile);
if (!user) {
// Create new user
const [newUser] = await db.insert(users).values({
user = await couponDbService.createUser({
name: null,
email: null,
mobile: cleanMobile,
}).returning();
user = newUser;
});
}
// Generate unique coupon code
const timestamp = Date.now().toString().slice(-6);
const random = Math.random().toString(36).substring(2, 6).toUpperCase();
const couponCode = `MF${cleanMobile.slice(-4)}${timestamp}${random}`;
// Check if coupon code already exists (very unlikely but safe)
const existingCode = await db.query.coupons.findFirst({
where: eq(coupons.couponCode, couponCode),
});
const existingCode = await couponDbService.getCouponByCode(couponCode);
if (existingCode) {
throw new Error("Generated coupon code already exists - please try again");
}
// Create the coupon
const [coupon] = await db.insert(coupons).values({
const coupon = await couponDbService.createCoupon({
couponCode,
isUserBased: true,
discountPercent: "20", // 20% discount
minOrder: "1000", // ₹1000 minimum order
maxValue: "500", // ₹500 maximum discount
maxLimitForUser: 1, // One-time use
discountPercent: "20",
minOrder: "1000",
maxValue: "500",
maxLimitForUser: 1,
isApplyForAll: false,
exclusiveApply: false,
createdBy: staffUserId,
validTill: dayjs().add(90, 'days').toDate(), // 90 days from now
}).returning();
// Associate coupon with user
await db.insert(couponApplicableUsers).values({
couponId: coupon.id,
userId: user.id,
validTill: dayjs().add(90, 'days').toDate(),
});
await couponDbService.addApplicableUsers(coupon.id, [user.id]);
return {
success: true,
coupon: {

View file

@ -1,28 +1,15 @@
import { router, protectedProcedure } from "@/src/trpc/trpc-index"
import { z } from "zod";
import { db } from "@/src/db/db_index"
import {
orders,
orderItems,
orderStatus,
users,
addresses,
refunds,
coupons,
couponUsage,
complaints,
payments,
} from "@/src/db/schema";
import { eq, and, gte, lt, desc, SQL, inArray } from "drizzle-orm";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import { ApiError } from "@/src/lib/api-error"
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
import { ApiError } from '@/src/lib/api-error'
import {
sendOrderPackagedNotification,
sendOrderDeliveredNotification,
} from "@/src/lib/notif-job";
import { publishCancellation } from "@/src/lib/post-order-handler"
import { getMultipleUserNegativityScores } from "@/src/stores/user-negativity-store"
} from '@/src/lib/notif-job'
import { publishCancellation } from '@/src/lib/post-order-handler'
import { getMultipleUserNegativityScores } from '@/src/stores/user-negativity-store'
import { orderDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/main'
const updateOrderNotesSchema = z.object({
orderId: z.number(),
@ -89,19 +76,13 @@ export const orderRouter = router({
.mutation(async ({ input }) => {
const { orderId, adminNotes } = input;
const result = await db
.update(orders)
.set({
adminNotes: adminNotes || null,
})
.where(eq(orders.id, orderId))
.returning();
const result = await orderDbService.updateOrderNotes(orderId, adminNotes || null)
if (result.length === 0) {
if (!result) {
throw new Error("Order not found");
}
return result[0];
return result;
}),
getFullOrder: protectedProcedure
@ -109,34 +90,14 @@ export const orderRouter = router({
.query(async ({ input }) => {
const { orderId } = input;
const orderData = await db.query.orders.findFirst({
where: eq(orders.id, orderId),
with: {
user: true,
address: true,
slot: true,
orderItems: {
with: {
product: {
with: {
unit: true,
},
},
},
},
payment: true,
paymentInfo: true,
},
});
const orderData = await orderDbService.getOrderWithRelations(orderId)
if (!orderData) {
throw new Error("Order not found");
}
// Get order status separately
const statusRecord = await db.query.orderStatus.findFirst({
where: eq(orderStatus.orderId, orderId),
});
const statusRecord = await orderDbService.getOrderStatusByOrderId(orderId)
let status: "pending" | "delivered" | "cancelled" = "pending";
if (statusRecord?.isCancelled) {
@ -148,9 +109,7 @@ export const orderRouter = router({
// Get refund details if order is cancelled
let refund = null;
if (status === "cancelled") {
refund = await db.query.refunds.findFirst({
where: eq(refunds.orderId, orderId),
});
refund = await orderDbService.getRefundByOrderId(orderId)
}
return {
@ -220,39 +179,14 @@ export const orderRouter = router({
const { orderId } = input;
// Single optimized query with all relations
const orderData = await db.query.orders.findFirst({
where: eq(orders.id, orderId),
with: {
user: true,
address: true,
slot: true,
orderItems: {
with: {
product: {
with: {
unit: true,
},
},
},
},
payment: true,
paymentInfo: true,
orderStatus: true, // Include in main query
refunds: true, // Include in main query
},
});
const orderData = await orderDbService.getOrderWithDetails(orderId)
if (!orderData) {
throw new Error("Order not found");
}
// Get coupon usage for this specific order using new orderId field
const couponUsageData = await db.query.couponUsage.findMany({
where: eq(couponUsage.orderId, orderData.id), // Use new orderId field
with: {
coupon: true,
},
});
const couponUsageData = await orderDbService.getCouponUsageByOrderId(orderData.id)
let couponData = null;
if (couponUsageData.length > 0) {
@ -388,27 +322,15 @@ export const orderRouter = router({
const { orderId, isPackaged } = input;
// Update all order items to the specified packaged state
await db
.update(orderItems)
.set({ is_packaged: isPackaged })
.where(eq(orderItems.orderId, parseInt(orderId)));
const parsedOrderId = parseInt(orderId)
await orderDbService.updateOrderItemsPackaged(parsedOrderId, isPackaged)
// Also update the order status table for backward compatibility
if (!isPackaged) {
await db
.update(orderStatus)
.set({ isPackaged, isDelivered: false })
.where(eq(orderStatus.orderId, parseInt(orderId)));
} else {
await db
.update(orderStatus)
.set({ isPackaged })
.where(eq(orderStatus.orderId, parseInt(orderId)));
}
const currentStatus = await orderDbService.getOrderStatusByOrderId(parsedOrderId)
const isDelivered = !isPackaged ? false : currentStatus?.isDelivered || false
const order = await db.query.orders.findFirst({
where: eq(orders.id, parseInt(orderId)),
});
await orderDbService.updateOrderStatusPackaged(parsedOrderId, isPackaged, isDelivered)
const order = await orderDbService.getOrderById(parsedOrderId)
if (order) await sendOrderPackagedNotification(order.userId, orderId);
return { success: true };
@ -419,14 +341,10 @@ export const orderRouter = router({
.mutation(async ({ input }) => {
const { orderId, isDelivered } = input;
await db
.update(orderStatus)
.set({ isDelivered })
.where(eq(orderStatus.orderId, parseInt(orderId)));
const parsedOrderId = parseInt(orderId)
await orderDbService.updateOrderStatusDelivered(parsedOrderId, isDelivered)
const order = await db.query.orders.findFirst({
where: eq(orders.id, parseInt(orderId)),
});
const order = await orderDbService.getOrderById(parsedOrderId)
if (order) await sendOrderDeliveredNotification(order.userId, orderId);
return { success: true };
@ -438,9 +356,7 @@ export const orderRouter = router({
const { orderItemId, isPackaged, isPackageVerified } = input;
// Validate that orderItem exists
const orderItem = await db.query.orderItems.findFirst({
where: eq(orderItems.id, orderItemId),
});
const orderItem = await orderDbService.getOrderItemById(orderItemId)
if (!orderItem) {
throw new ApiError("Order item not found", 404);
@ -456,10 +372,7 @@ export const orderRouter = router({
}
// Update the order item
await db
.update(orderItems)
.set(updateData)
.where(eq(orderItems.id, orderItemId));
await orderDbService.updateOrderItem(orderItemId, updateData)
return { success: true };
}),
@ -469,9 +382,7 @@ export const orderRouter = router({
.mutation(async ({ input }) => {
const { orderId } = input;
const order = await db.query.orders.findFirst({
where: eq(orders.id, orderId),
});
const order = await orderDbService.getOrderById(orderId)
if (!order) {
throw new Error('Order not found');
@ -481,13 +392,7 @@ export const orderRouter = router({
const currentTotalAmount = parseFloat(order.totalAmount?.toString() || '0');
const newTotalAmount = currentTotalAmount - currentDeliveryCharge;
await db
.update(orders)
.set({
deliveryCharge: '0',
totalAmount: newTotalAmount.toString()
})
.where(eq(orders.id, orderId));
await orderDbService.removeDeliveryCharge(orderId, newTotalAmount.toString())
return { success: true, message: 'Delivery charge removed' };
}),
@ -497,27 +402,10 @@ export const orderRouter = router({
.query(async ({ input }) => {
const { slotId } = input;
const slotOrders = await db.query.orders.findMany({
where: eq(orders.slotId, parseInt(slotId)),
with: {
user: true,
address: true,
slot: true,
orderItems: {
with: {
product: {
with: {
unit: true,
},
},
},
},
orderStatus: true,
},
});
const slotOrders = await orderDbService.getOrdersBySlotId(parseInt(slotId))
const filteredOrders = slotOrders.filter((order) => {
const statusRecord = order.orderStatus[0];
const statusRecord = order.orderStatus?.[0];
return (
order.isCod ||
(statusRecord && statusRecord.paymentStatus === "success")
@ -525,7 +413,7 @@ export const orderRouter = router({
});
const formattedOrders = filteredOrders.map((order) => {
const statusRecord = order.orderStatus[0]; // assuming one status per order
const statusRecord = order.orderStatus?.[0]; // assuming one status per order
let status: "pending" | "delivered" | "cancelled" = "pending";
if (statusRecord?.isCancelled) {
status = "cancelled";
@ -582,39 +470,14 @@ export const orderRouter = router({
const start = dayjs().startOf("day").toDate();
const end = dayjs().endOf("day").toDate();
let whereCondition = and(
gte(orders.createdAt, start),
lt(orders.createdAt, end)
);
if (slotId) {
whereCondition = and(
whereCondition,
eq(orders.slotId, parseInt(slotId))
);
}
const todaysOrders = await db.query.orders.findMany({
where: whereCondition,
with: {
user: true,
address: true,
slot: true,
orderItems: {
with: {
product: {
with: {
unit: true,
},
},
},
},
orderStatus: true,
},
});
const todaysOrders = await orderDbService.getOrdersByDateRange(
start,
end,
slotId ? parseInt(slotId) : undefined
)
const filteredOrders = todaysOrders.filter((order) => {
const statusRecord = order.orderStatus[0];
const statusRecord = order.orderStatus?.[0];
return (
order.isCod ||
(statusRecord && statusRecord.paymentStatus === "success")
@ -622,7 +485,7 @@ export const orderRouter = router({
});
const formattedOrders = filteredOrders.map((order) => {
const statusRecord = order.orderStatus[0]; // assuming one status per order
const statusRecord = order.orderStatus?.[0]; // assuming one status per order
let status: "pending" | "delivered" | "cancelled" = "pending";
if (statusRecord?.isCancelled) {
status = "cancelled";
@ -677,16 +540,9 @@ export const orderRouter = router({
.mutation(async ({ input }) => {
const { addressId, latitude, longitude } = input;
const result = await db
.update(addresses)
.set({
adminLatitude: latitude,
adminLongitude: longitude,
})
.where(eq(addresses.id, addressId))
.returning();
const result = await orderDbService.updateAddressCoords(addressId, latitude, longitude)
if (result.length === 0) {
if (!result) {
throw new ApiError("Address not found", 404);
}
@ -707,78 +563,15 @@ export const orderRouter = router({
flashDeliveryFilter,
} = input;
let whereCondition: SQL<unknown> | undefined = eq(orders.id, orders.id); // always true
if (cursor) {
whereCondition = and(whereCondition, lt(orders.id, cursor));
}
if (slotId) {
whereCondition = and(whereCondition, eq(orders.slotId, slotId));
}
if (packagedFilter === "packaged") {
whereCondition = and(
whereCondition,
eq(orderStatus.isPackaged, true)
);
} else if (packagedFilter === "not_packaged") {
whereCondition = and(
whereCondition,
eq(orderStatus.isPackaged, false)
);
}
if (deliveredFilter === "delivered") {
whereCondition = and(
whereCondition,
eq(orderStatus.isDelivered, true)
);
} else if (deliveredFilter === "not_delivered") {
whereCondition = and(
whereCondition,
eq(orderStatus.isDelivered, false)
);
}
if (cancellationFilter === "cancelled") {
whereCondition = and(
whereCondition,
eq(orderStatus.isCancelled, true)
);
} else if (cancellationFilter === "not_cancelled") {
whereCondition = and(
whereCondition,
eq(orderStatus.isCancelled, false)
);
}
if (flashDeliveryFilter === "flash") {
whereCondition = and(
whereCondition,
eq(orders.isFlashDelivery, true)
);
} else if (flashDeliveryFilter === "regular") {
whereCondition = and(
whereCondition,
eq(orders.isFlashDelivery, false)
);
}
const allOrders = await db.query.orders.findMany({
where: whereCondition,
orderBy: desc(orders.createdAt),
limit: limit + 1, // fetch one extra to check if there's more
with: {
user: true,
address: true,
slot: true,
orderItems: {
with: {
product: {
with: {
unit: true,
},
},
},
},
orderStatus: true,
},
});
const allOrders = await orderDbService.getAllOrdersWithFilters({
cursor,
limit,
slotId,
packagedFilter,
deliveredFilter,
cancellationFilter,
flashDeliveryFilter,
})
const hasMore = allOrders.length > limit;
const ordersToReturn = hasMore ? allOrders.slice(0, limit) : allOrders;
@ -787,7 +580,7 @@ export const orderRouter = router({
const negativityScores = await getMultipleUserNegativityScores(userIds);
const filteredOrders = ordersToReturn.filter((order) => {
const statusRecord = order.orderStatus[0];
const statusRecord = order.orderStatus?.[0];
return (
order.isCod ||
(statusRecord && statusRecord.paymentStatus === "success")
@ -795,7 +588,7 @@ export const orderRouter = router({
});
const formattedOrders = filteredOrders.map((order) => {
const statusRecord = order.orderStatus[0];
const statusRecord = order.orderStatus?.[0];
let status: "pending" | "delivered" | "cancelled" = "pending";
if (statusRecord?.isCancelled) {
status = "cancelled";
@ -868,21 +661,7 @@ export const orderRouter = router({
.mutation(async ({ input }) => {
const slotIds = input.slotIds;
const ordersList = await db.query.orders.findMany({
where: inArray(orders.slotId, slotIds),
with: {
orderItems: {
with: {
product: true
}
},
couponUsages: {
with: {
coupon: true
}
},
}
});
const ordersList = await orderDbService.getOrdersBySlotIds(slotIds)
const processedOrdersData = ordersList.map((order) => {
@ -921,19 +700,19 @@ export const orderRouter = router({
})
const updatedOrderIds: number[] = [];
await db.transaction(async (tx) => {
for (const { order, updatedOrderItems, newTotal } of processedOrdersData) {
await tx.update(orders).set({ totalAmount: newTotal.toString() }).where(eq(orders.id, order.id));
updatedOrderIds.push(order.id);
for (const item of updatedOrderItems) {
await tx.update(orderItems).set({
await orderDbService.updateOrdersAndItemsInTransaction(
processedOrdersData.map((entry) => ({
orderId: entry.order.id,
totalAmount: entry.newTotal.toString(),
items: entry.updatedOrderItems.map((item) => ({
id: item.id,
price: item.price,
discountedPrice: item.discountedPrice
}).where(eq(orderItems.id, item.id));
}
}
});
discountedPrice: item.discountedPrice || item.price,
})),
}))
)
processedOrdersData.forEach((entry) => updatedOrderIds.push(entry.order.id))
return { success: true, updatedOrders: updatedOrderIds, message: `Rebalanced ${updatedOrderIds.length} orders.` };
}),
@ -946,12 +725,7 @@ export const orderRouter = router({
.mutation(async ({ input }) => {
const { orderId, reason } = input;
const order = await db.query.orders.findFirst({
where: eq(orders.id, orderId),
with: {
orderStatus: true,
},
});
const order = await orderDbService.getOrderWithStatus(orderId)
if (!order) {
throw new ApiError("Order not found", 404);
@ -970,28 +744,13 @@ export const orderRouter = router({
throw new ApiError("Cannot cancel delivered order", 400);
}
const result = await db.transaction(async (tx) => {
await tx
.update(orderStatus)
.set({
isCancelled: true,
isCancelledByAdmin: true,
cancelReason: reason,
cancellationAdminNotes: reason,
cancellationReviewed: true,
cancellationReviewedAt: new Date(),
})
.where(eq(orderStatus.id, status.id));
await orderDbService.cancelOrderStatus(status.id, reason)
const refundStatus = order.isCod ? "na" : "pending";
const refundStatus = order.isCod ? 'na' : 'pending'
await tx.insert(refunds).values({
orderId: order.id,
refundStatus,
});
await orderDbService.createRefund(order.id, refundStatus)
return { orderId: order.id, userId: order.userId };
});
const result = { orderId: order.id, userId: order.userId }
// Publish to Redis for Telegram notification
await publishCancellation(result.orderId, 'admin', reason);
@ -1005,14 +764,5 @@ export const orderRouter = router({
type RefundStatus = "success" | "pending" | "failed" | "none" | "na";
export async function deleteOrderById(orderId: number): Promise<void> {
await db.transaction(async (tx) => {
await tx.delete(orderItems).where(eq(orderItems.orderId, orderId));
await tx.delete(orderStatus).where(eq(orderStatus.orderId, orderId));
await tx.delete(payments).where(eq(payments.orderId, orderId));
await tx.delete(refunds).where(eq(refunds.orderId, orderId));
await tx.delete(couponUsage).where(eq(couponUsage.orderId, orderId));
await tx.delete(complaints).where(eq(complaints.orderId, orderId));
await tx.delete(orders).where(eq(orders.id, orderId));
});
await orderDbService.deleteOrderById(orderId)
}

View file

@ -1,15 +1,7 @@
import { router, protectedProcedure } from "@/src/trpc/trpc-index"
import { z } from "zod";
import { db } from "@/src/db/db_index"
import {
orders,
orderStatus,
payments,
refunds,
} from "@/src/db/schema";
import { and, eq } from "drizzle-orm";
import { ApiError } from "@/src/lib/api-error"
import { RazorpayPaymentService } from "@/src/lib/payments-utils"
import { refundDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/main'
const initiateRefundSchema = z
.object({
@ -37,18 +29,14 @@ export const adminPaymentsRouter = router({
const { orderId, refundPercent, refundAmount } = input;
// Validate order exists
const order = await db.query.orders.findFirst({
where: eq(orders.id, orderId),
});
const order = await refundDbService.getOrderById(orderId);
if (!order) {
throw new ApiError("Order not found", 404);
}
// Check if order is paid
const orderStatusRecord = await db.query.orderStatus.findFirst({
where: eq(orderStatus.orderId, orderId),
});
const orderStatusRecord = await refundDbService.getOrderStatusByOrderId(orderId);
if(order.isCod) {
throw new ApiError("Order is a Cash On Delivery. Not eligible for refund")
@ -76,54 +64,31 @@ export const adminPaymentsRouter = router({
throw new ApiError("Invalid refund parameters", 400);
}
let razorpayRefund = null;
let merchantRefundId = null;
let merchantRefundId = 'xxx'; //temporary suppressal
// Get payment record for online payments
const payment = await db.query.payments.findFirst({
where: and(
eq(payments.orderId, orderId),
eq(payments.status, "success")
),
});
const payment = await refundDbService.getSuccessfulPaymentByOrderId(orderId);
if (!payment || payment.status !== "success") {
throw new ApiError("Payment not found or not successful", 404);
}
const payload = payment.payload as any;
// Initiate Razorpay refund
razorpayRefund = await RazorpayPaymentService.initiateRefund(
payload.payment_id,
Math.round(calculatedRefundAmount * 100) // Convert to paisa
);
merchantRefundId = razorpayRefund.id;
// Check if refund already exists for this order
const existingRefund = await db.query.refunds.findFirst({
where: eq(refunds.orderId, orderId),
});
const existingRefund = await refundDbService.getRefundByOrderId(orderId);
const refundStatus = "initiated";
if (existingRefund) {
// Update existing refund
await db
.update(refunds)
.set({
await refundDbService.updateRefund(existingRefund.id, {
refundAmount: calculatedRefundAmount.toString(),
refundStatus,
merchantRefundId,
refundProcessedAt: order.isCod ? new Date() : null,
})
.where(eq(refunds.id, existingRefund.id));
});
} else {
// Insert new refund
await db
.insert(refunds)
.values({
await refundDbService.createRefund({
orderId,
refundAmount: calculatedRefundAmount.toString(),
refundStatus,

View file

@ -1,9 +1,7 @@
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod';
import { db } from '@/src/db/db_index'
import { productAvailabilitySchedules } from '@/src/db/schema'
import { eq } from 'drizzle-orm';
import { refreshScheduleJobs } from '@/src/lib/automatedJobs';
import { scheduleDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/main'
const createScheduleSchema = z.object({
scheduleName: z.string().min(1, "Schedule name is required"),
@ -35,33 +33,29 @@ export const productAvailabilitySchedulesRouter = router({
}
// Check if schedule name already exists
const existingSchedule = await db.query.productAvailabilitySchedules.findFirst({
where: eq(productAvailabilitySchedules.scheduleName, scheduleName),
});
const existingSchedule = await scheduleDbService.getScheduleByName(scheduleName);
if (existingSchedule) {
throw new Error("Schedule name already exists");
}
// Create schedule with arrays
const scheduleResult = await db.insert(productAvailabilitySchedules).values({
const scheduleResult = await scheduleDbService.createSchedule({
scheduleName,
time,
action,
productIds,
groupIds,
}).returning();
});
// Refresh cron jobs to include new schedule
await refreshScheduleJobs();
return scheduleResult[0];
return scheduleResult;
}),
getAll: protectedProcedure
.query(async () => {
const schedules = await db.query.productAvailabilitySchedules.findMany({
orderBy: (productAvailabilitySchedules, { desc }) => [desc(productAvailabilitySchedules.createdAt)],
});
const schedules = await scheduleDbService.getAllSchedules();
return schedules.map(schedule => ({
...schedule,
@ -75,9 +69,7 @@ export const productAvailabilitySchedulesRouter = router({
.query(async ({ input }) => {
const { id } = input;
const schedule = await db.query.productAvailabilitySchedules.findFirst({
where: eq(productAvailabilitySchedules.id, id),
});
const schedule = await scheduleDbService.getScheduleById(id);
if (!schedule) {
throw new Error("Schedule not found");
@ -92,18 +84,14 @@ export const productAvailabilitySchedulesRouter = router({
const { id, updates } = input;
// Check if schedule exists
const existingSchedule = await db.query.productAvailabilitySchedules.findFirst({
where: eq(productAvailabilitySchedules.id, id),
});
const existingSchedule = await scheduleDbService.getScheduleById(id);
if (!existingSchedule) {
throw new Error("Schedule not found");
}
// Check schedule name uniqueness if being updated
if (updates.scheduleName && updates.scheduleName !== existingSchedule.scheduleName) {
const duplicateSchedule = await db.query.productAvailabilitySchedules.findFirst({
where: eq(productAvailabilitySchedules.scheduleName, updates.scheduleName),
});
const duplicateSchedule = await scheduleDbService.getScheduleByName(updates.scheduleName);
if (duplicateSchedule) {
throw new Error("Schedule name already exists");
}
@ -116,21 +104,13 @@ export const productAvailabilitySchedulesRouter = router({
if (updates.action !== undefined) updateData.action = updates.action;
if (updates.productIds !== undefined) updateData.productIds = updates.productIds;
if (updates.groupIds !== undefined) updateData.groupIds = updates.groupIds;
updateData.lastUpdated = new Date();
const result = await db.update(productAvailabilitySchedules)
.set(updateData)
.where(eq(productAvailabilitySchedules.id, id))
.returning();
if (result.length === 0) {
throw new Error("Failed to update schedule");
}
const result = await scheduleDbService.updateSchedule(id, updateData);
// Refresh cron jobs to reflect changes
await refreshScheduleJobs();
return result[0];
return result;
}),
delete: protectedProcedure
@ -138,13 +118,7 @@ export const productAvailabilitySchedulesRouter = router({
.mutation(async ({ input }) => {
const { id } = input;
const result = await db.delete(productAvailabilitySchedules)
.where(eq(productAvailabilitySchedules.id, id))
.returning();
if (result.length === 0) {
throw new Error("Schedule not found");
}
await scheduleDbService.deleteSchedule(id);
// Refresh cron jobs to remove deleted schedule
await refreshScheduleJobs();

View file

@ -1,12 +1,9 @@
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod';
import { db } from '@/src/db/db_index'
import { productInfo, units, specialDeals, productSlots, productTags, productReviews, users, productGroupInfo, productGroupMembership } from '@/src/db/schema'
import { eq, and, inArray, desc, sql } from 'drizzle-orm';
import { productDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/main'
import { ApiError } from '@/src/lib/api-error'
import { imageUploadS3, scaffoldAssetUrl, getOriginalUrlFromSignedUrl, claimUploadUrl } from '@/src/lib/s3-client'
import { scaffoldAssetUrl, claimUploadUrl } from '@/src/lib/s3-client'
import { deleteS3Image } from '@/src/lib/delete-image'
import type { SpecialDeal } from '@/src/db/types'
import { scheduleStoreInitialization } from '@/src/stores/store-initializer'
@ -19,13 +16,7 @@ type CreateDeal = {
export const productRouter = router({
getProducts: protectedProcedure
.query(async ({ ctx }) => {
const products = await db.query.productInfo.findMany({
orderBy: productInfo.name,
with: {
unit: true,
store: true,
},
});
const products = await productDbService.getAllProducts();
// Generate signed URLs for all product images
const productsWithSignedUrls = await Promise.all(
@ -48,30 +39,17 @@ export const productRouter = router({
.query(async ({ input, ctx }) => {
const { id } = input;
const product = await db.query.productInfo.findFirst({
where: eq(productInfo.id, id),
with: {
unit: true,
},
});
const product = await productDbService.getProductById(id);
if (!product) {
throw new ApiError("Product not found", 404);
}
// Fetch special deals for this product
const deals = await db.query.specialDeals.findMany({
where: eq(specialDeals.productId, id),
orderBy: specialDeals.quantity,
});
const deals = await productDbService.getDealsByProductId(id);
// Fetch associated tags for this product
const productTagsData = await db.query.productTags.findMany({
where: eq(productTags.productId, id),
with: {
tag: true,
},
});
const productTagsData = await productDbService.getTagsByProductId(id);
// Generate signed URLs for product images
const productWithSignedUrls = {
@ -93,10 +71,7 @@ export const productRouter = router({
.mutation(async ({ input, ctx }) => {
const { id } = input;
const [deletedProduct] = await db
.delete(productInfo)
.where(eq(productInfo.id, id))
.returning();
const deletedProduct = await productDbService.deleteProduct(id);
if (!deletedProduct) {
throw new ApiError("Product not found", 404);
@ -146,25 +121,20 @@ export const productRouter = router({
}
// Check for duplicate name
const existingProduct = await db.query.productInfo.findFirst({
where: eq(productInfo.name, name.trim()),
});
const allProducts = await productDbService.getAllProducts();
const existingProduct = allProducts.find(p => p.name === name.trim());
if (existingProduct) {
throw new ApiError("A product with this name already exists", 400);
}
// Check if unit exists
const unit = await db.query.units.findFirst({
where: eq(units.id, unitId),
});
const unit = await productDbService.getUnitById(unitId);
if (!unit) {
throw new ApiError("Invalid unit ID", 400);
}
console.log(imageKeys)
const [newProduct] = await db
.insert(productInfo)
.values({
const newProduct = await productDbService.createProduct({
name: name.trim(),
shortDescription,
longDescription,
@ -178,8 +148,7 @@ export const productRouter = router({
isFlashAvailable,
flashPrice: flashPrice?.toString(),
images: imageKeys || [],
})
.returning();
});
// Handle deals
if (deals && deals.length > 0) {
@ -189,7 +158,7 @@ export const productRouter = router({
price: deal.price.toString(),
validTill: new Date(deal.validTill),
}));
await db.insert(specialDeals).values(dealInserts);
await productDbService.createDeals(dealInserts);
}
// Handle tags
@ -198,7 +167,7 @@ export const productRouter = router({
productId: newProduct.id,
tagId,
}));
await db.insert(productTags).values(tagAssociations);
await productDbService.createTagAssociations(tagAssociations);
}
// Claim upload URLs
@ -207,7 +176,7 @@ export const productRouter = router({
try {
await claimUploadUrl(key);
} catch (e) {
console.warn(`Failed to claim upload URL for key: ${key}`, e);
console.warn("Failed to claim upload URL for key:", key, e);
}
}
}
@ -248,9 +217,7 @@ export const productRouter = router({
const { id, newImageKeys, imagesToDelete, deals, tagIds, ...updateData } = input;
// Get current product
const currentProduct = await db.query.productInfo.findFirst({
where: eq(productInfo.id, id),
});
const currentProduct = await productDbService.getProductById(id);
if (!currentProduct) {
throw new ApiError("Product not found", 404);
}
@ -262,11 +229,11 @@ export const productRouter = router({
try {
await deleteS3Image(imageUrl);
} catch (e) {
console.error(`Failed to delete image: ${imageUrl}`, e);
console.error("Failed to delete image:", imageUrl, e);
}
}
currentImages = currentImages.filter(img => {
//!imagesToDelete.includes(img)
// imagesToDelete.includes(img)
const isRemoved = imagesToDelete.some(item => item.includes(img));
return !isRemoved;
});
@ -280,28 +247,24 @@ export const productRouter = router({
try {
await claimUploadUrl(key);
} catch (e) {
console.warn(`Failed to claim upload URL for key: ${key}`, e);
console.warn("Failed to claim upload URL for key:", key, e);
}
}
}
// Update product - convert numeric fields to strings for PostgreSQL numeric type
const { price, marketPrice, flashPrice, ...otherData } = updateData;
const [updatedProduct] = await db
.update(productInfo)
.set({
const updatedProduct = await productDbService.updateProduct(id, {
...otherData,
...(price !== undefined && { price: price.toString() }),
...(marketPrice !== undefined && { marketPrice: marketPrice.toString() }),
...(flashPrice !== undefined && { flashPrice: flashPrice.toString() }),
images: currentImages,
})
.where(eq(productInfo.id, id))
.returning();
});
// Handle deals update
if (deals !== undefined) {
await db.delete(specialDeals).where(eq(specialDeals.productId, id));
await productDbService.deleteDealsByProductId(id);
if (deals.length > 0) {
const dealInserts = deals.map(deal => ({
productId: id,
@ -309,19 +272,19 @@ export const productRouter = router({
price: deal.price.toString(),
validTill: new Date(deal.validTill),
}));
await db.insert(specialDeals).values(dealInserts);
await productDbService.createDeals(dealInserts);
}
}
// Handle tags update
if (tagIds !== undefined) {
await db.delete(productTags).where(eq(productTags.productId, id));
await productDbService.deleteTagAssociationsByProductId(id);
if (tagIds.length > 0) {
const tagAssociations = tagIds.map(tagId => ({
productId: id,
tagId,
}));
await db.insert(productTags).values(tagAssociations);
await productDbService.createTagAssociations(tagAssociations);
}
}
@ -340,21 +303,15 @@ export const productRouter = router({
.mutation(async ({ input, ctx }) => {
const { id } = input;
const product = await db.query.productInfo.findFirst({
where: eq(productInfo.id, id),
});
const product = await productDbService.getProductById(id);
if (!product) {
throw new ApiError("Product not found", 404);
}
const [updatedProduct] = await db
.update(productInfo)
.set({
const updatedProduct = await productDbService.updateProduct(id, {
isOutOfStock: !product.isOutOfStock,
})
.where(eq(productInfo.id, id))
.returning();
});
// Reinitialize stores to reflect changes
scheduleStoreInitialization()
@ -378,12 +335,7 @@ export const productRouter = router({
}
// Get current associations
const currentAssociations = await db.query.productSlots.findMany({
where: eq(productSlots.slotId, parseInt(slotId)),
columns: {
productId: true,
},
});
const currentAssociations = await productDbService.getProductSlotsBySlotId(parseInt(slotId));
const currentProductIds = currentAssociations.map(assoc => assoc.productId);
const newProductIds = productIds.map((id: string) => parseInt(id));
@ -394,22 +346,16 @@ export const productRouter = router({
// Remove associations for products that are no longer selected
if (productsToRemove.length > 0) {
await db.delete(productSlots).where(
and(
eq(productSlots.slotId, parseInt(slotId)),
inArray(productSlots.productId, productsToRemove)
)
);
for (const productId of productsToRemove) {
await productDbService.deleteProductSlot(parseInt(slotId), productId);
}
}
// Add associations for newly selected products
if (productsToAdd.length > 0) {
const newAssociations = productsToAdd.map(productId => ({
productId,
slotId: parseInt(slotId),
}));
await db.insert(productSlots).values(newAssociations);
for (const productId of productsToAdd) {
await productDbService.createProductSlot(parseInt(slotId), productId);
}
}
// Reinitialize stores to reflect changes
@ -429,12 +375,7 @@ export const productRouter = router({
.query(async ({ input, ctx }) => {
const { slotId } = input;
const associations = await db.query.productSlots.findMany({
where: eq(productSlots.slotId, parseInt(slotId)),
columns: {
productId: true,
},
});
const associations = await productDbService.getProductSlotsBySlotId(parseInt(slotId));
const productIds = associations.map(assoc => assoc.productId);
@ -459,13 +400,7 @@ export const productRouter = router({
}
// Fetch all associations for the requested slots
const associations = await db.query.productSlots.findMany({
where: inArray(productSlots.slotId, slotIds),
columns: {
slotId: true,
productId: true,
},
});
const associations = await productDbService.getProductSlotsBySlotIds(slotIds);
// Group by slotId
const result = associations.reduce((acc, assoc) => {
@ -495,23 +430,7 @@ export const productRouter = router({
.query(async ({ input }) => {
const { productId, limit, offset } = input;
const reviews = await db
.select({
id: productReviews.id,
reviewBody: productReviews.reviewBody,
ratings: productReviews.ratings,
imageUrls: productReviews.imageUrls,
reviewTime: productReviews.reviewTime,
adminResponse: productReviews.adminResponse,
adminResponseImages: productReviews.adminResponseImages,
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);
const reviews = await productDbService.getReviewsByProductId(productId, limit, offset);
// Generate signed URLs for images
const reviewsWithSignedUrls = await Promise.all(
@ -523,12 +442,7 @@ export const productRouter = router({
);
// Check if more reviews exist
const totalCountResult = await db
.select({ count: sql`count(*)` })
.from(productReviews)
.where(eq(productReviews.productId, productId));
const totalCount = Number(totalCountResult[0].count);
const totalCount = await productDbService.getReviewCountByProductId(productId);
const hasMore = offset + limit < totalCount;
return { reviews: reviewsWithSignedUrls, hasMore };
@ -544,14 +458,10 @@ export const productRouter = router({
.mutation(async ({ input }) => {
const { reviewId, adminResponse, adminResponseImages, uploadUrls } = input;
const [updatedReview] = await db
.update(productReviews)
.set({
const updatedReview = await productDbService.updateReview(reviewId, {
adminResponse,
adminResponseImages,
})
.where(eq(productReviews.id, reviewId))
.returning();
});
if (!updatedReview) {
throw new ApiError('Review not found', 404);
@ -559,7 +469,6 @@ export const productRouter = router({
// Claim upload URLs
if (uploadUrls && uploadUrls.length > 0) {
// const { claimUploadUrl } = await import('@/src/lib/s3-client');
await Promise.all(uploadUrls.map(url => claimUploadUrl(url)));
}
@ -568,22 +477,13 @@ export const productRouter = router({
getGroups: protectedProcedure
.query(async ({ ctx }) => {
const groups = await db.query.productGroupInfo.findMany({
with: {
memberships: {
with: {
product: true,
},
},
},
orderBy: desc(productGroupInfo.createdAt),
});
const groups = await productDbService.getAllGroups() as any[];
return {
groups: groups.map(group => ({
...group,
products: group.memberships.map(m => m.product),
productCount: group.memberships.length,
products: group.memberships?.map((m: any) => m.product) || [],
productCount: group.memberships?.length || 0,
})),
};
}),
@ -597,13 +497,10 @@ export const productRouter = router({
.mutation(async ({ input, ctx }) => {
const { group_name, description, product_ids } = input;
const [newGroup] = await db
.insert(productGroupInfo)
.values({
const newGroup = await productDbService.createGroup({
groupName: group_name,
description,
})
.returning();
});
if (product_ids.length > 0) {
const memberships = product_ids.map(productId => ({
@ -611,7 +508,7 @@ export const productRouter = router({
groupId: newGroup.id,
}));
await db.insert(productGroupMembership).values(memberships);
await productDbService.createGroupMemberships(memberships);
}
// Reinitialize stores to reflect changes
@ -637,11 +534,7 @@ export const productRouter = router({
if (group_name !== undefined) updateData.groupName = group_name;
if (description !== undefined) updateData.description = description;
const [updatedGroup] = await db
.update(productGroupInfo)
.set(updateData)
.where(eq(productGroupInfo.id, id))
.returning();
const updatedGroup = await productDbService.updateGroup(id, updateData);
if (!updatedGroup) {
throw new ApiError('Group not found', 404);
@ -649,7 +542,7 @@ export const productRouter = router({
if (product_ids !== undefined) {
// Delete existing memberships
await db.delete(productGroupMembership).where(eq(productGroupMembership.groupId, id));
await productDbService.deleteGroupMembershipsByGroupId(id);
// Insert new memberships
if (product_ids.length > 0) {
@ -658,7 +551,7 @@ export const productRouter = router({
groupId: id,
}));
await db.insert(productGroupMembership).values(memberships);
await productDbService.createGroupMemberships(memberships);
}
}
@ -679,13 +572,10 @@ export const productRouter = router({
const { id } = input;
// Delete memberships first
await db.delete(productGroupMembership).where(eq(productGroupMembership.groupId, id));
await productDbService.deleteGroupMembershipsByGroupId(id);
// Delete group
const [deletedGroup] = await db
.delete(productGroupInfo)
.where(eq(productGroupInfo.id, id))
.returning();
const deletedGroup = await productDbService.deleteGroup(id);
if (!deletedGroup) {
throw new ApiError('Group not found', 404);
@ -718,34 +608,28 @@ export const productRouter = router({
// Validate that all productIds exist
const productIds = updates.map(u => u.productId);
const existingProducts = await db.query.productInfo.findMany({
where: inArray(productInfo.id, productIds),
columns: { id: true },
});
const allExist = await productDbService.validateProductIdsExist(productIds);
const existingIds = new Set(existingProducts.map(p => p.id));
const invalidIds = productIds.filter(id => !existingIds.has(id));
if (invalidIds.length > 0) {
throw new ApiError(`Invalid product IDs: ${invalidIds.join(', ')}`, 400);
if (!allExist) {
throw new ApiError('Some product IDs are invalid', 400);
}
// Perform batch update
const updatePromises = updates.map(async (update) => {
const batchUpdates = updates.map(update => {
const { productId, price, marketPrice, flashPrice, isFlashAvailable } = update;
const updateData: any = {};
if (price !== undefined) updateData.price = price;
if (marketPrice !== undefined) updateData.marketPrice = marketPrice;
if (flashPrice !== undefined) updateData.flashPrice = flashPrice;
if (price !== undefined) updateData.price = price.toString();
if (marketPrice !== undefined) updateData.marketPrice = marketPrice?.toString();
if (flashPrice !== undefined) updateData.flashPrice = flashPrice?.toString();
if (isFlashAvailable !== undefined) updateData.isFlashAvailable = isFlashAvailable;
return db
.update(productInfo)
.set(updateData)
.where(eq(productInfo.id, productId));
return {
productId,
data: updateData,
};
});
await Promise.all(updatePromises);
await productDbService.batchUpdateProducts(batchUpdates);
// Reinitialize stores to reflect changes
scheduleStoreInitialization()

View file

@ -1,15 +1,12 @@
import { router, protectedProcedure } from "@/src/trpc/trpc-index"
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { db } from "@/src/db/db_index"
import { deliverySlotInfo, productSlots, productInfo, vendorSnippets, productGroupInfo } from "@/src/db/schema"
import { eq, inArray, and, desc } from "drizzle-orm";
import { ApiError } from "@/src/lib/api-error"
import { appUrl } from "@/src/lib/env-exporter"
import redisClient from "@/src/lib/redis-client"
import { getSlotSequenceKey } from "@/src/lib/redisKeyGetters"
import { scheduleStoreInitialization } from '@/src/stores/store-initializer'
import { slotDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/main'
interface CachedDeliverySequence {
[userId: string]: number[];
@ -58,50 +55,29 @@ const getDeliverySequenceSchema = z.object({
const updateDeliverySequenceSchema = z.object({
id: z.number(),
// deliverySequence: z.array(z.number()),
deliverySequence: z.any(),
});
export const slotsRouter = router({
// Exact replica of GET /av/slots
getAll: protectedProcedure.query(async ({ ctx }) => {
if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
}
const slots = await db.query.deliverySlotInfo
.findMany({
where: eq(deliverySlotInfo.isActive, true),
orderBy: desc(deliverySlotInfo.deliveryTime),
with: {
productSlots: {
with: {
product: {
columns: {
id: true,
name: true,
images: true,
},
},
},
},
},
})
.then((slots) =>
slots.map((slot) => ({
const slots = await slotDbService.getAllSlots();
const transformedSlots = slots.map((slot) => ({
...slot,
deliverySequence: slot.deliverySequence as number[],
products: slot.productSlots.map((ps) => ps.product),
}))
);
products: slot.productSlots.map((ps: any) => ps.product),
}));
return {
slots,
count: slots.length,
slots: transformedSlots,
count: transformedSlots.length,
};
}),
// Exact replica of POST /av/products/slots/product-ids
getSlotsProductIds: protectedProcedure
.input(z.object({ slotIds: z.array(z.number()) }))
.query(async ({ input, ctx }) => {
@ -122,25 +98,16 @@ export const slotsRouter = router({
return {};
}
// Fetch all associations for the requested slots
const associations = await db.query.productSlots.findMany({
where: inArray(productSlots.slotId, slotIds),
columns: {
slotId: true,
productId: true,
},
});
const associations = await slotDbService.getProductSlotsBySlotIds(slotIds);
// Group by slotId
const result = associations.reduce((acc, assoc) => {
const result = associations.reduce((acc: Record<number, number[]>, assoc) => {
if (!acc[assoc.slotId]) {
acc[assoc.slotId] = [];
}
acc[assoc.slotId].push(assoc.productId);
return acc;
}, {} as Record<number, number[]>);
}, {});
// Ensure all requested slots have entries (even if empty)
slotIds.forEach((slotId) => {
if (!result[slotId]) {
result[slotId] = [];
@ -150,14 +117,8 @@ export const slotsRouter = router({
return result;
}),
// Exact replica of PUT /av/products/slots/:slotId/products
updateSlotProducts: protectedProcedure
.input(
z.object({
slotId: z.number(),
productIds: z.array(z.number()),
})
)
.input(z.object({ slotId: z.number(), productIds: z.array(z.number()) }))
.mutation(async ({ input, ctx }) => {
if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
@ -172,51 +133,22 @@ export const slotsRouter = router({
});
}
// Get current associations
const currentAssociations = await db.query.productSlots.findMany({
where: eq(productSlots.slotId, slotId),
columns: {
productId: true,
},
});
const currentProductIds = currentAssociations.map(
(assoc) => assoc.productId
);
const currentAssociations = await slotDbService.getProductSlotsBySlotId(slotId);
const currentProductIds = currentAssociations.map((assoc) => assoc.productId);
const newProductIds = productIds;
// Find products to add and remove
const productsToAdd = newProductIds.filter(
(id) => !currentProductIds.includes(id)
);
const productsToRemove = currentProductIds.filter(
(id) => !newProductIds.includes(id)
);
const productsToAdd = newProductIds.filter((id) => !currentProductIds.includes(id));
const productsToRemove = currentProductIds.filter((id) => !newProductIds.includes(id));
// Remove associations for products that are no longer selected
if (productsToRemove.length > 0) {
await db
.delete(productSlots)
.where(
and(
eq(productSlots.slotId, slotId),
inArray(productSlots.productId, productsToRemove)
)
);
for (const productId of productsToRemove) {
await slotDbService.deleteProductSlot(slotId, productId);
}
// Add associations for newly selected products
if (productsToAdd.length > 0) {
const newAssociations = productsToAdd.map((productId) => ({
productId,
slotId,
}));
await db.insert(productSlots).values(newAssociations);
for (const productId of productsToAdd) {
await slotDbService.createProductSlot(slotId, productId);
}
// Reinitialize stores to reflect changes
scheduleStoreInitialization()
scheduleStoreInitialization();
return {
message: "Slot products updated successfully",
@ -234,58 +166,43 @@ export const slotsRouter = router({
const { deliveryTime, freezeTime, isActive, productIds, vendorSnippets: snippets, groupIds } = input;
// Validate required fields
if (!deliveryTime || !freezeTime) {
throw new ApiError("Delivery time and orders close time are required", 400);
}
const result = await db.transaction(async (tx) => {
// Create slot
const [newSlot] = await tx
.insert(deliverySlotInfo)
.values({
const result = await slotDbService.withTransaction(async (tx) => {
const newSlot = await slotDbService.createSlot({
deliveryTime: new Date(deliveryTime),
freezeTime: new Date(freezeTime),
isActive: isActive !== undefined ? isActive : true,
groupIds: groupIds !== undefined ? groupIds : [],
})
.returning();
});
// Insert product associations if provided
if (productIds && productIds.length > 0) {
const associations = productIds.map((productId) => ({
productId,
slotId: newSlot.id,
}));
await tx.insert(productSlots).values(associations);
for (const productId of productIds) {
await slotDbService.createProductSlot(newSlot.id, productId);
}
}
// Create vendor snippets if provided
let createdSnippets: any[] = [];
if (snippets && snippets.length > 0) {
for (const snippet of snippets) {
// Validate products exist
const products = await tx.query.productInfo.findMany({
where: inArray(productInfo.id, snippet.productIds),
});
if (products.length !== snippet.productIds.length) {
const productsValid = await slotDbService.validateProductsExist(snippet.productIds);
if (!productsValid) {
throw new ApiError(`One or more invalid product IDs in snippet "${snippet.name}"`, 400);
}
// Check if snippet name already exists
const existingSnippet = await tx.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.snippetCode, snippet.name),
});
if (existingSnippet) {
const codeExists = await slotDbService.checkSnippetCodeExists(snippet.name);
if (codeExists) {
throw new ApiError(`Snippet name "${snippet.name}" already exists`, 400);
}
const [createdSnippet] = await tx.insert(vendorSnippets).values({
const createdSnippet = await slotDbService.createVendorSnippet({
snippetCode: snippet.name,
slotId: newSlot.id,
productIds: snippet.productIds,
validTill: snippet.validTill ? new Date(snippet.validTill) : undefined,
}).returning();
});
createdSnippets.push(createdSnippet);
}
@ -298,8 +215,7 @@ export const slotsRouter = router({
};
});
// Reinitialize stores to reflect changes (outside transaction)
scheduleStoreInitialization()
scheduleStoreInitialization();
return result;
}),
@ -309,9 +225,7 @@ export const slotsRouter = router({
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
}
const slots = await db.query.deliverySlotInfo.findMany({
where: eq(deliverySlotInfo.isActive, true),
});
const slots = await slotDbService.getActiveSlots();
return {
slots,
@ -328,23 +242,7 @@ export const slotsRouter = router({
const { id } = input;
const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, id),
with: {
productSlots: {
with: {
product: {
columns: {
id: true,
name: true,
images: true,
},
},
},
},
vendorSnippets: true,
},
});
const slot = await slotDbService.getSlotById(id);
if (!slot) {
throw new ApiError("Slot not found", 404);
@ -355,8 +253,8 @@ export const slotsRouter = router({
...slot,
deliverySequence: slot.deliverySequence as number[],
groupIds: slot.groupIds as number[],
products: slot.productSlots.map((ps) => ps.product),
vendorSnippets: slot.vendorSnippets?.map(snippet => ({
products: slot.productSlots.map((ps: any) => ps.product),
vendorSnippets: slot.vendorSnippets?.map((snippet: any) => ({
...snippet,
accessUrl: `${appUrl}/vendor-order-list?id=${snippet.snippetCode}`
})),
@ -370,81 +268,60 @@ export const slotsRouter = router({
if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
}
try{
try {
const { id, deliveryTime, freezeTime, isActive, productIds, vendorSnippets: snippets, groupIds } = input;
if (!deliveryTime || !freezeTime) {
throw new ApiError("Delivery time and orders close time are required", 400);
}
// Filter groupIds to only include valid (existing) groups
let validGroupIds = groupIds;
if (groupIds && groupIds.length > 0) {
const existingGroups = await db.query.productGroupInfo.findMany({
where: inArray(productGroupInfo.id, groupIds),
columns: { id: true },
});
validGroupIds = existingGroups.map(g => g.id);
const existingGroups = await slotDbService.getGroupsByIds(groupIds);
validGroupIds = existingGroups.map((g: any) => g.id);
}
const result = await db.transaction(async (tx) => {
const [updatedSlot] = await tx
.update(deliverySlotInfo)
.set({
const result = await slotDbService.withTransaction(async (tx) => {
const updatedSlot = await slotDbService.updateSlot(id, {
deliveryTime: new Date(deliveryTime),
freezeTime: new Date(freezeTime),
isActive: isActive !== undefined ? isActive : true,
groupIds: validGroupIds !== undefined ? validGroupIds : [],
})
.where(eq(deliverySlotInfo.id, id))
.returning();
});
if (!updatedSlot) {
throw new ApiError("Slot not found", 404);
}
// Update product associations
if (productIds !== undefined) {
// Delete existing associations
await tx.delete(productSlots).where(eq(productSlots.slotId, id));
await slotDbService.deleteProductSlotsBySlotId(id);
// Insert new associations
if (productIds.length > 0) {
const associations = productIds.map((productId) => ({
productId,
slotId: id,
}));
await tx.insert(productSlots).values(associations);
for (const productId of productIds) {
await slotDbService.createProductSlot(id, productId);
}
}
}
// Create vendor snippets if provided
let createdSnippets: any[] = [];
if (snippets && snippets.length > 0) {
for (const snippet of snippets) {
// Validate products exist
const products = await tx.query.productInfo.findMany({
where: inArray(productInfo.id, snippet.productIds),
});
if (products.length !== snippet.productIds.length) {
const productsValid = await slotDbService.validateProductsExist(snippet.productIds);
if (!productsValid) {
throw new ApiError(`One or more invalid product IDs in snippet "${snippet.name}"`, 400);
}
// Check if snippet name already exists
const existingSnippet = await tx.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.snippetCode, snippet.name),
});
if (existingSnippet) {
const codeExists = await slotDbService.checkSnippetCodeExists(snippet.name);
if (codeExists) {
throw new ApiError(`Snippet name "${snippet.name}" already exists`, 400);
}
const [createdSnippet] = await tx.insert(vendorSnippets).values({
const createdSnippet = await slotDbService.createVendorSnippet({
snippetCode: snippet.name,
slotId: id,
productIds: snippet.productIds,
validTill: snippet.validTill ? new Date(snippet.validTill) : undefined,
}).returning();
});
createdSnippets.push(createdSnippet);
}
@ -457,13 +334,11 @@ export const slotsRouter = router({
};
});
// Reinitialize stores to reflect changes (outside transaction)
scheduleStoreInitialization()
scheduleStoreInitialization();
return result;
}
catch(e) {
console.log(e)
} catch (e) {
console.log(e);
throw new ApiError("Unable to Update Slot");
}
}),
@ -477,18 +352,13 @@ export const slotsRouter = router({
const { id } = input;
const [deletedSlot] = await db
.update(deliverySlotInfo)
.set({ isActive: false })
.where(eq(deliverySlotInfo.id, id))
.returning();
const deletedSlot = await slotDbService.deactivateSlot(id);
if (!deletedSlot) {
throw new ApiError("Slot not found", 404);
}
// Reinitialize stores to reflect changes
scheduleStoreInitialization()
scheduleStoreInitialization();
return {
message: "Slot deleted successfully",
@ -497,8 +367,7 @@ export const slotsRouter = router({
getDeliverySequence: protectedProcedure
.input(getDeliverySequenceSchema)
.query(async ({ input, ctx }) => {
.query(async ({ input }) => {
const { id } = input;
const slotId = parseInt(id);
const cacheKey = getSlotSequenceKey(slotId);
@ -508,19 +377,14 @@ export const slotsRouter = router({
if (cached) {
const parsed = JSON.parse(cached);
const validated = cachedSequenceSchema.parse(parsed) as CachedDeliverySequence;
console.log('sending cached response')
console.log('sending cached response');
return { deliverySequence: validated };
}
} catch (error) {
console.warn('Redis cache read/validation failed, falling back to DB:', error);
// Continue to DB fallback
}
// Fallback to DB
const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, slotId),
});
const slot = await slotDbService.getSlotById(slotId);
if (!slot) {
throw new ApiError("Slot not found", 404);
@ -528,7 +392,6 @@ export const slotsRouter = router({
const sequence = (slot.deliverySequence || {}) as CachedDeliverySequence;
// Cache the validated result
try {
const validated = cachedSequenceSchema.parse(sequence);
await redisClient.set(cacheKey, JSON.stringify(validated), 3600);
@ -548,20 +411,12 @@ export const slotsRouter = router({
const { id, deliverySequence } = input;
const [updatedSlot] = await db
.update(deliverySlotInfo)
.set({ deliverySequence })
.where(eq(deliverySlotInfo.id, id))
.returning({
id: deliverySlotInfo.id,
deliverySequence: deliverySlotInfo.deliverySequence,
});
const updatedSlot = await slotDbService.updateSlot(id, { deliverySequence });
if (!updatedSlot) {
throw new ApiError("Slot not found", 404);
}
// Cache the updated sequence
const cacheKey = getSlotSequenceKey(id);
try {
const validated = cachedSequenceSchema.parse(deliverySequence);
@ -571,7 +426,7 @@ export const slotsRouter = router({
}
return {
slot: updatedSlot,
slot: { id: updatedSlot.id, deliverySequence: updatedSlot.deliverySequence },
message: "Delivery sequence updated successfully",
};
}),
@ -588,18 +443,13 @@ export const slotsRouter = router({
const { slotId, isCapacityFull } = input;
const [updatedSlot] = await db
.update(deliverySlotInfo)
.set({ isCapacityFull })
.where(eq(deliverySlotInfo.id, slotId))
.returning();
const updatedSlot = await slotDbService.updateSlot(slotId, { isCapacityFull });
if (!updatedSlot) {
throw new ApiError("Slot not found", 404);
}
// Reinitialize stores to reflect changes
scheduleStoreInitialization()
scheduleStoreInitialization();
return {
success: true,

View file

@ -1,11 +1,9 @@
import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod';
import { db } from '@/src/db/db_index'
import { staffUsers, staffRoles, users, userDetails, orders } from '@/src/db/schema'
import { eq, or, ilike, and, lt, desc } from 'drizzle-orm';
import bcrypt from 'bcryptjs';
import { ApiError } from '@/src/lib/api-error'
import { signToken } from '@/src/lib/jwt-utils'
import { staffUserDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/main'
export const staffUserRouter = router({
login: publicProcedure
@ -20,9 +18,7 @@ export const staffUserRouter = router({
throw new ApiError('Name and password are required', 400);
}
const staff = await db.query.staffUsers.findFirst({
where: eq(staffUsers.name, name),
});
const staff = await staffUserDbService.getStaffUserByName(name);
if (!staff) {
throw new ApiError('Invalid credentials', 401);
@ -46,24 +42,8 @@ export const staffUserRouter = router({
}),
getStaff: protectedProcedure
.query(async ({ ctx }) => {
const staff = await db.query.staffUsers.findMany({
columns: {
id: true,
name: true,
},
with: {
role: {
with: {
rolePermissions: {
with: {
permission: true,
},
},
},
},
},
});
.query(async () => {
const staff = await staffUserDbService.getAllStaff();
// Transform the data to include role and permissions in a cleaner format
const transformedStaff = staff.map((user) => ({
@ -93,29 +73,7 @@ export const staffUserRouter = router({
.query(async ({ input }) => {
const { cursor, limit, search } = input;
let whereCondition = undefined;
if (search) {
whereCondition = or(
ilike(users.name, `%${search}%`),
ilike(users.email, `%${search}%`),
ilike(users.mobile, `%${search}%`)
);
}
if (cursor) {
const cursorCondition = lt(users.id, cursor);
whereCondition = whereCondition ? and(whereCondition, cursorCondition) : cursorCondition;
}
const allUsers = await db.query.users.findMany({
where: whereCondition,
with: {
userDetails: true,
},
orderBy: desc(users.id),
limit: limit + 1, // fetch one extra to check if there's more
});
const allUsers = await staffUserDbService.getUsers({ cursor, limit, search });
const hasMore = allUsers.length > limit;
const usersToReturn = hasMore ? allUsers.slice(0, limit) : allUsers;
@ -139,22 +97,13 @@ export const staffUserRouter = router({
.query(async ({ input }) => {
const { userId } = input;
const user = await db.query.users.findFirst({
where: eq(users.id, userId),
with: {
userDetails: true,
orders: {
orderBy: desc(orders.createdAt),
limit: 1,
},
},
});
const user = await staffUserDbService.getUserById(userId);
if (!user) {
throw new ApiError("User not found", 404);
}
const lastOrder = user.orders[0];
const lastOrder = user.orders?.[0];
return {
id: user.id,
@ -172,13 +121,7 @@ export const staffUserRouter = router({
.mutation(async ({ input }) => {
const { userId, isSuspended } = input;
await db
.insert(userDetails)
.values({ userId, isSuspended })
.onConflictDoUpdate({
target: userDetails.userId,
set: { isSuspended },
});
await staffUserDbService.upsertUserDetails({ userId, isSuspended });
return { success: true };
}),
@ -189,22 +132,18 @@ export const staffUserRouter = router({
password: z.string().min(6, 'Password must be at least 6 characters'),
roleId: z.number().int().positive('Role is required'),
}))
.mutation(async ({ input, ctx }) => {
.mutation(async ({ input }) => {
const { name, password, roleId } = input;
// Check if staff user already exists
const existingUser = await db.query.staffUsers.findFirst({
where: eq(staffUsers.name, name),
});
const existingUser = await staffUserDbService.getStaffUserByName(name);
if (existingUser) {
throw new ApiError('Staff user with this name already exists', 409);
}
// Check if role exists
const role = await db.query.staffRoles.findFirst({
where: eq(staffRoles.id, roleId),
});
const role = await staffUserDbService.getRoleById(roleId);
if (!role) {
throw new ApiError('Invalid role selected', 400);
@ -214,23 +153,18 @@ export const staffUserRouter = router({
const hashedPassword = await bcrypt.hash(password, 12);
// Create staff user
const [newUser] = await db.insert(staffUsers).values({
const newUser = await staffUserDbService.createStaffUser({
name: name.trim(),
password: hashedPassword,
staffRoleId: roleId,
}).returning();
});
return { success: true, user: { id: newUser.id, name: newUser.name } };
}),
getRoles: protectedProcedure
.query(async ({ ctx }) => {
const roles = await db.query.staffRoles.findMany({
columns: {
id: true,
roleName: true,
},
});
.query(async () => {
const roles = await staffUserDbService.getAllRoles();
return {
roles: roles.map(role => ({

View file

@ -1,30 +1,22 @@
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod';
import { db } from '@/src/db/db_index'
import { storeInfo, productInfo } from '@/src/db/schema'
import { eq, inArray } from 'drizzle-orm';
import { ApiError } from '@/src/lib/api-error'
import { extractKeyFromPresignedUrl, deleteImageUtil, scaffoldAssetUrl } from '@/src/lib/s3-client'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { extractKeyFromPresignedUrl, deleteImageUtil, scaffoldAssetUrl } from '@/src/lib/s3-client'
import { scheduleStoreInitialization } from '@/src/stores/store-initializer'
import { storeDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/main'
export const storeRouter = router({
getStores: protectedProcedure
.query(async ({ ctx }) => {
const stores = await db.query.storeInfo.findMany({
with: {
owner: true,
},
});
.query(async () => {
const stores = await storeDbService.getAllStores();
Promise.all(stores.map(async store => {
if(store.imageUrl)
store.imageUrl = scaffoldAssetUrl(store.imageUrl)
})).catch((e) => {
throw new ApiError("Unable to find store image urls")
}
)
})
return {
stores,
count: stores.length,
@ -35,20 +27,17 @@ export const storeRouter = router({
.input(z.object({
id: z.number(),
}))
.query(async ({ input, ctx }) => {
.query(async ({ input }) => {
const { id } = input;
const store = await db.query.storeInfo.findFirst({
where: eq(storeInfo.id, id),
with: {
owner: true,
},
});
const store = await storeDbService.getStoreById(id);
if (!store) {
throw new ApiError("Store not found", 404);
}
store.imageUrl = scaffoldAssetUrl(store.imageUrl);
return {
store,
};
@ -62,31 +51,21 @@ export const storeRouter = router({
owner: z.number().min(1, "Owner is required"),
products: z.array(z.number()).optional(),
}))
.mutation(async ({ input, ctx }) => {
.mutation(async ({ input }) => {
const { name, description, imageUrl, owner, products } = input;
// const imageKey = imageUrl ? extractKeyFromPresignedUrl(imageUrl) : undefined;
const imageKey = imageUrl
const [newStore] = await db
.insert(storeInfo)
.values({
const newStore = await storeDbService.createStore({
name,
description,
imageUrl: imageKey,
imageUrl: imageUrl || null,
owner,
})
.returning();
});
// Assign selected products to this store
if (products && products.length > 0) {
await db
.update(productInfo)
.set({ storeId: newStore.id })
.where(inArray(productInfo.id, products));
await storeDbService.assignProductsToStore(newStore.id, products);
}
// Reinitialize stores to reflect changes
scheduleStoreInitialization()
return {
@ -104,12 +83,10 @@ export const storeRouter = router({
owner: z.number().min(1, "Owner is required"),
products: z.array(z.number()).optional(),
}))
.mutation(async ({ input, ctx }) => {
.mutation(async ({ input }) => {
const { id, name, description, imageUrl, owner, products } = input;
const existingStore = await db.query.storeInfo.findFirst({
where: eq(storeInfo.id, id),
});
const existingStore = await storeDbService.getStoreById(id);
if (!existingStore) {
throw new ApiError("Store not found", 404);
@ -129,43 +106,27 @@ export const storeRouter = router({
await deleteImageUtil({keys: [oldImageKey]});
} catch (error) {
console.error('Failed to delete old image:', error);
// Continue with update even if deletion fails
}
}
const [updatedStore] = await db
.update(storeInfo)
.set({
const updatedStore = await storeDbService.updateStore(id, {
name,
description,
imageUrl: newImageKey,
owner,
})
.where(eq(storeInfo.id, id))
.returning();
if (!updatedStore) {
throw new ApiError("Store not found", 404);
}
});
// Update products if provided
if (products) {
// First, set storeId to null for products not in the list but currently assigned to this store
await db
.update(productInfo)
.set({ storeId: null })
.where(eq(productInfo.storeId, id));
// First, remove all products from this store
await storeDbService.removeProductsFromStore(id);
// Then, assign the selected products to this store
if (products.length > 0) {
await db
.update(productInfo)
.set({ storeId: id })
.where(inArray(productInfo.id, products));
await storeDbService.assignProductsToStore(id, products);
}
}
// Reinitialize stores to reflect changes
scheduleStoreInitialization()
return {
@ -178,34 +139,19 @@ export const storeRouter = router({
.input(z.object({
storeId: z.number(),
}))
.mutation(async ({ input, ctx }) => {
.mutation(async ({ input }) => {
const { storeId } = input;
const result = await db.transaction(async (tx) => {
// First, update all products of this store to set storeId to null
await tx
.update(productInfo)
.set({ storeId: null })
.where(eq(productInfo.storeId, storeId));
// First, remove all products from this store
await storeDbService.removeProductsFromStore(storeId);
// Then delete the store
const [deletedStore] = await tx
.delete(storeInfo)
.where(eq(storeInfo.id, storeId))
.returning();
await storeDbService.deleteStore(storeId);
if (!deletedStore) {
throw new ApiError("Store not found", 404);
}
scheduleStoreInitialization()
return {
message: "Store deleted successfully",
};
});
// Reinitialize stores to reflect changes (outside transaction)
scheduleStoreInitialization()
return result;
}),
});
});

View file

@ -1,20 +1,15 @@
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod';
import { db } from '@/src/db/db_index'
import { productTagInfo } from '@/src/db/schema'
import { eq } from 'drizzle-orm';
import { ApiError } from '@/src/lib/api-error'
import { scaffoldAssetUrl, claimUploadUrl } from '@/src/lib/s3-client'
import { deleteS3Image } from '@/src/lib/delete-image'
import { scheduleStoreInitialization } from '@/src/stores/store-initializer'
import { tagDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/main'
export const tagRouter = router({
getTags: protectedProcedure
.query(async () => {
const tags = await db
.select()
.from(productTagInfo)
.orderBy(productTagInfo.tagName);
const tags = await tagDbService.getAllTags();
// Generate asset URLs for tag images
const tagsWithUrls = tags.map(tag => ({
@ -33,9 +28,7 @@ export const tagRouter = router({
id: z.number(),
}))
.query(async ({ input }) => {
const tag = await db.query.productTagInfo.findFirst({
where: eq(productTagInfo.id, input.id),
});
const tag = await tagDbService.getTagById(input.id);
if (!tag) {
throw new ApiError("Tag not found", 404);
@ -65,24 +58,19 @@ export const tagRouter = router({
const { tagName, tagDescription, isDashboardTag, relatedStores, imageKey } = input;
// Check for duplicate tag name
const existingTag = await db.query.productTagInfo.findFirst({
where: eq(productTagInfo.tagName, tagName.trim()),
});
const existingTag = await tagDbService.getTagByName(tagName);
if (existingTag) {
throw new ApiError("A tag with this name already exists", 400);
}
const [newTag] = await db
.insert(productTagInfo)
.values({
const newTag = await tagDbService.createTag({
tagName: tagName.trim(),
tagDescription,
imageUrl: imageKey || null,
isDashboardTag,
relatedStores,
})
.returning();
});
// Claim upload URL if image was provided
if (imageKey) {
@ -115,9 +103,7 @@ export const tagRouter = router({
const { id, imageKey, deleteExistingImage, ...updateData } = input;
// Get current tag
const currentTag = await db.query.productTagInfo.findFirst({
where: eq(productTagInfo.id, id),
});
const currentTag = await tagDbService.getTagById(id);
if (!currentTag) {
throw new ApiError("Tag not found", 404);
@ -155,17 +141,13 @@ export const tagRouter = router({
}
}
const [updatedTag] = await db
.update(productTagInfo)
.set({
const updatedTag = await tagDbService.updateTag(id, {
tagName: updateData.tagName.trim(),
tagDescription: updateData.tagDescription,
isDashboardTag: updateData.isDashboardTag,
relatedStores: updateData.relatedStores,
imageUrl: newImageUrl,
})
.where(eq(productTagInfo.id, id))
.returning();
});
scheduleStoreInitialization();
@ -183,9 +165,7 @@ export const tagRouter = router({
const { id } = input;
// Get tag to check for image
const tag = await db.query.productTagInfo.findFirst({
where: eq(productTagInfo.id, id),
});
const tag = await tagDbService.getTagById(id);
if (!tag) {
throw new ApiError("Tag not found", 404);
@ -201,7 +181,7 @@ export const tagRouter = router({
}
// Delete tag (will fail if tag is assigned to products due to FK constraint)
await db.delete(productTagInfo).where(eq(productTagInfo.id, id));
await tagDbService.deleteTag(id);
scheduleStoreInitialization();

View file

@ -1,41 +1,28 @@
import { protectedProcedure } from '@/src/trpc/trpc-index';
import { z } from 'zod';
import { db } from '@/src/db/db_index';
import { users, complaints, orders, orderItems, notifCreds, unloggedUserTokens, userDetails, userIncidents } from '@/src/db/schema';
import { eq, sql, desc, asc, count, max, inArray } from 'drizzle-orm';
import { ApiError } from '@/src/lib/api-error';
import { notificationQueue } from '@/src/lib/notif-job';
import { recomputeUserNegativityScore } from '@/src/stores/user-negativity-store';
import { userDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/main';
async function createUserByMobile(mobile: string): Promise<typeof users.$inferSelect> {
// Clean mobile number (remove non-digits)
async function createUserByMobile(mobile: string) {
const cleanMobile = mobile.replace(/\D/g, '');
// Validate: exactly 10 digits
if (cleanMobile.length !== 10) {
throw new ApiError('Mobile number must be exactly 10 digits', 400);
}
// Check if user already exists
const [existingUser] = await db
.select()
.from(users)
.where(eq(users.mobile, cleanMobile))
.limit(1);
const existingUser = await userDbService.getUserByMobile(cleanMobile);
if (existingUser) {
throw new ApiError('User with this mobile number already exists', 409);
}
// Create user
const [newUser] = await db
.insert(users)
.values({
const newUser = await userDbService.createUser({
name: null,
email: null,
mobile: cleanMobile,
})
.returning();
});
return newUser;
}
@ -56,7 +43,7 @@ export const userRouter = {
getEssentials: protectedProcedure
.query(async () => {
const count = await db.$count(complaints, eq(complaints.isResolved, false));
const count = await userDbService.getUnresolvedComplaintCount();
return {
unresolvedComplaints: count || 0,
@ -72,78 +59,23 @@ export const userRouter = {
.query(async ({ input }) => {
const { limit, cursor, search } = input;
// Build where conditions
const whereConditions = [];
const usersList = await userDbService.getUsers({ limit, cursor, search });
if (search && search.trim()) {
whereConditions.push(sql`${users.mobile} ILIKE ${`%${search.trim()}%`}`);
}
if (cursor) {
whereConditions.push(sql`${users.id} > ${cursor}`);
}
// Get users with filters applied
const usersList = await db
.select({
id: users.id,
name: users.name,
mobile: users.mobile,
createdAt: users.createdAt,
})
.from(users)
.where(whereConditions.length > 0 ? sql.join(whereConditions, sql` AND `) : undefined)
.orderBy(asc(users.id))
.limit(limit + 1); // Get one extra to determine if there's more
// Check if there are more results
const hasMore = usersList.length > limit;
const usersToReturn = hasMore ? usersList.slice(0, limit) : usersList;
// Get order stats for each user
const userIds = usersToReturn.map(u => u.id);
let orderCounts: { userId: number; totalOrders: number }[] = [];
let lastOrders: { userId: number; lastOrderDate: Date | null }[] = [];
let suspensionStatuses: { userId: number; isSuspended: boolean }[] = [];
const orderCounts = await userDbService.getOrderCountByUserIds(userIds);
const lastOrders = await userDbService.getLastOrderDateByUserIds(userIds);
if (userIds.length > 0) {
// Get total orders per user
orderCounts = await db
.select({
userId: orders.userId,
totalOrders: count(orders.id),
})
.from(orders)
.where(sql`${orders.userId} IN (${sql.join(userIds, sql`, `)})`)
.groupBy(orders.userId);
const userDetailsList = await Promise.all(
userIds.map(id => userDbService.getUserDetailsByUserId(id))
);
// Get last order date per user
lastOrders = await db
.select({
userId: orders.userId,
lastOrderDate: max(orders.createdAt),
})
.from(orders)
.where(sql`${orders.userId} IN (${sql.join(userIds, sql`, `)})`)
.groupBy(orders.userId);
// Get suspension status for each user
suspensionStatuses = await db
.select({
userId: userDetails.userId,
isSuspended: userDetails.isSuspended,
})
.from(userDetails)
.where(sql`${userDetails.userId} IN (${sql.join(userIds, sql`, `)})`);
}
// Create lookup maps
const orderCountMap = new Map(orderCounts.map(o => [o.userId, o.totalOrders]));
const lastOrderMap = new Map(lastOrders.map(o => [o.userId, o.lastOrderDate]));
const suspensionMap = new Map(suspensionStatuses.map(s => [s.userId, s.isSuspended]));
const suspensionMap = new Map(userDetailsList.map((ud, idx) => [userIds[idx], ud?.isSuspended ?? false]));
// Combine data
const usersWithStats = usersToReturn.map(user => ({
...user,
totalOrders: orderCountMap.get(user.id) || 0,
@ -151,7 +83,6 @@ export const userRouter = {
isSuspended: suspensionMap.get(user.id) ?? false,
}));
// Get next cursor
const nextCursor = hasMore ? usersToReturn[usersToReturn.length - 1].id : undefined;
return {
@ -168,76 +99,22 @@ export const userRouter = {
.query(async ({ input }) => {
const { userId } = input;
// Get user info
const user = await db
.select({
id: users.id,
name: users.name,
mobile: users.mobile,
createdAt: users.createdAt,
})
.from(users)
.where(eq(users.id, userId))
.limit(1);
const user = await userDbService.getUserById(userId);
if (!user || user.length === 0) {
if (!user) {
throw new ApiError('User not found', 404);
}
// Get user suspension status
const userDetail = await db
.select({
isSuspended: userDetails.isSuspended,
})
.from(userDetails)
.where(eq(userDetails.userId, userId))
.limit(1);
// Get all orders for this user with order items count
const userOrders = await db
.select({
id: orders.id,
readableId: orders.readableId,
totalAmount: orders.totalAmount,
createdAt: orders.createdAt,
isFlashDelivery: orders.isFlashDelivery,
})
.from(orders)
.where(eq(orders.userId, userId))
.orderBy(desc(orders.createdAt));
// Get order status for each order
const userDetail = await userDbService.getUserDetailsByUserId(userId);
const userOrders = await userDbService.getOrdersByUserId(userId);
const orderIds = userOrders.map(o => o.id);
let orderStatuses: { orderId: number; isDelivered: boolean; isCancelled: boolean }[] = [];
const orderStatuses = await userDbService.getOrderStatusByOrderIds(orderIds);
const itemCounts = await userDbService.getOrderItemCountByOrderIds(orderIds);
if (orderIds.length > 0) {
const { orderStatus } = await import('@/src/db/schema');
orderStatuses = await db
.select({
orderId: orderStatus.orderId,
isDelivered: orderStatus.isDelivered,
isCancelled: orderStatus.isCancelled,
})
.from(orderStatus)
.where(sql`${orderStatus.orderId} IN (${sql.join(orderIds, sql`, `)})`);
}
// Get item counts for each order
const itemCounts = await db
.select({
orderId: orderItems.orderId,
itemCount: count(orderItems.id),
})
.from(orderItems)
.where(sql`${orderItems.orderId} IN (${sql.join(orderIds, sql`, `)})`)
.groupBy(orderItems.orderId);
// Create lookup maps
const statusMap = new Map(orderStatuses.map(s => [s.orderId, s]));
const itemCountMap = new Map(itemCounts.map(c => [c.orderId, c.itemCount]));
// Determine status string
const getStatus = (status: { isDelivered: boolean; isCancelled: boolean } | undefined) => {
if (!status) return 'pending';
if (status.isCancelled) return 'cancelled';
@ -245,15 +122,14 @@ export const userRouter = {
return 'pending';
};
// Combine data
const ordersWithDetails = userOrders.map(order => {
const status = statusMap.get(order.id);
return {
id: order.id,
readableId: order.readableId,
readableId: (order as any).readableId,
totalAmount: order.totalAmount,
createdAt: order.createdAt,
isFlashDelivery: order.isFlashDelivery,
isFlashDelivery: (order as any).isFlashDelivery,
status: getStatus(status),
itemCount: itemCountMap.get(order.id) || 0,
};
@ -261,8 +137,8 @@ export const userRouter = {
return {
user: {
...user[0],
isSuspended: userDetail[0]?.isSuspended ?? false,
...user,
isSuspended: userDetail?.isSuspended ?? false,
},
orders: ordersWithDetails,
};
@ -276,39 +152,13 @@ export const userRouter = {
.mutation(async ({ input }) => {
const { userId, isSuspended } = input;
// Check if user exists
const user = await db
.select({ id: users.id })
.from(users)
.where(eq(users.id, userId))
.limit(1);
const user = await userDbService.getUserById(userId);
if (!user || user.length === 0) {
if (!user) {
throw new ApiError('User not found', 404);
}
// Check if user_details record exists
const existingDetail = await db
.select({ id: userDetails.id })
.from(userDetails)
.where(eq(userDetails.userId, userId))
.limit(1);
if (existingDetail.length > 0) {
// Update existing record
await db
.update(userDetails)
.set({ isSuspended })
.where(eq(userDetails.userId, userId));
} else {
// Insert new record
await db
.insert(userDetails)
.values({
userId,
isSuspended,
});
}
await userDbService.upsertUserDetails({ userId, isSuspended });
return {
success: true,
@ -323,40 +173,17 @@ export const userRouter = {
.query(async ({ input }) => {
const { search } = input;
// Get all users
let usersList;
if (search && search.trim()) {
usersList = await db
.select({
id: users.id,
name: users.name,
mobile: users.mobile,
})
.from(users)
.where(sql`${users.mobile} ILIKE ${`%${search.trim()}%`} OR ${users.name} ILIKE ${`%${search.trim()}%`}`);
} else {
usersList = await db
.select({
id: users.id,
name: users.name,
mobile: users.mobile,
})
.from(users);
}
const usersList = await userDbService.getUsers({ limit: 1000, search });
// Get eligible users (have notif_creds entry)
const eligibleUsers = await db
.select({ userId: notifCreds.userId })
.from(notifCreds);
const eligibleSet = new Set(eligibleUsers.map(u => u.userId));
const allTokens = await userDbService.getAllNotifTokens();
const eligibleSet = new Set(allTokens);
return {
users: usersList.map(user => ({
id: user.id,
name: user.name,
mobile: user.mobile,
isEligibleForNotif: eligibleSet.has(user.id),
isEligibleForNotif: eligibleSet.has(user.mobile || ''),
})),
};
}),
@ -374,25 +201,13 @@ export const userRouter = {
let tokens: string[] = [];
if (userIds.length === 0) {
// Send to all users - get tokens from both logged-in and unlogged users
const loggedInTokens = await db.select({ token: notifCreds.token }).from(notifCreds);
const unloggedTokens = await db.select({ token: unloggedUserTokens.token }).from(unloggedUserTokens);
tokens = [
...loggedInTokens.map(t => t.token),
...unloggedTokens.map(t => t.token)
];
const allTokens = await userDbService.getAllNotifTokens();
const unloggedTokens = await userDbService.getUnloggedTokens();
tokens = [...allTokens, ...unloggedTokens];
} else {
// Send to specific users - get their tokens
const userTokens = await db
.select({ token: notifCreds.token })
.from(notifCreds)
.where(inArray(notifCreds.userId, userIds));
tokens = userTokens.map(t => t.token);
tokens = await userDbService.getNotifTokensByUserIds(userIds);
}
// Queue one job per token
let queuedCount = 0;
for (const token of tokens) {
try {
@ -427,18 +242,7 @@ export const userRouter = {
.query(async ({ input }) => {
const { userId } = input;
const incidents = await db.query.userIncidents.findMany({
where: eq(userIncidents.userId, userId),
with: {
order: {
with: {
orderStatus: true,
},
},
addedBy: true,
},
orderBy: desc(userIncidents.dateAdded),
});
const incidents = await userDbService.getUserIncidentsByUserId(userId);
return {
incidents: incidents.map(incident => ({
@ -470,14 +274,13 @@ export const userRouter = {
throw new ApiError('Admin user not authenticated', 401);
}
const incidentObj = { userId, orderId, adminComment, addedBy: adminUserId, negativityScore };
const [incident] = await db.insert(userIncidents)
.values({
...incidentObj,
})
.returning();
const incident = await userDbService.createUserIncident({
userId,
orderId: orderId || null,
adminComment: adminComment || null,
addedBy: adminUserId,
negativityScore: negativityScore || null,
});
recomputeUserNegativityScore(userId);

View file

@ -1,10 +1,8 @@
import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod';
import dayjs from 'dayjs';
import { db } from '@/src/db/db_index'
import { vendorSnippets, deliverySlotInfo, productInfo, orders, orderItems, users, orderStatus } from '@/src/db/schema'
import { eq, and, inArray, isNotNull, gt, sql, asc, ne } from 'drizzle-orm';
import { appUrl } from '@/src/lib/env-exporter'
import { vendorSnippetDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/main'
const createSnippetSchema = z.object({
snippetCode: z.string().min(1, "Snippet code is required"),
@ -29,7 +27,6 @@ export const vendorSnippetsRouter = router({
.mutation(async ({ input, ctx }) => {
const { snippetCode, slotId, productIds, validTill, isPermanent } = input;
// Get staff user ID from auth middleware
const staffUserId = ctx.staffUser?.id;
if (!staffUserId) {
throw new Error("Unauthorized");
@ -37,59 +34,42 @@ export const vendorSnippetsRouter = router({
// Validate slot exists
if(slotId) {
const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, slotId),
});
const slot = await vendorSnippetDbService.getSlotById(slotId);
if (!slot) {
throw new Error("Invalid slot ID");
}
}
// Validate products exist
const products = await db.query.productInfo.findMany({
where: inArray(productInfo.id, productIds),
});
if (products.length !== productIds.length) {
const productsValid = await vendorSnippetDbService.validateProductsExist(productIds);
if (!productsValid) {
throw new Error("One or more invalid product IDs");
}
// Check if snippet code already exists
const existingSnippet = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.snippetCode, snippetCode),
});
if (existingSnippet) {
const codeExists = await vendorSnippetDbService.checkSnippetCodeExists(snippetCode);
if (codeExists) {
throw new Error("Snippet code already exists");
}
const result = await db.insert(vendorSnippets).values({
const result = await vendorSnippetDbService.createSnippet({
snippetCode,
slotId,
slotId: slotId || null,
productIds,
isPermanent,
validTill: validTill ? new Date(validTill) : undefined,
}).returning();
validTill: validTill ? new Date(validTill) : null,
});
return result[0];
return result;
}),
getAll: protectedProcedure
.query(async () => {
console.log('from the vendor snipptes methods')
try {
const result = await db.query.vendorSnippets.findMany({
with: {
slot: true,
},
orderBy: (vendorSnippets, { desc }) => [desc(vendorSnippets.createdAt)],
});
const result = await vendorSnippetDbService.getAllSnippets();
const snippetsWithProducts = await Promise.all(
result.map(async (snippet) => {
const products = await db.query.productInfo.findMany({
where: inArray(productInfo.id, snippet.productIds),
columns: { id: true, name: true },
});
const products = await vendorSnippetDbService.getProductsByIds(snippet.productIds);
return {
...snippet,
@ -100,24 +80,12 @@ export const vendorSnippetsRouter = router({
);
return snippetsWithProducts;
}
catch(e) {
console.log(e)
}
return [];
}),
getById: protectedProcedure
.input(z.object({ id: z.number().int().positive() }))
.query(async ({ input }) => {
const { id } = input;
const result = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.id, id),
with: {
slot: true,
},
});
const result = await vendorSnippetDbService.getSnippetById(input.id);
if (!result) {
throw new Error("Vendor snippet not found");
@ -131,19 +99,14 @@ export const vendorSnippetsRouter = router({
.mutation(async ({ input }) => {
const { id, updates } = input;
// Check if snippet exists
const existingSnippet = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.id, id),
});
const existingSnippet = await vendorSnippetDbService.getSnippetById(id);
if (!existingSnippet) {
throw new Error("Vendor snippet not found");
}
// Validate slot if being updated
if (updates.slotId) {
const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, updates.slotId),
});
const slot = await vendorSnippetDbService.getSlotById(updates.slotId);
if (!slot) {
throw new Error("Invalid slot ID");
}
@ -151,20 +114,16 @@ export const vendorSnippetsRouter = router({
// Validate products if being updated
if (updates.productIds) {
const products = await db.query.productInfo.findMany({
where: inArray(productInfo.id, updates.productIds),
});
if (products.length !== updates.productIds.length) {
const productsValid = await vendorSnippetDbService.validateProductsExist(updates.productIds);
if (!productsValid) {
throw new Error("One or more invalid product IDs");
}
}
// Check snippet code uniqueness if being updated
if (updates.snippetCode && updates.snippetCode !== existingSnippet.snippetCode) {
const duplicateSnippet = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.snippetCode, updates.snippetCode),
});
if (duplicateSnippet) {
const codeExists = await vendorSnippetDbService.checkSnippetCodeExists(updates.snippetCode);
if (codeExists) {
throw new Error("Snippet code already exists");
}
}
@ -174,91 +133,46 @@ export const vendorSnippetsRouter = router({
updateData.validTill = updates.validTill ? new Date(updates.validTill) : null;
}
const result = await db.update(vendorSnippets)
.set(updateData)
.where(eq(vendorSnippets.id, id))
.returning();
if (result.length === 0) {
throw new Error("Failed to update vendor snippet");
}
return result[0];
const result = await vendorSnippetDbService.updateSnippet(id, updateData);
return result;
}),
delete: protectedProcedure
.input(z.object({ id: z.number().int().positive() }))
.mutation(async ({ input }) => {
const { id } = input;
const result = await db.delete(vendorSnippets)
.where(eq(vendorSnippets.id, id))
.returning();
if (result.length === 0) {
throw new Error("Vendor snippet not found");
}
await vendorSnippetDbService.deleteSnippet(input.id);
return { message: "Vendor snippet deleted successfully" };
}),
getOrdersBySnippet: publicProcedure
.input(z.object({
snippetCode: z.string().min(1, "Snippet code is required")
}))
.input(z.object({ snippetCode: z.string().min(1, "Snippet code is required") }))
.query(async ({ input }) => {
const { snippetCode } = input;
// Find the snippet
const snippet = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.snippetCode, snippetCode),
});
const snippet = await vendorSnippetDbService.getSnippetByCode(input.snippetCode);
if (!snippet) {
throw new Error("Vendor snippet not found");
}
// Check if snippet is still valid
if (snippet.validTill && new Date(snippet.validTill) < new Date()) {
throw new Error("Vendor snippet has expired");
}
// Query orders that match the snippet criteria
const matchingOrders = await db.query.orders.findMany({
where: eq(orders.slotId, snippet.slotId!),
with: {
orderItems: {
with: {
product: {
with: {
unit: true,
},
},
},
},
orderStatus: true,
user: true,
slot: true,
},
orderBy: (orders, { desc }) => [desc(orders.createdAt)],
});
const matchingOrders = await vendorSnippetDbService.getOrdersBySlotId(snippet.slotId!);
// Filter orders that contain at least one of the snippet's products
const filteredOrders = matchingOrders.filter(order => {
// Filter and format orders
const formattedOrders = matchingOrders
.filter((order: any) => {
const status = order.orderStatus;
if (status[0].isCancelled) return false;
const orderProductIds = order.orderItems.map(item => item.productId);
if (status?.[0]?.isCancelled) return false;
const orderProductIds = order.orderItems.map((item: any) => item.productId);
return snippet.productIds.some(productId => orderProductIds.includes(productId));
});
// Format the response
const formattedOrders = filteredOrders.map(order => {
// Filter orderItems to only include products attached to the snippet
const attachedOrderItems = order.orderItems.filter(item =>
})
.map((order: any) => {
const attachedOrderItems = order.orderItems.filter((item: any) =>
snippet.productIds.includes(item.productId)
);
const products = attachedOrderItems.map(item => ({
const products = attachedOrderItems.map((item: any) => ({
orderItemId: item.id,
productId: item.productId,
productName: item.product.name,
@ -271,7 +185,7 @@ export const vendorSnippetsRouter = router({
is_package_verified: item.is_package_verified,
}));
const orderTotal = products.reduce((sum, p) => sum + p.subtotal, 0);
const orderTotal = products.reduce((sum: number, p: any) => sum + p.subtotal, 0);
return {
orderId: `ORD${order.id}`,
@ -283,7 +197,7 @@ export const vendorSnippetsRouter = router({
sequence: order.slot.deliverySequence,
} : null,
products,
matchedProducts: snippet.productIds, // All snippet products are considered matched
matchedProducts: snippet.productIds,
snippetCode: snippet.snippetCode,
};
});
@ -305,45 +219,14 @@ export const vendorSnippetsRouter = router({
getVendorOrders: protectedProcedure
.query(async () => {
const vendorOrders = await db.query.orders.findMany({
with: {
user: true,
orderItems: {
with: {
product: {
with: {
unit: true,
},
},
},
},
},
orderBy: (orders, { desc }) => [desc(orders.createdAt)],
});
return vendorOrders.map(order => ({
id: order.id,
status: 'pending', // Default status since orders table may not have status field
orderDate: order.createdAt.toISOString(),
totalQuantity: order.orderItems.reduce((sum, item) => sum + parseFloat(item.quantity || '0'), 0),
products: order.orderItems.map(item => ({
name: item.product.name,
quantity: parseFloat(item.quantity || '0'),
unit: item.product.unit?.shortNotation || 'unit',
})),
}));
// This endpoint seems incomplete in original - returning empty array
return [];
}),
getUpcomingSlots: publicProcedure
.query(async () => {
const threeHoursAgo = dayjs().subtract(3, 'hour').toDate();
const slots = await db.query.deliverySlotInfo.findMany({
where: and(
eq(deliverySlotInfo.isActive, true),
gt(deliverySlotInfo.deliveryTime, threeHoursAgo)
),
orderBy: asc(deliverySlotInfo.deliveryTime),
});
const slots = await vendorSnippetDbService.getUpcomingSlots(threeHoursAgo);
return {
success: true,
@ -364,60 +247,31 @@ export const vendorSnippetsRouter = router({
.query(async ({ input }) => {
const { snippetCode, slotId } = input;
// Find the snippet
const snippet = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.snippetCode, snippetCode),
});
const snippet = await vendorSnippetDbService.getSnippetByCode(snippetCode);
if (!snippet) {
throw new Error("Vendor snippet not found");
}
// Find the slot
const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, slotId),
});
const slot = await vendorSnippetDbService.getSlotById(slotId);
if (!slot) {
throw new Error("Slot not found");
}
// Query orders that match the slot and snippet criteria
const matchingOrders = await db.query.orders.findMany({
where: eq(orders.slotId, slotId),
with: {
orderItems: {
with: {
product: {
with: {
unit: true,
},
},
},
},
orderStatus: true,
user: true,
slot: true,
},
orderBy: (orders, { desc }) => [desc(orders.createdAt)],
});
const matchingOrders = await vendorSnippetDbService.getOrdersBySlotId(slotId);
// Filter orders that contain at least one of the snippet's products
const filteredOrders = matchingOrders.filter(order => {
const formattedOrders = matchingOrders
.filter((order: any) => {
const status = order.orderStatus;
if (status[0]?.isCancelled) return false;
const orderProductIds = order.orderItems.map(item => item.productId);
if (status?.[0]?.isCancelled) return false;
const orderProductIds = order.orderItems.map((item: any) => item.productId);
return snippet.productIds.some(productId => orderProductIds.includes(productId));
});
// Format the response
const formattedOrders = filteredOrders.map(order => {
// Filter orderItems to only include products attached to the snippet
const attachedOrderItems = order.orderItems.filter(item =>
})
.map((order: any) => {
const attachedOrderItems = order.orderItems.filter((item: any) =>
snippet.productIds.includes(item.productId)
);
const products = attachedOrderItems.map(item => ({
const products = attachedOrderItems.map((item: any) => ({
orderItemId: item.id,
productId: item.productId,
productName: item.product.name,
@ -430,7 +284,7 @@ export const vendorSnippetsRouter = router({
is_package_verified: item.is_package_verified,
}));
const orderTotal = products.reduce((sum, p) => sum + p.subtotal, 0);
const orderTotal = products.reduce((sum: number, p: any) => sum + p.subtotal, 0);
return {
orderId: `ORD${order.id}`,
@ -473,54 +327,16 @@ export const vendorSnippetsRouter = router({
orderItemId: z.number().int().positive("Valid order item ID required"),
is_packaged: z.boolean()
}))
.mutation(async ({ input, ctx }) => {
.mutation(async ({ input }) => {
const { orderItemId, is_packaged } = input;
// Get staff user ID from auth middleware
// const staffUserId = ctx.staffUser?.id;
// if (!staffUserId) {
// throw new Error("Unauthorized");
// }
// Check if order item exists and get related data
const orderItem = await db.query.orderItems.findFirst({
where: eq(orderItems.id, orderItemId),
with: {
order: {
with: {
slot: true
}
}
}
});
const orderItem = await vendorSnippetDbService.getOrderItemById(orderItemId);
if (!orderItem) {
throw new Error("Order item not found");
}
// Check if this order item belongs to a slot that has vendor snippets
// This ensures only order items from vendor-accessible orders can be updated
if (!orderItem.order.slotId) {
throw new Error("Order item not associated with a vendor slot");
}
const snippetExists = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.slotId, orderItem.order.slotId),
});
if (!snippetExists) {
throw new Error("No vendor snippet found for this order's slot");
}
// Update the is_packaged field
const result = await db.update(orderItems)
.set({ is_packaged })
.where(eq(orderItems.id, orderItemId))
.returning();
if (result.length === 0) {
throw new Error("Failed to update packaging status");
}
await vendorSnippetDbService.updateOrderItemPackaging(orderItemId, is_packaged);
return {
success: true,

View file

@ -0,0 +1,12 @@
import { homeBanners } from '@/src/db/schema'
export type Banner = typeof homeBanners.$inferSelect
export type NewBanner = typeof homeBanners.$inferInsert
export interface IBannerDbService {
getAllBanners(): Promise<Banner[]>
getBannerById(id: number): Promise<Banner | undefined>
createBanner(data: NewBanner): Promise<Banner>
updateBannerById(id: number, data: Partial<NewBanner>): Promise<Banner>
deleteBannerById(id: number): Promise<void>
}

View file

@ -0,0 +1,9 @@
import { complaints, users } from '@/src/db/schema'
export type Complaint = typeof complaints.$inferSelect
export type NewComplaint = typeof complaints.$inferInsert
export interface IComplaintDbService {
getComplaints(cursor?: number, limit?: number): Promise<Array<Complaint & { userName?: string | null; userMobile?: string | null }>>
resolveComplaint(id: number, response?: string): Promise<void>
}

View file

@ -0,0 +1,9 @@
import { keyValStore } from '@/src/db/schema'
export type Constant = typeof keyValStore.$inferSelect
export type NewConstant = typeof keyValStore.$inferInsert
export interface IConstantDbService {
getAllConstants(): Promise<Constant[]>
upsertConstants(constants: { key: string; value: any }[]): Promise<number>
}

View file

@ -0,0 +1,46 @@
import { coupons, couponApplicableUsers, couponApplicableProducts, reservedCoupons, users, orders, orderStatus, staffUsers, productInfo } from '@/src/db/schema'
export type Coupon = typeof coupons.$inferSelect
export type NewCoupon = typeof coupons.$inferInsert
export type ReservedCoupon = typeof reservedCoupons.$inferSelect
export type NewReservedCoupon = typeof reservedCoupons.$inferInsert
export type CouponWithRelations = Omit<Coupon, 'productIds'> & {
productIds: number[] | null
creator?: typeof staffUsers.$inferSelect
applicableUsers: Array<typeof couponApplicableUsers.$inferSelect & { user: typeof users.$inferSelect }>
applicableProducts: Array<typeof couponApplicableProducts.$inferSelect & { product: typeof productInfo.$inferSelect }>
}
export interface ICouponDbService {
// Regular coupons
createCoupon(data: NewCoupon): Promise<Coupon>
getCouponById(id: number): Promise<CouponWithRelations | undefined>
getCouponByCode(code: string): Promise<Coupon | undefined>
getAllCoupons(options: { cursor?: number; limit: number; search?: string }): Promise<CouponWithRelations[]>
updateCoupon(id: number, data: Partial<NewCoupon>): Promise<Coupon>
invalidateCoupon(id: number): Promise<Coupon>
// Coupon applicable users/products
addApplicableUsers(couponId: number, userIds: number[]): Promise<void>
addApplicableProducts(couponId: number, productIds: number[]): Promise<void>
removeAllApplicableUsers(couponId: number): Promise<void>
removeAllApplicableProducts(couponId: number): Promise<void>
countApplicableUsers(couponId: number): Promise<number>
// Reserved coupons
createReservedCoupon(data: NewReservedCoupon): Promise<ReservedCoupon>
getReservedCoupons(options: { cursor?: number; limit: number; search?: string }): Promise<ReservedCoupon[]>
// User operations
getUsersByIds(ids: number[]): Promise<Array<{ id: number; name: string | null; mobile: string | null }>>
getUsersBySearch(search: string, limit: number, offset: number): Promise<Array<{ id: number; name: string | null; mobile: string | null }>>
createUser(data: Partial<typeof users.$inferInsert>): Promise<typeof users.$inferSelect>
getUserByMobile(mobile: string): Promise<typeof users.$inferSelect | undefined>
// Order operations
getOrderByIdWithUserAndStatus(id: number): Promise<typeof orders.$inferSelect & { user?: typeof users.$inferSelect; orderStatus?: any[] } | undefined>
updateOrderStatusRefundCoupon(orderId: number, couponId: number): Promise<void>
// Transaction support
withTransaction<T>(fn: (tx: any) => Promise<T>): Promise<T>
}

View file

@ -0,0 +1,108 @@
import {
orders,
orderItems,
orderStatus,
users,
addresses,
refunds,
coupons,
couponUsage,
complaints,
payments,
deliverySlotInfo,
productInfo,
units,
paymentInfoTable,
} from '@/src/db/schema'
export type Order = typeof orders.$inferSelect
export type OrderItem = typeof orderItems.$inferSelect
export type OrderStatus = typeof orderStatus.$inferSelect
export type User = typeof users.$inferSelect
export type Address = typeof addresses.$inferSelect
export type Refund = typeof refunds.$inferSelect
export type Coupon = typeof coupons.$inferSelect
export type CouponUsage = typeof couponUsage.$inferSelect
export type Complaint = typeof complaints.$inferSelect
export type Payment = typeof payments.$inferSelect
export type Slot = typeof deliverySlotInfo.$inferSelect
export type PaymentInfo = typeof paymentInfoTable.$inferSelect
export type OrderWithRelations = Order & {
user: User
address: Address
slot: Slot | null
orderItems: Array<
OrderItem & {
product: typeof productInfo.$inferSelect & { unit: typeof units.$inferSelect | null }
}
>
payment?: Payment | null
paymentInfo?: PaymentInfo | null
orderStatus?: OrderStatus[]
refunds?: Refund[]
}
export type OrderWithStatus = Order & {
orderStatus: OrderStatus[]
}
export type OrderWithCouponUsages = Order & {
orderItems: Array<OrderItem & { product: typeof productInfo.$inferSelect }>
couponUsages: Array<CouponUsage & { coupon: Coupon }>
}
export interface IOrderDbService {
// Order updates
updateOrderNotes(orderId: number, adminNotes: string | null): Promise<Order>
removeDeliveryCharge(orderId: number, totalAmount: string): Promise<void>
// Order reads
getOrderById(orderId: number): Promise<Order | undefined>
getOrderWithRelations(orderId: number): Promise<OrderWithRelations | undefined>
getOrderWithDetails(orderId: number): Promise<OrderWithRelations | undefined>
getOrderWithStatus(orderId: number): Promise<OrderWithStatus | undefined>
// Order status
getOrderStatusByOrderId(orderId: number): Promise<OrderStatus | undefined>
updateOrderStatusPackaged(orderId: number, isPackaged: boolean, isDelivered: boolean): Promise<void>
updateOrderStatusDelivered(orderId: number, isDelivered: boolean): Promise<void>
cancelOrderStatus(statusId: number, reason: string): Promise<void>
// Refunds
getRefundByOrderId(orderId: number): Promise<Refund | undefined>
createRefund(orderId: number, refundStatus: string): Promise<void>
// Coupon usage
getCouponUsageByOrderId(orderId: number): Promise<Array<CouponUsage & { coupon: Coupon }>>
// Order items
getOrderItemById(orderItemId: number): Promise<OrderItem | undefined>
updateOrderItem(orderItemId: number, data: Partial<OrderItem>): Promise<void>
updateOrderItemsPackaged(orderId: number, isPackaged: boolean): Promise<void>
// Address
updateAddressCoords(addressId: number, latitude: number, longitude: number): Promise<Address>
// Slot queries
getOrdersBySlotId(slotId: number): Promise<OrderWithRelations[]>
getOrdersBySlotIds(slotIds: number[]): Promise<OrderWithCouponUsages[]>
getOrdersByDateRange(start: Date, end: Date, slotId?: number): Promise<OrderWithRelations[]>
// Filtered orders
getAllOrdersWithFilters(options: {
cursor?: number
limit: number
slotId?: number | null
packagedFilter: 'all' | 'packaged' | 'not_packaged'
deliveredFilter: 'all' | 'delivered' | 'not_delivered'
cancellationFilter: 'all' | 'cancelled' | 'not_cancelled'
flashDeliveryFilter: 'all' | 'flash' | 'regular'
}): Promise<OrderWithRelations[]>
// Batch updates
updateOrdersAndItemsInTransaction(data: Array<{ orderId: number; totalAmount: string; items: Array<{ id: number; price: string; discountedPrice: string }> }>): Promise<void>
// Delete
deleteOrderById(orderId: number): Promise<void>
}

View file

@ -0,0 +1,53 @@
import { productInfo, units, specialDeals, productSlots, productTags, productReviews, productGroupInfo, productGroupMembership } from '@/src/db/schema'
export type Product = typeof productInfo.$inferSelect
export type NewProduct = typeof productInfo.$inferInsert
export type ProductGroup = typeof productGroupInfo.$inferSelect
export type NewProductGroup = typeof productGroupInfo.$inferInsert
export interface IProductDbService {
// Product CRUD
getAllProducts(): Promise<Product[]>
getProductById(id: number): Promise<Product | undefined>
createProduct(data: NewProduct): Promise<Product>
updateProduct(id: number, data: Partial<NewProduct>): Promise<Product>
deleteProduct(id: number): Promise<Product>
// Product deals
getDealsByProductId(productId: number): Promise<typeof specialDeals.$inferSelect[]>
createDeals(deals: Partial<typeof specialDeals.$inferInsert>[]): Promise<void>
deleteDealsByProductId(productId: number): Promise<void>
// Product tags
getTagsByProductId(productId: number): Promise<Array<{ tag: { id: number; tagName: string; tagDescription: string | null; imageUrl: string | null; isDashboardTag: boolean; relatedStores: any } }>>
createTagAssociations(associations: { productId: number; tagId: number }[]): Promise<void>
deleteTagAssociationsByProductId(productId: number): Promise<void>
// Product slots
getProductSlotsBySlotId(slotId: number): Promise<typeof productSlots.$inferSelect[]>
getProductSlotsBySlotIds(slotIds: number[]): Promise<typeof productSlots.$inferSelect[]>
createProductSlot(slotId: number, productId: number): Promise<void>
deleteProductSlotsBySlotId(slotId: number): Promise<void>
deleteProductSlot(slotId: number, productId: number): Promise<void>
// Product reviews
getReviewsByProductId(productId: number, limit: number, offset: number): Promise<(typeof productReviews.$inferSelect & { userName: string | null })[]>
getReviewCountByProductId(productId: number): Promise<number>
updateReview(reviewId: number, data: Partial<typeof productReviews.$inferInsert>): Promise<typeof productReviews.$inferSelect>
// Product groups
getAllGroups(): Promise<ProductGroup[]>
getGroupById(id: number): Promise<ProductGroup | undefined>
createGroup(data: NewProductGroup): Promise<ProductGroup>
updateGroup(id: number, data: Partial<NewProductGroup>): Promise<ProductGroup>
deleteGroup(id: number): Promise<ProductGroup>
deleteGroupMembershipsByGroupId(groupId: number): Promise<void>
createGroupMemberships(memberships: { productId: number; groupId: number }[]): Promise<void>
// Unit validation
getUnitById(id: number): Promise<typeof units.$inferSelect | undefined>
// Batch operations
validateProductIdsExist(productIds: number[]): Promise<boolean>
batchUpdateProducts(updates: { productId: number; data: Partial<NewProduct> }[]): Promise<void>
}

View file

@ -0,0 +1,18 @@
import { refunds, orders, orderStatus, payments } from '@/src/db/schema'
export type Refund = typeof refunds.$inferSelect
export type NewRefund = typeof refunds.$inferInsert
export interface IRefundDbService {
// Refund operations
createRefund(data: NewRefund): Promise<Refund>
updateRefund(id: number, data: Partial<NewRefund>): Promise<Refund>
getRefundByOrderId(orderId: number): Promise<Refund | undefined>
// Order operations
getOrderById(id: number): Promise<typeof orders.$inferSelect | undefined>
getOrderStatusByOrderId(orderId: number): Promise<typeof orderStatus.$inferSelect | undefined>
// Payment operations
getSuccessfulPaymentByOrderId(orderId: number): Promise<typeof payments.$inferSelect | undefined>
}

View file

@ -0,0 +1,13 @@
import { productAvailabilitySchedules } from '@/src/db/schema'
export type Schedule = typeof productAvailabilitySchedules.$inferSelect
export type NewSchedule = typeof productAvailabilitySchedules.$inferInsert
export interface IScheduleDbService {
createSchedule(data: NewSchedule): Promise<Schedule>
getAllSchedules(): Promise<Schedule[]>
getScheduleById(id: number): Promise<Schedule | undefined>
getScheduleByName(name: string): Promise<Schedule | undefined>
updateSchedule(id: number, data: Partial<NewSchedule>): Promise<Schedule>
deleteSchedule(id: number): Promise<Schedule>
}

View file

@ -0,0 +1,43 @@
import { deliverySlotInfo, productSlots, vendorSnippets, productInfo, productGroupInfo } from '@/src/db/schema'
export type Slot = typeof deliverySlotInfo.$inferSelect
export type NewSlot = typeof deliverySlotInfo.$inferInsert
export type ProductSlot = typeof productSlots.$inferSelect
export type NewProductSlot = typeof productSlots.$inferInsert
export type SlotWithRelations = Slot & {
productSlots?: Array<{ product: { id: number; name: string; images: any } }>
vendorSnippets?: Array<{ id: number; snippetCode: string; slotId: number | null; productIds: number[]; validTill: Date | null; createdAt: Date; isPermanent: boolean | null }>
}
export interface ISlotDbService {
// Slot CRUD
getAllSlots(): Promise<SlotWithRelations[]>
getActiveSlots(): Promise<Slot[]>
getSlotById(id: number): Promise<SlotWithRelations | undefined>
createSlot(data: NewSlot): Promise<Slot>
updateSlot(id: number, data: Partial<NewSlot>): Promise<Slot>
deactivateSlot(id: number): Promise<Slot>
// Product associations
getProductSlotsBySlotId(slotId: number): Promise<ProductSlot[]>
getProductSlotsBySlotIds(slotIds: number[]): Promise<ProductSlot[]>
createProductSlot(slotId: number, productId: number): Promise<void>
deleteProductSlot(slotId: number, productId: number): Promise<void>
deleteProductSlotsBySlotId(slotId: number): Promise<void>
// Vendor snippets
getVendorSnippetsBySlotId(slotId: number): Promise<Array<{ id: number; snippetCode: string; slotId: number | null; productIds: number[]; validTill: Date | null; createdAt: Date; isPermanent: boolean | null }>>
createVendorSnippet(data: { snippetCode: string; slotId: number; productIds: number[]; validTill?: Date }): Promise<{ id: number; snippetCode: string; slotId: number | null; productIds: number[]; validTill: Date | null; createdAt: Date; isPermanent: boolean | null }>
checkSnippetCodeExists(code: string): Promise<boolean>
// Product validation
validateProductsExist(productIds: number[]): Promise<boolean>
getProductsByIds(productIds: number[]): Promise<typeof productInfo.$inferSelect[]>
// Group validation
getGroupsByIds(groupIds: number[]): Promise<Array<{ id: number; groupName: string; description: string | null; createdAt: Date }>>
// Transaction support
withTransaction<T>(fn: (tx: any) => Promise<T>): Promise<T>
}

View file

@ -0,0 +1,40 @@
import { staffUsers, staffRoles, users, userDetails, orders } from '@/src/db/schema'
export type StaffUser = typeof staffUsers.$inferSelect
export type NewStaffUser = typeof staffUsers.$inferInsert
export type StaffRole = typeof staffRoles.$inferSelect
// Flexible types for queries with relations
export type StaffUserWithRole = {
id: number
name: string
role?: {
id: number
roleName: string
rolePermissions: Array<{
permission: {
id: number
permissionName: string
}
}>
} | null
}
export interface IStaffUserDbService {
// Staff operations
getStaffUserByName(name: string): Promise<StaffUser | undefined>
getAllStaff(): Promise<StaffUserWithRole[]>
createStaffUser(data: NewStaffUser): Promise<StaffUser>
// Role operations
getRoleById(id: number): Promise<StaffRole | undefined>
getAllRoles(): Promise<Array<{ id: number; roleName: string }>>
// User operations
getUsers(options: { cursor?: number; limit: number; search?: string }): Promise<Array<typeof users.$inferSelect & { userDetails?: typeof userDetails.$inferSelect | null }>>
getUserById(id: number): Promise<(typeof users.$inferSelect & { userDetails?: typeof userDetails.$inferSelect | null; orders?: typeof orders.$inferSelect[] }) | undefined>
upsertUserDetails(data: Partial<typeof userDetails.$inferInsert> & { userId: number }): Promise<void>
// Order operations
getLastOrderByUserId(userId: number): Promise<typeof orders.$inferSelect | undefined>
}

View file

@ -0,0 +1,14 @@
import { storeInfo, productInfo } from '@/src/db/schema'
export type Store = typeof storeInfo.$inferSelect
export type NewStore = typeof storeInfo.$inferInsert
export interface IStoreDbService {
getAllStores(): Promise<Store[]>
getStoreById(id: number): Promise<Store | undefined>
createStore(data: NewStore): Promise<Store>
updateStore(id: number, data: Partial<NewStore>): Promise<Store>
deleteStore(id: number): Promise<void>
assignProductsToStore(storeId: number, productIds: number[]): Promise<void>
removeProductsFromStore(storeId: number): Promise<void>
}

View file

@ -0,0 +1,13 @@
import { productTagInfo } from '@/src/db/schema'
export type Tag = typeof productTagInfo.$inferSelect
export type NewTag = typeof productTagInfo.$inferInsert
export interface ITagDbService {
getAllTags(): Promise<Tag[]>
getTagById(id: number): Promise<Tag | undefined>
getTagByName(name: string): Promise<Tag | undefined>
createTag(data: NewTag): Promise<Tag>
updateTag(id: number, data: Partial<NewTag>): Promise<Tag>
deleteTag(id: number): Promise<void>
}

View file

@ -0,0 +1,37 @@
import { users, userDetails, orders, orderItems, orderStatus, complaints, notifCreds, unloggedUserTokens, userIncidents } from '@/src/db/schema'
export type User = typeof users.$inferSelect
export type NewUser = typeof users.$inferInsert
export type UserDetail = typeof userDetails.$inferSelect
export interface IUserDbService {
// User operations
getUserById(id: number): Promise<User | undefined>
getUserByMobile(mobile: string): Promise<User | undefined>
getUsers(options: { limit: number; cursor?: number; search?: string }): Promise<User[]>
createUser(data: NewUser): Promise<User>
// User details
getUserDetailsByUserId(userId: number): Promise<UserDetail | undefined>
upsertUserDetails(data: Partial<UserDetail> & { userId: number }): Promise<void>
// Order operations
getOrdersByUserId(userId: number): Promise<typeof orders.$inferSelect[]>
getLastOrderByUserId(userId: number): Promise<typeof orders.$inferSelect | undefined>
getOrderCountByUserIds(userIds: number[]): Promise<{ userId: number; totalOrders: number }[]>
getLastOrderDateByUserIds(userIds: number[]): Promise<{ userId: number; lastOrderDate: Date | null }[]>
getOrderStatusByOrderIds(orderIds: number[]): Promise<{ orderId: number; isDelivered: boolean; isCancelled: boolean }[]>
getOrderItemCountByOrderIds(orderIds: number[]): Promise<{ orderId: number; itemCount: number }[]>
// Complaint operations
getUnresolvedComplaintCount(): Promise<number>
// Notification operations
getAllNotifTokens(): Promise<string[]>
getNotifTokensByUserIds(userIds: number[]): Promise<string[]>
getUnloggedTokens(): Promise<string[]>
// User incidents
getUserIncidentsByUserId(userId: number): Promise<Array<typeof userIncidents.$inferSelect & { order?: { orderStatus: Array<{ isCancelled: boolean }> } | null; addedBy?: { name: string | null } | null }>>
createUserIncident(data: { userId: number; orderId?: number | null; adminComment?: string | null; addedBy: number; negativityScore?: number | null }): Promise<typeof userIncidents.$inferSelect>
}

View file

@ -0,0 +1,34 @@
import { vendorSnippets, deliverySlotInfo, productInfo, orders, orderItems } from '@/src/db/schema'
export type VendorSnippet = typeof vendorSnippets.$inferSelect
export type NewVendorSnippet = typeof vendorSnippets.$inferInsert
export interface IVendorSnippetDbService {
// Snippet CRUD
createSnippet(data: NewVendorSnippet): Promise<VendorSnippet>
getAllSnippets(): Promise<VendorSnippet[]>
getSnippetById(id: number): Promise<VendorSnippet | undefined>
getSnippetByCode(code: string): Promise<VendorSnippet | undefined>
updateSnippet(id: number, data: Partial<NewVendorSnippet>): Promise<VendorSnippet>
deleteSnippet(id: number): Promise<VendorSnippet>
// Validation
checkSnippetCodeExists(code: string): Promise<boolean>
// Slot operations
getSlotById(id: number): Promise<typeof deliverySlotInfo.$inferSelect | undefined>
getUpcomingSlots(since: Date): Promise<typeof deliverySlotInfo.$inferSelect[]>
// Product operations
getProductsByIds(ids: number[]): Promise<Array<{ id: number; name: string }>>
validateProductsExist(ids: number[]): Promise<boolean>
// Order operations
getOrdersBySlotId(slotId: number): Promise<typeof orders.$inferSelect[]>
getOrderItemsByOrderIds(orderIds: number[]): Promise<typeof orderItems.$inferSelect[]>
getOrderItemById(id: number): Promise<typeof orderItems.$inferSelect | undefined>
updateOrderItemPackaging(id: number, is_packaged: boolean): Promise<void>
// Relations check
hasSnippetForSlot(slotId: number): Promise<boolean>
}

View file

@ -0,0 +1,41 @@
export type { IBannerDbService, Banner, NewBanner } from '@/src/trpc/apis/admin-apis/dataAccessors/interfaces/banner-db-service.interface'
export { bannerDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/postgres/banner-queries'
export type { IComplaintDbService, Complaint, NewComplaint } from '@/src/trpc/apis/admin-apis/dataAccessors/interfaces/complaint-db-service.interface'
export { complaintDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/postgres/complaint-queries'
export type { IConstantDbService, Constant, NewConstant } from '@/src/trpc/apis/admin-apis/dataAccessors/interfaces/constant-db-service.interface'
export { constantDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/postgres/constant-queries'
export type { ICouponDbService, Coupon, NewCoupon, ReservedCoupon, NewReservedCoupon, CouponWithRelations } from '@/src/trpc/apis/admin-apis/dataAccessors/interfaces/coupon-db-service.interface'
export { couponDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/postgres/coupon-queries'
export type { IOrderDbService, Order, OrderItem, OrderStatus, OrderWithRelations, OrderWithStatus, OrderWithCouponUsages } from '@/src/trpc/apis/admin-apis/dataAccessors/interfaces/order-db-service.interface'
export { orderDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/postgres/order-queries'
export type { IProductDbService, Product, NewProduct, ProductGroup, NewProductGroup } from '@/src/trpc/apis/admin-apis/dataAccessors/interfaces/product-db-service.interface'
export { productDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/postgres/product-queries'
export type { IRefundDbService, Refund, NewRefund } from '@/src/trpc/apis/admin-apis/dataAccessors/interfaces/refund-db-service.interface'
export { refundDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/postgres/refund-queries'
export type { IScheduleDbService, Schedule, NewSchedule } from '@/src/trpc/apis/admin-apis/dataAccessors/interfaces/schedule-db-service.interface'
export { scheduleDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/postgres/schedule-queries'
export type { ISlotDbService, Slot, NewSlot, ProductSlot, NewProductSlot, SlotWithRelations } from '@/src/trpc/apis/admin-apis/dataAccessors/interfaces/slot-db-service.interface'
export { slotDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/postgres/slot-queries'
export type { IStaffUserDbService, StaffUser, NewStaffUser, StaffRole, StaffUserWithRole } from '@/src/trpc/apis/admin-apis/dataAccessors/interfaces/staff-user-db-service.interface'
export { staffUserDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/postgres/staff-user-queries'
export type { IStoreDbService, Store, NewStore } from '@/src/trpc/apis/admin-apis/dataAccessors/interfaces/store-db-service.interface'
export { storeDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/postgres/store-queries'
export type { ITagDbService, Tag, NewTag } from '@/src/trpc/apis/admin-apis/dataAccessors/interfaces/tag-db-service.interface'
export { tagDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/postgres/tag-queries'
export type { IUserDbService, User, NewUser, UserDetail } from '@/src/trpc/apis/admin-apis/dataAccessors/interfaces/user-db-service.interface'
export { userDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/postgres/user-queries'
export type { IVendorSnippetDbService, VendorSnippet, NewVendorSnippet } from '@/src/trpc/apis/admin-apis/dataAccessors/interfaces/vendor-snippet-db-service.interface'
export { vendorSnippetDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/postgres/vendor-snippets-queries'

View file

@ -0,0 +1,38 @@
import { db } from '@/src/db/db_index'
import { homeBanners } from '@/src/db/schema'
import { eq, desc } from 'drizzle-orm'
import { IBannerDbService, Banner, NewBanner } from '@/src/trpc/apis/admin-apis/dataAccessors/interfaces/banner-db-service.interface'
export class BannerDbService implements IBannerDbService {
async getAllBanners(): Promise<Banner[]> {
return db.query.homeBanners.findMany({
orderBy: desc(homeBanners.createdAt),
})
}
async getBannerById(id: number): Promise<Banner | undefined> {
return db.query.homeBanners.findFirst({
where: eq(homeBanners.id, id),
})
}
async createBanner(data: NewBanner): Promise<Banner> {
const [banner] = await db.insert(homeBanners).values(data).returning()
return banner
}
async updateBannerById(id: number, data: Partial<NewBanner>): Promise<Banner> {
const [banner] = await db
.update(homeBanners)
.set({ ...data, lastUpdated: new Date() })
.where(eq(homeBanners.id, id))
.returning()
return banner
}
async deleteBannerById(id: number): Promise<void> {
await db.delete(homeBanners).where(eq(homeBanners.id, id))
}
}
export const bannerDbService: IBannerDbService = new BannerDbService()

View file

@ -0,0 +1,43 @@
import { db } from '@/src/db/db_index'
import { complaints, users } from '@/src/db/schema'
import { eq, desc, lt } from 'drizzle-orm'
import { IComplaintDbService, Complaint, NewComplaint } from '@/src/trpc/apis/admin-apis/dataAccessors/interfaces/complaint-db-service.interface'
export class ComplaintDbService implements IComplaintDbService {
async getComplaints(
cursor?: number,
limit: number = 20
): Promise<Array<Complaint & { userName?: string | null; userMobile?: string | null }>> {
let whereCondition = cursor ? lt(complaints.id, cursor) : undefined
const complaintsData = await db
.select({
id: complaints.id,
complaintBody: complaints.complaintBody,
userId: complaints.userId,
orderId: complaints.orderId,
isResolved: complaints.isResolved,
createdAt: complaints.createdAt,
response: complaints.response,
images: complaints.images,
userName: users.name,
userMobile: users.mobile,
})
.from(complaints)
.leftJoin(users, eq(complaints.userId, users.id))
.where(whereCondition)
.orderBy(desc(complaints.id))
.limit(limit + 1)
return complaintsData
}
async resolveComplaint(id: number, response?: string): Promise<void> {
await db
.update(complaints)
.set({ isResolved: true, response })
.where(eq(complaints.id, id))
}
}
export const complaintDbService: IComplaintDbService = new ComplaintDbService()

View file

@ -0,0 +1,25 @@
import { db } from '@/src/db/db_index'
import { keyValStore } from '@/src/db/schema'
import { IConstantDbService, Constant, NewConstant } from '@/src/trpc/apis/admin-apis/dataAccessors/interfaces/constant-db-service.interface'
export class ConstantDbService implements IConstantDbService {
async getAllConstants(): Promise<Constant[]> {
return db.select().from(keyValStore)
}
async upsertConstants(constants: { key: string; value: any }[]): Promise<number> {
await db.transaction(async (tx) => {
for (const { key, value } of constants) {
await tx.insert(keyValStore)
.values({ key, value })
.onConflictDoUpdate({
target: keyValStore.key,
set: { value },
})
}
})
return constants.length
}
}
export const constantDbService: IConstantDbService = new ConstantDbService()

View file

@ -0,0 +1,191 @@
import { db } from '@/src/db/db_index'
import { coupons, couponApplicableUsers, couponApplicableProducts, reservedCoupons, users, orders, orderStatus } from '@/src/db/schema'
import { eq, and, like, or, inArray, lt, asc } from 'drizzle-orm'
import { ICouponDbService, Coupon, NewCoupon, ReservedCoupon, NewReservedCoupon, CouponWithRelations } from '@/src/trpc/apis/admin-apis/dataAccessors/interfaces/coupon-db-service.interface'
export class CouponDbService implements ICouponDbService {
async createCoupon(data: NewCoupon): Promise<Coupon> {
const [coupon] = await db.insert(coupons).values(data).returning()
return coupon
}
async getCouponById(id: number): Promise<CouponWithRelations | undefined> {
const result = await db.query.coupons.findFirst({
where: eq(coupons.id, id),
with: {
creator: true,
applicableUsers: { with: { user: true } },
applicableProducts: { with: { product: true } },
},
})
if (!result) return undefined
return {
...result,
productIds: (result.productIds as number[] | null) || null,
} as CouponWithRelations
}
async getCouponByCode(code: string): Promise<Coupon | undefined> {
return db.query.coupons.findFirst({
where: eq(coupons.couponCode, code),
})
}
async getAllCoupons(options: { cursor?: number; limit: number; search?: string }): Promise<CouponWithRelations[]> {
const { cursor, limit, search } = options
let whereCondition = undefined
const conditions = []
if (cursor) {
conditions.push(lt(coupons.id, cursor))
}
if (search && search.trim()) {
conditions.push(like(coupons.couponCode, `%${search}%`))
}
if (conditions.length > 0) {
whereCondition = and(...conditions)
}
const result = await db.query.coupons.findMany({
where: whereCondition,
with: {
creator: true,
applicableUsers: { with: { user: true } },
applicableProducts: { with: { product: true } },
},
orderBy: (coupons, { desc }) => [desc(coupons.createdAt)],
limit: limit + 1,
})
return result.map((coupon) => ({
...coupon,
productIds: (coupon.productIds as number[] | null) || null,
})) as CouponWithRelations[]
}
async updateCoupon(id: number, data: Partial<NewCoupon>): Promise<Coupon> {
const [coupon] = await db.update(coupons).set(data).where(eq(coupons.id, id)).returning()
return coupon
}
async invalidateCoupon(id: number): Promise<Coupon> {
const [coupon] = await db.update(coupons).set({ isInvalidated: true }).where(eq(coupons.id, id)).returning()
return coupon
}
async addApplicableUsers(couponId: number, userIds: number[]): Promise<void> {
await db.insert(couponApplicableUsers).values(
userIds.map(userId => ({ couponId, userId }))
)
}
async addApplicableProducts(couponId: number, productIds: number[]): Promise<void> {
await db.insert(couponApplicableProducts).values(
productIds.map(productId => ({ couponId, productId }))
)
}
async removeAllApplicableUsers(couponId: number): Promise<void> {
await db.delete(couponApplicableUsers).where(eq(couponApplicableUsers.couponId, couponId))
}
async removeAllApplicableProducts(couponId: number): Promise<void> {
await db.delete(couponApplicableProducts).where(eq(couponApplicableProducts.couponId, couponId))
}
async countApplicableUsers(couponId: number): Promise<number> {
return db.$count(couponApplicableUsers, eq(couponApplicableUsers.couponId, couponId))
}
async createReservedCoupon(data: NewReservedCoupon): Promise<ReservedCoupon> {
const [coupon] = await db.insert(reservedCoupons).values(data).returning()
return coupon
}
async getReservedCoupons(options: { cursor?: number; limit: number; search?: string }): Promise<ReservedCoupon[]> {
const { cursor, limit, search } = options
let whereCondition = undefined
const conditions = []
if (cursor) {
conditions.push(lt(reservedCoupons.id, cursor))
}
if (search && search.trim()) {
conditions.push(or(
like(reservedCoupons.secretCode, `%${search}%`),
like(reservedCoupons.couponCode, `%${search}%`)
))
}
if (conditions.length > 0) {
whereCondition = and(...conditions)
}
return db.query.reservedCoupons.findMany({
where: whereCondition,
with: { redeemedUser: true, creator: true },
orderBy: (reservedCoupons, { desc }) => [desc(reservedCoupons.createdAt)],
limit: limit + 1,
})
}
async getUsersByIds(ids: number[]): Promise<Array<{ id: number; name: string | null; mobile: string | null }>> {
return db.query.users.findMany({
where: inArray(users.id, ids),
columns: { id: true, name: true, mobile: true },
})
}
async getUsersBySearch(search: string, limit: number, offset: number): Promise<Array<{ id: number; name: string | null; mobile: string | null }>> {
const whereCondition = or(
like(users.name, `%${search}%`),
like(users.mobile, `%${search}%`)
)
return db.query.users.findMany({
where: whereCondition,
columns: { id: true, name: true, mobile: true },
limit,
offset,
orderBy: (users, { asc }) => [asc(users.name)],
})
}
async createUser(data: Partial<typeof users.$inferInsert>): Promise<typeof users.$inferSelect> {
const [user] = await db.insert(users).values(data).returning()
return user
}
async getUserByMobile(mobile: string): Promise<typeof users.$inferSelect | undefined> {
return db.query.users.findFirst({
where: eq(users.mobile, mobile),
})
}
async getOrderByIdWithUserAndStatus(id: number): Promise<typeof orders.$inferSelect & { user?: typeof users.$inferSelect; orderStatus?: any[] } | undefined> {
return db.query.orders.findFirst({
where: eq(orders.id, id),
with: {
user: true,
orderStatus: true,
},
})
}
async updateOrderStatusRefundCoupon(orderId: number, couponId: number): Promise<void> {
await db.update(orderStatus)
.set({ refundCouponId: couponId })
.where(eq(orderStatus.orderId, orderId))
}
async withTransaction<T>(fn: (tx: any) => Promise<T>): Promise<T> {
return db.transaction(fn)
}
}
export const couponDbService: ICouponDbService = new CouponDbService()

View file

@ -0,0 +1,334 @@
import { db } from '@/src/db/db_index'
import {
orders,
orderItems,
orderStatus,
users,
addresses,
refunds,
coupons,
couponUsage,
complaints,
payments,
deliverySlotInfo,
productInfo,
units,
paymentInfoTable,
} from '@/src/db/schema'
import { eq, and, gte, lt, desc, inArray, SQL } from 'drizzle-orm'
import {
IOrderDbService,
Order,
OrderItem,
OrderStatus,
Address,
Refund,
OrderWithRelations,
OrderWithStatus,
OrderWithCouponUsages,
} from '@/src/trpc/apis/admin-apis/dataAccessors/interfaces/order-db-service.interface'
export class OrderDbService implements IOrderDbService {
async updateOrderNotes(orderId: number, adminNotes: string | null): Promise<Order> {
const [updated] = await db
.update(orders)
.set({ adminNotes })
.where(eq(orders.id, orderId))
.returning()
return updated
}
async removeDeliveryCharge(orderId: number, totalAmount: string): Promise<void> {
await db
.update(orders)
.set({ deliveryCharge: '0', totalAmount })
.where(eq(orders.id, orderId))
}
async getOrderById(orderId: number): Promise<Order | undefined> {
return db.query.orders.findFirst({
where: eq(orders.id, orderId),
})
}
async getOrderWithRelations(orderId: number): Promise<OrderWithRelations | undefined> {
return db.query.orders.findFirst({
where: eq(orders.id, orderId),
with: {
user: true,
address: true,
slot: true,
orderItems: {
with: {
product: {
with: { unit: true },
},
},
},
payment: true,
paymentInfo: true,
},
}) as Promise<OrderWithRelations | undefined>
}
async getOrderWithDetails(orderId: number): Promise<OrderWithRelations | undefined> {
return db.query.orders.findFirst({
where: eq(orders.id, orderId),
with: {
user: true,
address: true,
slot: true,
orderItems: {
with: {
product: {
with: { unit: true },
},
},
},
payment: true,
paymentInfo: true,
orderStatus: true,
refunds: true,
},
}) as Promise<OrderWithRelations | undefined>
}
async getOrderWithStatus(orderId: number): Promise<OrderWithStatus | undefined> {
return db.query.orders.findFirst({
where: eq(orders.id, orderId),
with: {
orderStatus: true,
},
}) as Promise<OrderWithStatus | undefined>
}
async getOrderStatusByOrderId(orderId: number): Promise<OrderStatus | undefined> {
return db.query.orderStatus.findFirst({
where: eq(orderStatus.orderId, orderId),
})
}
async updateOrderStatusPackaged(orderId: number, isPackaged: boolean, isDelivered: boolean): Promise<void> {
await db
.update(orderStatus)
.set({ isPackaged, isDelivered })
.where(eq(orderStatus.orderId, orderId))
}
async updateOrderStatusDelivered(orderId: number, isDelivered: boolean): Promise<void> {
await db
.update(orderStatus)
.set({ isDelivered })
.where(eq(orderStatus.orderId, orderId))
}
async cancelOrderStatus(statusId: number, reason: string): Promise<void> {
await db
.update(orderStatus)
.set({
isCancelled: true,
isCancelledByAdmin: true,
cancelReason: reason,
cancellationAdminNotes: reason,
cancellationReviewed: true,
cancellationReviewedAt: new Date(),
})
.where(eq(orderStatus.id, statusId))
}
async getRefundByOrderId(orderId: number): Promise<Refund | undefined> {
return db.query.refunds.findFirst({
where: eq(refunds.orderId, orderId),
})
}
async createRefund(orderId: number, refundStatus: string): Promise<void> {
await db.insert(refunds).values({ orderId, refundStatus })
}
async getCouponUsageByOrderId(orderId: number): Promise<Array<typeof couponUsage.$inferSelect & { coupon: typeof coupons.$inferSelect }>> {
return db.query.couponUsage.findMany({
where: eq(couponUsage.orderId, orderId),
with: { coupon: true },
})
}
async getOrderItemById(orderItemId: number): Promise<OrderItem | undefined> {
return db.query.orderItems.findFirst({
where: eq(orderItems.id, orderItemId),
})
}
async updateOrderItem(orderItemId: number, data: Partial<OrderItem>): Promise<void> {
await db
.update(orderItems)
.set(data)
.where(eq(orderItems.id, orderItemId))
}
async updateOrderItemsPackaged(orderId: number, isPackaged: boolean): Promise<void> {
await db
.update(orderItems)
.set({ is_packaged: isPackaged })
.where(eq(orderItems.orderId, orderId))
}
async updateAddressCoords(addressId: number, latitude: number, longitude: number): Promise<Address> {
const [updated] = await db
.update(addresses)
.set({ adminLatitude: latitude, adminLongitude: longitude })
.where(eq(addresses.id, addressId))
.returning()
return updated
}
async getOrdersBySlotId(slotId: number): Promise<OrderWithRelations[]> {
return db.query.orders.findMany({
where: eq(orders.slotId, slotId),
with: {
user: true,
address: true,
slot: true,
orderItems: {
with: {
product: { with: { unit: true } },
},
},
orderStatus: true,
},
}) as Promise<OrderWithRelations[]>
}
async getOrdersBySlotIds(slotIds: number[]): Promise<OrderWithCouponUsages[]> {
return db.query.orders.findMany({
where: inArray(orders.slotId, slotIds),
with: {
orderItems: {
with: {
product: true,
},
},
couponUsages: {
with: { coupon: true },
},
},
}) as Promise<OrderWithCouponUsages[]>
}
async getOrdersByDateRange(start: Date, end: Date, slotId?: number): Promise<OrderWithRelations[]> {
let whereCondition = and(gte(orders.createdAt, start), lt(orders.createdAt, end))
if (slotId) {
whereCondition = and(whereCondition, eq(orders.slotId, slotId))
}
return db.query.orders.findMany({
where: whereCondition,
with: {
user: true,
address: true,
slot: true,
orderItems: {
with: {
product: { with: { unit: true } },
},
},
orderStatus: true,
},
}) as Promise<OrderWithRelations[]>
}
async getAllOrdersWithFilters(options: {
cursor?: number
limit: number
slotId?: number | null
packagedFilter: 'all' | 'packaged' | 'not_packaged'
deliveredFilter: 'all' | 'delivered' | 'not_delivered'
cancellationFilter: 'all' | 'cancelled' | 'not_cancelled'
flashDeliveryFilter: 'all' | 'flash' | 'regular'
}): Promise<OrderWithRelations[]> {
const {
cursor,
limit,
slotId,
packagedFilter,
deliveredFilter,
cancellationFilter,
flashDeliveryFilter,
} = options
let whereCondition: SQL<unknown> | undefined = eq(orders.id, orders.id)
if (cursor) {
whereCondition = and(whereCondition, lt(orders.id, cursor))
}
if (slotId) {
whereCondition = and(whereCondition, eq(orders.slotId, slotId))
}
if (packagedFilter === 'packaged') {
whereCondition = and(whereCondition, eq(orderStatus.isPackaged, true))
} else if (packagedFilter === 'not_packaged') {
whereCondition = and(whereCondition, eq(orderStatus.isPackaged, false))
}
if (deliveredFilter === 'delivered') {
whereCondition = and(whereCondition, eq(orderStatus.isDelivered, true))
} else if (deliveredFilter === 'not_delivered') {
whereCondition = and(whereCondition, eq(orderStatus.isDelivered, false))
}
if (cancellationFilter === 'cancelled') {
whereCondition = and(whereCondition, eq(orderStatus.isCancelled, true))
} else if (cancellationFilter === 'not_cancelled') {
whereCondition = and(whereCondition, eq(orderStatus.isCancelled, false))
}
if (flashDeliveryFilter === 'flash') {
whereCondition = and(whereCondition, eq(orders.isFlashDelivery, true))
} else if (flashDeliveryFilter === 'regular') {
whereCondition = and(whereCondition, eq(orders.isFlashDelivery, false))
}
return db.query.orders.findMany({
where: whereCondition,
orderBy: desc(orders.createdAt),
limit: limit + 1,
with: {
user: true,
address: true,
slot: true,
orderItems: {
with: {
product: { with: { unit: true } },
},
},
orderStatus: true,
},
}) as Promise<OrderWithRelations[]>
}
async updateOrdersAndItemsInTransaction(
data: Array<{ orderId: number; totalAmount: string; items: Array<{ id: number; price: string; discountedPrice: string }> }>
): Promise<void> {
await db.transaction(async (tx) => {
for (const entry of data) {
await tx.update(orders).set({ totalAmount: entry.totalAmount }).where(eq(orders.id, entry.orderId))
for (const item of entry.items) {
await tx.update(orderItems).set({ price: item.price, discountedPrice: item.discountedPrice }).where(eq(orderItems.id, item.id))
}
}
})
}
async deleteOrderById(orderId: number): Promise<void> {
await db.transaction(async (tx) => {
await tx.delete(orderItems).where(eq(orderItems.orderId, orderId))
await tx.delete(orderStatus).where(eq(orderStatus.orderId, orderId))
await tx.delete(payments).where(eq(payments.orderId, orderId))
await tx.delete(refunds).where(eq(refunds.orderId, orderId))
await tx.delete(couponUsage).where(eq(couponUsage.orderId, orderId))
await tx.delete(complaints).where(eq(complaints.orderId, orderId))
await tx.delete(orders).where(eq(orders.id, orderId))
})
}
}
export const orderDbService: IOrderDbService = new OrderDbService()

View file

@ -0,0 +1,226 @@
import { db } from '@/src/db/db_index'
import { productInfo, units, specialDeals, productSlots, productTags, productReviews, productGroupInfo, productGroupMembership, users } from '@/src/db/schema'
import { eq, and, inArray, desc, sql } from 'drizzle-orm'
import { IProductDbService, Product, NewProduct, ProductGroup, NewProductGroup } from '@/src/trpc/apis/admin-apis/dataAccessors/interfaces/product-db-service.interface'
export class ProductDbService implements IProductDbService {
async getAllProducts(): Promise<Product[]> {
return db.query.productInfo.findMany({
orderBy: productInfo.name,
with: {
unit: true,
store: true,
},
})
}
async getProductById(id: number): Promise<Product | undefined> {
return db.query.productInfo.findFirst({
where: eq(productInfo.id, id),
with: {
unit: true,
},
})
}
async createProduct(data: NewProduct): Promise<Product> {
const [product] = await db.insert(productInfo).values(data).returning()
return product
}
async updateProduct(id: number, data: Partial<NewProduct>): Promise<Product> {
const [product] = await db
.update(productInfo)
.set(data)
.where(eq(productInfo.id, id))
.returning()
return product
}
async deleteProduct(id: number): Promise<Product> {
const [product] = await db
.delete(productInfo)
.where(eq(productInfo.id, id))
.returning()
return product
}
async getDealsByProductId(productId: number): Promise<typeof specialDeals.$inferSelect[]> {
return db.query.specialDeals.findMany({
where: eq(specialDeals.productId, productId),
orderBy: specialDeals.quantity,
})
}
async createDeals(deals: Partial<typeof specialDeals.$inferInsert>[]): Promise<void> {
if (deals.length > 0) {
await db.insert(specialDeals).values(deals as any)
}
}
async deleteDealsByProductId(productId: number): Promise<void> {
await db.delete(specialDeals).where(eq(specialDeals.productId, productId))
}
async getTagsByProductId(productId: number): Promise<Array<{ tag: { id: number; tagName: string; tagDescription: string | null; imageUrl: string | null; isDashboardTag: boolean; relatedStores: any } }>> {
return db.query.productTags.findMany({
where: eq(productTags.productId, productId),
with: {
tag: true,
},
}) as any
}
async createTagAssociations(associations: { productId: number; tagId: number }[]): Promise<void> {
if (associations.length > 0) {
await db.insert(productTags).values(associations)
}
}
async deleteTagAssociationsByProductId(productId: number): Promise<void> {
await db.delete(productTags).where(eq(productTags.productId, productId))
}
async getProductSlotsBySlotId(slotId: number): Promise<typeof productSlots.$inferSelect[]> {
return db.query.productSlots.findMany({
where: eq(productSlots.slotId, slotId),
})
}
async getProductSlotsBySlotIds(slotIds: number[]): Promise<typeof productSlots.$inferSelect[]> {
return db.query.productSlots.findMany({
where: inArray(productSlots.slotId, slotIds),
columns: { slotId: true, productId: true },
})
}
async createProductSlot(slotId: number, productId: number): Promise<void> {
await db.insert(productSlots).values({ slotId, productId })
}
async deleteProductSlotsBySlotId(slotId: number): Promise<void> {
await db.delete(productSlots).where(eq(productSlots.slotId, slotId))
}
async deleteProductSlot(slotId: number, productId: number): Promise<void> {
await db
.delete(productSlots)
.where(and(eq(productSlots.slotId, slotId), eq(productSlots.productId, productId)))
}
async getReviewsByProductId(productId: number, limit: number, offset: number): Promise<(typeof productReviews.$inferSelect & { userName: string | null })[]> {
const reviews = await db
.select({
id: productReviews.id,
reviewBody: productReviews.reviewBody,
ratings: productReviews.ratings,
imageUrls: productReviews.imageUrls,
reviewTime: productReviews.reviewTime,
adminResponse: productReviews.adminResponse,
adminResponseImages: productReviews.adminResponseImages,
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)
return reviews as any
}
async getReviewCountByProductId(productId: number): Promise<number> {
const result = await db
.select({ count: sql`count(*)` })
.from(productReviews)
.where(eq(productReviews.productId, productId))
return Number(result[0].count)
}
async updateReview(reviewId: number, data: Partial<typeof productReviews.$inferInsert>): Promise<typeof productReviews.$inferSelect> {
const [review] = await db
.update(productReviews)
.set(data)
.where(eq(productReviews.id, reviewId))
.returning()
return review
}
async getAllGroups(): Promise<ProductGroup[]> {
return db.query.productGroupInfo.findMany({
with: {
memberships: {
with: {
product: true,
},
},
},
orderBy: desc(productGroupInfo.createdAt),
})
}
async getGroupById(id: number): Promise<ProductGroup | undefined> {
return db.query.productGroupInfo.findFirst({
where: eq(productGroupInfo.id, id),
})
}
async createGroup(data: NewProductGroup): Promise<ProductGroup> {
const [group] = await db.insert(productGroupInfo).values(data).returning()
return group
}
async updateGroup(id: number, data: Partial<NewProductGroup>): Promise<ProductGroup> {
const [group] = await db
.update(productGroupInfo)
.set(data)
.where(eq(productGroupInfo.id, id))
.returning()
return group
}
async deleteGroup(id: number): Promise<ProductGroup> {
const [group] = await db
.delete(productGroupInfo)
.where(eq(productGroupInfo.id, id))
.returning()
return group
}
async deleteGroupMembershipsByGroupId(groupId: number): Promise<void> {
await db.delete(productGroupMembership).where(eq(productGroupMembership.groupId, groupId))
}
async createGroupMemberships(memberships: { productId: number; groupId: number }[]): Promise<void> {
if (memberships.length > 0) {
await db.insert(productGroupMembership).values(memberships)
}
}
async getUnitById(id: number): Promise<typeof units.$inferSelect | undefined> {
return db.query.units.findFirst({
where: eq(units.id, id),
})
}
async validateProductIdsExist(productIds: number[]): Promise<boolean> {
const products = await db.query.productInfo.findMany({
where: inArray(productInfo.id, productIds),
columns: { id: true },
})
return products.length === productIds.length
}
async batchUpdateProducts(updates: { productId: number; data: Partial<NewProduct> }[]): Promise<void> {
const promises = updates.map(update =>
db
.update(productInfo)
.set(update.data)
.where(eq(productInfo.id, update.productId))
)
await Promise.all(promises)
}
}
export const productDbService: IProductDbService = new ProductDbService()

View file

@ -0,0 +1,49 @@
import { db } from '@/src/db/db_index'
import { refunds, orders, orderStatus, payments } from '@/src/db/schema'
import { eq, and } from 'drizzle-orm'
import { IRefundDbService, Refund, NewRefund } from '@/src/trpc/apis/admin-apis/dataAccessors/interfaces/refund-db-service.interface'
export class RefundDbService implements IRefundDbService {
async createRefund(data: NewRefund): Promise<Refund> {
const [refund] = await db.insert(refunds).values(data).returning()
return refund
}
async updateRefund(id: number, data: Partial<NewRefund>): Promise<Refund> {
const [refund] = await db
.update(refunds)
.set(data)
.where(eq(refunds.id, id))
.returning()
return refund
}
async getRefundByOrderId(orderId: number): Promise<Refund | undefined> {
return db.query.refunds.findFirst({
where: eq(refunds.orderId, orderId),
})
}
async getOrderById(id: number): Promise<typeof orders.$inferSelect | undefined> {
return db.query.orders.findFirst({
where: eq(orders.id, id),
})
}
async getOrderStatusByOrderId(orderId: number): Promise<typeof orderStatus.$inferSelect | undefined> {
return db.query.orderStatus.findFirst({
where: eq(orderStatus.orderId, orderId),
})
}
async getSuccessfulPaymentByOrderId(orderId: number): Promise<typeof payments.$inferSelect | undefined> {
return db.query.payments.findFirst({
where: and(
eq(payments.orderId, orderId),
eq(payments.status, "success")
),
})
}
}
export const refundDbService: IRefundDbService = new RefundDbService()

View file

@ -0,0 +1,48 @@
import { db } from '@/src/db/db_index'
import { productAvailabilitySchedules } from '@/src/db/schema'
import { eq, desc } from 'drizzle-orm'
import { IScheduleDbService, Schedule, NewSchedule } from '@/src/trpc/apis/admin-apis/dataAccessors/interfaces/schedule-db-service.interface'
export class ScheduleDbService implements IScheduleDbService {
async createSchedule(data: NewSchedule): Promise<Schedule> {
const [schedule] = await db.insert(productAvailabilitySchedules).values(data).returning()
return schedule
}
async getAllSchedules(): Promise<Schedule[]> {
return db.query.productAvailabilitySchedules.findMany({
orderBy: desc(productAvailabilitySchedules.createdAt),
})
}
async getScheduleById(id: number): Promise<Schedule | undefined> {
return db.query.productAvailabilitySchedules.findFirst({
where: eq(productAvailabilitySchedules.id, id),
})
}
async getScheduleByName(name: string): Promise<Schedule | undefined> {
return db.query.productAvailabilitySchedules.findFirst({
where: eq(productAvailabilitySchedules.scheduleName, name),
})
}
async updateSchedule(id: number, data: Partial<NewSchedule>): Promise<Schedule> {
const [schedule] = await db
.update(productAvailabilitySchedules)
.set({ ...data, lastUpdated: new Date() })
.where(eq(productAvailabilitySchedules.id, id))
.returning()
return schedule
}
async deleteSchedule(id: number): Promise<Schedule> {
const [schedule] = await db
.delete(productAvailabilitySchedules)
.where(eq(productAvailabilitySchedules.id, id))
.returning()
return schedule
}
}
export const scheduleDbService: IScheduleDbService = new ScheduleDbService()

View file

@ -0,0 +1,142 @@
import { db } from '@/src/db/db_index'
import { deliverySlotInfo, productSlots, vendorSnippets, productInfo, productGroupInfo } from '@/src/db/schema'
import { eq, inArray, and, desc } from 'drizzle-orm'
import { ISlotDbService, Slot, NewSlot, ProductSlot, SlotWithRelations } from '@/src/trpc/apis/admin-apis/dataAccessors/interfaces/slot-db-service.interface'
export class SlotDbService implements ISlotDbService {
async getAllSlots(): Promise<SlotWithRelations[]> {
return db.query.deliverySlotInfo.findMany({
orderBy: desc(deliverySlotInfo.deliveryTime),
with: {
productSlots: {
with: {
product: {
columns: { id: true, name: true, images: true },
},
},
},
},
}) as Promise<SlotWithRelations[]>
}
async getActiveSlots(): Promise<Slot[]> {
return db.query.deliverySlotInfo.findMany({
where: eq(deliverySlotInfo.isActive, true),
orderBy: desc(deliverySlotInfo.deliveryTime),
})
}
async getSlotById(id: number): Promise<SlotWithRelations | undefined> {
return db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, id),
with: {
productSlots: {
with: {
product: {
columns: { id: true, name: true, images: true },
},
},
},
vendorSnippets: true,
},
}) as Promise<SlotWithRelations | undefined>
}
async createSlot(data: NewSlot): Promise<Slot> {
const [slot] = await db.insert(deliverySlotInfo).values(data).returning()
return slot
}
async updateSlot(id: number, data: Partial<NewSlot>): Promise<Slot> {
const [slot] = await db
.update(deliverySlotInfo)
.set(data)
.where(eq(deliverySlotInfo.id, id))
.returning()
return slot
}
async deactivateSlot(id: number): Promise<Slot> {
const [slot] = await db
.update(deliverySlotInfo)
.set({ isActive: false })
.where(eq(deliverySlotInfo.id, id))
.returning()
return slot
}
async getProductSlotsBySlotId(slotId: number): Promise<ProductSlot[]> {
return db.query.productSlots.findMany({
where: eq(productSlots.slotId, slotId),
})
}
async getProductSlotsBySlotIds(slotIds: number[]): Promise<ProductSlot[]> {
return db.query.productSlots.findMany({
where: inArray(productSlots.slotId, slotIds),
columns: { slotId: true, productId: true },
})
}
async createProductSlot(slotId: number, productId: number): Promise<void> {
await db.insert(productSlots).values({ slotId, productId })
}
async deleteProductSlot(slotId: number, productId: number): Promise<void> {
await db
.delete(productSlots)
.where(and(eq(productSlots.slotId, slotId), eq(productSlots.productId, productId)))
}
async deleteProductSlotsBySlotId(slotId: number): Promise<void> {
await db.delete(productSlots).where(eq(productSlots.slotId, slotId))
}
async getVendorSnippetsBySlotId(slotId: number): Promise<Array<{ id: number; snippetCode: string; slotId: number | null; productIds: number[]; validTill: Date | null; createdAt: Date; isPermanent: boolean | null }>> {
return db.query.vendorSnippets.findMany({
where: eq(vendorSnippets.slotId, slotId),
})
}
async createVendorSnippet(data: { snippetCode: string; slotId: number; productIds: number[]; validTill?: Date }): Promise<{ id: number; snippetCode: string; slotId: number | null; productIds: number[]; validTill: Date | null; createdAt: Date; isPermanent: boolean | null }> {
const [snippet] = await db.insert(vendorSnippets).values({
snippetCode: data.snippetCode,
slotId: data.slotId,
productIds: data.productIds,
validTill: data.validTill || null,
}).returning()
return snippet
}
async checkSnippetCodeExists(code: string): Promise<boolean> {
const existing = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.snippetCode, code),
})
return !!existing
}
async validateProductsExist(productIds: number[]): Promise<boolean> {
const products = await db.query.productInfo.findMany({
where: inArray(productInfo.id, productIds),
})
return products.length === productIds.length
}
async getProductsByIds(productIds: number[]): Promise<typeof productInfo.$inferSelect[]> {
return db.query.productInfo.findMany({
where: inArray(productInfo.id, productIds),
})
}
async getGroupsByIds(groupIds: number[]): Promise<Array<{ id: number; groupName: string; description: string | null; createdAt: Date }>> {
return db.query.productGroupInfo.findMany({
where: inArray(productGroupInfo.id, groupIds),
})
}
async withTransaction<T>(fn: (tx: any) => Promise<T>): Promise<T> {
return db.transaction(fn)
}
}
export const slotDbService: ISlotDbService = new SlotDbService()

View file

@ -0,0 +1,104 @@
import { db } from '@/src/db/db_index'
import { staffUsers, staffRoles, users, userDetails, orders } from '@/src/db/schema'
import { eq, or, ilike, and, lt, desc } from 'drizzle-orm'
import { IStaffUserDbService, StaffUser, NewStaffUser, StaffRole } from '@/src/trpc/apis/admin-apis/dataAccessors/interfaces/staff-user-db-service.interface'
export class StaffUserDbService implements IStaffUserDbService {
async getStaffUserByName(name: string): Promise<StaffUser | undefined> {
return db.query.staffUsers.findFirst({
where: eq(staffUsers.name, name),
})
}
async getAllStaff(): Promise<StaffUser[]> {
return db.query.staffUsers.findMany({
columns: { id: true, name: true },
with: {
role: {
with: {
rolePermissions: {
with: { permission: true },
},
},
},
},
})
}
async createStaffUser(data: NewStaffUser): Promise<StaffUser> {
const [user] = await db.insert(staffUsers).values(data).returning()
return user
}
async getRoleById(id: number): Promise<StaffRole | undefined> {
return db.query.staffRoles.findFirst({
where: eq(staffRoles.id, id),
})
}
async getAllRoles(): Promise<StaffRole[]> {
return db.query.staffRoles.findMany({
columns: { id: true, roleName: true },
})
}
async getUsers(options: { cursor?: number; limit: number; search?: string }): Promise<typeof users.$inferSelect[]> {
const { cursor, limit, search } = options
let whereCondition = undefined
if (search) {
whereCondition = or(
ilike(users.name, `%${search}%`),
ilike(users.email, `%${search}%`),
ilike(users.mobile, `%${search}%`)
)
}
if (cursor) {
const cursorCondition = lt(users.id, cursor)
whereCondition = whereCondition ? and(whereCondition, cursorCondition) : cursorCondition
}
return db.query.users.findMany({
where: whereCondition,
with: { userDetails: true },
orderBy: desc(users.id),
limit: limit + 1,
})
}
async getUserById(id: number): Promise<typeof users.$inferSelect | undefined> {
return db.query.users.findFirst({
where: eq(users.id, id),
with: {
userDetails: true,
orders: {
orderBy: desc(orders.createdAt),
limit: 1,
},
},
})
}
async upsertUserDetails(data: Partial<typeof userDetails.$inferInsert> & { userId: number }): Promise<void> {
await db
.insert(userDetails)
.values(data)
.onConflictDoUpdate({
target: userDetails.userId,
set: data,
})
}
async getLastOrderByUserId(userId: number): Promise<typeof orders.$inferSelect | undefined> {
const userOrders = await db.query.orders.findMany({
where: eq(orders.userId, userId),
orderBy: desc(orders.createdAt),
limit: 1,
})
return userOrders[0]
}
}
export const staffUserDbService: IStaffUserDbService = new StaffUserDbService()

View file

@ -0,0 +1,53 @@
import { db } from '@/src/db/db_index'
import { storeInfo, productInfo } from '@/src/db/schema'
import { eq, inArray } from 'drizzle-orm'
import { IStoreDbService, Store, NewStore } from '@/src/trpc/apis/admin-apis/dataAccessors/interfaces/store-db-service.interface'
export class StoreDbService implements IStoreDbService {
async getAllStores(): Promise<Store[]> {
return db.query.storeInfo.findMany({
with: { owner: true },
})
}
async getStoreById(id: number): Promise<Store | undefined> {
return db.query.storeInfo.findFirst({
where: eq(storeInfo.id, id),
with: { owner: true },
})
}
async createStore(data: NewStore): Promise<Store> {
const [store] = await db.insert(storeInfo).values(data).returning()
return store
}
async updateStore(id: number, data: Partial<NewStore>): Promise<Store> {
const [store] = await db
.update(storeInfo)
.set(data)
.where(eq(storeInfo.id, id))
.returning()
return store
}
async deleteStore(id: number): Promise<void> {
await db.delete(storeInfo).where(eq(storeInfo.id, id))
}
async assignProductsToStore(storeId: number, productIds: number[]): Promise<void> {
await db
.update(productInfo)
.set({ storeId })
.where(inArray(productInfo.id, productIds))
}
async removeProductsFromStore(storeId: number): Promise<void> {
await db
.update(productInfo)
.set({ storeId: null })
.where(eq(productInfo.storeId, storeId))
}
}
export const storeDbService: IStoreDbService = new StoreDbService()

View file

@ -0,0 +1,42 @@
import { db } from '@/src/db/db_index'
import { productTagInfo } from '@/src/db/schema'
import { eq } from 'drizzle-orm'
import { ITagDbService, Tag, NewTag } from '@/src/trpc/apis/admin-apis/dataAccessors/interfaces/tag-db-service.interface'
export class TagDbService implements ITagDbService {
async getAllTags(): Promise<Tag[]> {
return db.select().from(productTagInfo).orderBy(productTagInfo.tagName)
}
async getTagById(id: number): Promise<Tag | undefined> {
return db.query.productTagInfo.findFirst({
where: eq(productTagInfo.id, id),
})
}
async getTagByName(name: string): Promise<Tag | undefined> {
return db.query.productTagInfo.findFirst({
where: eq(productTagInfo.tagName, name.trim()),
})
}
async createTag(data: NewTag): Promise<Tag> {
const [tag] = await db.insert(productTagInfo).values(data).returning()
return tag
}
async updateTag(id: number, data: Partial<NewTag>): Promise<Tag> {
const [tag] = await db
.update(productTagInfo)
.set(data)
.where(eq(productTagInfo.id, id))
.returning()
return tag
}
async deleteTag(id: number): Promise<void> {
await db.delete(productTagInfo).where(eq(productTagInfo.id, id))
}
}
export const tagDbService: ITagDbService = new TagDbService()

View file

@ -0,0 +1,170 @@
import { db } from '@/src/db/db_index'
import { users, userDetails, orders, orderItems, orderStatus, complaints, notifCreds, unloggedUserTokens, userIncidents } from '@/src/db/schema'
import { eq, sql, desc, asc, count, max, inArray } from 'drizzle-orm'
import { IUserDbService, User, NewUser, UserDetail } from '@/src/trpc/apis/admin-apis/dataAccessors/interfaces/user-db-service.interface'
export class UserDbService implements IUserDbService {
async getUserById(id: number): Promise<User | undefined> {
return db.query.users.findFirst({
where: eq(users.id, id),
})
}
async getUserByMobile(mobile: string): Promise<User | undefined> {
return db.query.users.findFirst({
where: eq(users.mobile, mobile),
})
}
async getUsers(options: { limit: number; cursor?: number; search?: string }): Promise<User[]> {
const { limit, cursor, search } = options
const whereConditions = []
if (search && search.trim()) {
whereConditions.push(sql`${users.mobile} ILIKE ${`%${search.trim()}%`}`)
}
if (cursor) {
whereConditions.push(sql`${users.id} > ${cursor}`)
}
return db
.select()
.from(users)
.where(whereConditions.length > 0 ? sql.join(whereConditions, sql` AND `) : undefined)
.orderBy(asc(users.id))
.limit(limit + 1)
}
async createUser(data: NewUser): Promise<User> {
const [user] = await db.insert(users).values(data).returning()
return user
}
async getUserDetailsByUserId(userId: number): Promise<UserDetail | undefined> {
return db.query.userDetails.findFirst({
where: eq(userDetails.userId, userId),
})
}
async upsertUserDetails(data: Partial<UserDetail> & { userId: number }): Promise<void> {
await db
.insert(userDetails)
.values(data)
.onConflictDoUpdate({
target: userDetails.userId,
set: data,
})
}
async getOrdersByUserId(userId: number): Promise<typeof orders.$inferSelect[]> {
return db
.select()
.from(orders)
.where(eq(orders.userId, userId))
.orderBy(desc(orders.createdAt))
}
async getLastOrderByUserId(userId: number): Promise<typeof orders.$inferSelect | undefined> {
const userOrders = await db
.select()
.from(orders)
.where(eq(orders.userId, userId))
.orderBy(desc(orders.createdAt))
.limit(1)
return userOrders[0]
}
async getOrderCountByUserIds(userIds: number[]): Promise<{ userId: number; totalOrders: number }[]> {
if (userIds.length === 0) return []
return db
.select({
userId: orders.userId,
totalOrders: count(orders.id),
})
.from(orders)
.where(sql`${orders.userId} IN (${sql.join(userIds, sql`, `)})`)
.groupBy(orders.userId)
}
async getLastOrderDateByUserIds(userIds: number[]): Promise<{ userId: number; lastOrderDate: Date | null }[]> {
if (userIds.length === 0) return []
return db
.select({
userId: orders.userId,
lastOrderDate: max(orders.createdAt),
})
.from(orders)
.where(sql`${orders.userId} IN (${sql.join(userIds, sql`, `)})`)
.groupBy(orders.userId)
}
async getOrderStatusByOrderIds(orderIds: number[]): Promise<{ orderId: number; isDelivered: boolean; isCancelled: boolean }[]> {
if (orderIds.length === 0) return []
return db
.select({
orderId: orderStatus.orderId,
isDelivered: orderStatus.isDelivered,
isCancelled: orderStatus.isCancelled,
})
.from(orderStatus)
.where(sql`${orderStatus.orderId} IN (${sql.join(orderIds, sql`, `)})`)
}
async getOrderItemCountByOrderIds(orderIds: number[]): Promise<{ orderId: number; itemCount: number }[]> {
if (orderIds.length === 0) return []
return db
.select({
orderId: orderItems.orderId,
itemCount: count(orderItems.id),
})
.from(orderItems)
.where(sql`${orderItems.orderId} IN (${sql.join(orderIds, sql`, `)})`)
.groupBy(orderItems.orderId)
}
async getUnresolvedComplaintCount(): Promise<number> {
return db.$count(complaints, eq(complaints.isResolved, false))
}
async getAllNotifTokens(): Promise<string[]> {
const tokens = await db.select({ token: notifCreds.token }).from(notifCreds)
return tokens.map(t => t.token)
}
async getNotifTokensByUserIds(userIds: number[]): Promise<string[]> {
const tokens = await db
.select({ token: notifCreds.token })
.from(notifCreds)
.where(inArray(notifCreds.userId, userIds))
return tokens.map(t => t.token)
}
async getUnloggedTokens(): Promise<string[]> {
const tokens = await db.select({ token: unloggedUserTokens.token }).from(unloggedUserTokens)
return tokens.map(t => t.token)
}
async getUserIncidentsByUserId(userId: number): Promise<Array<typeof userIncidents.$inferSelect & { order?: { orderStatus: Array<{ isCancelled: boolean }> } | null; addedBy?: { name: string | null } | null }>> {
return db.query.userIncidents.findMany({
where: eq(userIncidents.userId, userId),
with: {
order: {
with: {
orderStatus: true,
},
},
addedBy: true,
},
orderBy: desc(userIncidents.dateAdded),
})
}
async createUserIncident(data: { userId: number; orderId?: number | null; adminComment?: string | null; addedBy: number; negativityScore?: number | null }): Promise<typeof userIncidents.$inferSelect> {
const [incident] = await db.insert(userIncidents).values(data).returning()
return incident
}
}
export const userDbService: IUserDbService = new UserDbService()

View file

@ -0,0 +1,132 @@
import { db } from '@/src/db/db_index'
import { vendorSnippets, deliverySlotInfo, orders, orderItems, productInfo } from '@/src/db/schema'
import { eq, and, inArray, gt, asc, desc } from 'drizzle-orm'
import { IVendorSnippetDbService, VendorSnippet, NewVendorSnippet } from '@/src/trpc/apis/admin-apis/dataAccessors/interfaces/vendor-snippet-db-service.interface'
export class VendorSnippetDbService implements IVendorSnippetDbService {
async createSnippet(data: NewVendorSnippet): Promise<VendorSnippet> {
const [snippet] = await db.insert(vendorSnippets).values(data).returning()
return snippet
}
async getAllSnippets(): Promise<VendorSnippet[]> {
return db.query.vendorSnippets.findMany({
with: { slot: true },
orderBy: desc(vendorSnippets.createdAt),
})
}
async getSnippetById(id: number): Promise<VendorSnippet | undefined> {
return db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.id, id),
with: { slot: true },
})
}
async getSnippetByCode(code: string): Promise<VendorSnippet | undefined> {
return db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.snippetCode, code),
})
}
async updateSnippet(id: number, data: Partial<NewVendorSnippet>): Promise<VendorSnippet> {
const [snippet] = await db
.update(vendorSnippets)
.set(data)
.where(eq(vendorSnippets.id, id))
.returning()
return snippet
}
async deleteSnippet(id: number): Promise<VendorSnippet> {
const [snippet] = await db
.delete(vendorSnippets)
.where(eq(vendorSnippets.id, id))
.returning()
return snippet
}
async checkSnippetCodeExists(code: string): Promise<boolean> {
const existing = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.snippetCode, code),
})
return !!existing
}
async getSlotById(id: number): Promise<typeof deliverySlotInfo.$inferSelect | undefined> {
return db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, id),
})
}
async getUpcomingSlots(since: Date): Promise<typeof deliverySlotInfo.$inferSelect[]> {
return db.query.deliverySlotInfo.findMany({
where: and(
eq(deliverySlotInfo.isActive, true),
gt(deliverySlotInfo.deliveryTime, since)
),
orderBy: asc(deliverySlotInfo.deliveryTime),
})
}
async getProductsByIds(ids: number[]): Promise<Array<{ id: number; name: string }>> {
return db.query.productInfo.findMany({
where: inArray(productInfo.id, ids),
columns: { id: true, name: true },
})
}
async validateProductsExist(ids: number[]): Promise<boolean> {
const products = await db.query.productInfo.findMany({
where: inArray(productInfo.id, ids),
})
return products.length === ids.length
}
async getOrdersBySlotId(slotId: number): Promise<typeof orders.$inferSelect[]> {
return db.query.orders.findMany({
where: eq(orders.slotId, slotId),
with: {
orderItems: {
with: {
product: {
with: { unit: true },
},
},
},
orderStatus: true,
user: true,
slot: true,
},
orderBy: desc(orders.createdAt),
})
}
async getOrderItemsByOrderIds(orderIds: number[]): Promise<typeof orderItems.$inferSelect[]> {
return db.query.orderItems.findMany({
where: inArray(orderItems.orderId, orderIds),
})
}
async getOrderItemById(id: number): Promise<typeof orderItems.$inferSelect | undefined> {
return db.query.orderItems.findFirst({
where: eq(orderItems.id, id),
})
}
async updateOrderItemPackaging(id: number, is_packaged: boolean): Promise<void> {
await db
.update(orderItems)
.set({ is_packaged })
.where(eq(orderItems.id, id))
}
async hasSnippetForSlot(slotId: number): Promise<boolean> {
const snippet = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.slotId, slotId),
})
return !!snippet
}
}
export const vendorSnippetDbService: IVendorSnippetDbService = new VendorSnippetDbService()

View file

@ -1,29 +1,28 @@
import { router, protectedProcedure } from '@/src/trpc/trpc-index';
import { z } from 'zod';
import { db } from '@/src/db/db_index';
import { addresses, orders, orderStatus, deliverySlotInfo } from '@/src/db/schema';
import { eq, and, gte } from 'drizzle-orm';
import dayjs from 'dayjs';
import { extractCoordsFromRedirectUrl } from '@/src/lib/license-util';
import { userAddressDbService } from '@/src/trpc/apis/user-apis/dataAccessors/main'
export const addressRouter = router({
getDefaultAddress: protectedProcedure
.query(async ({ ctx }) => {
const userId = ctx.user.userId;
const userId = ctx.user?.userId;
if (!userId) {
throw new Error('Unauthorized')
}
const [defaultAddress] = await db
.select()
.from(addresses)
.where(and(eq(addresses.userId, userId), eq(addresses.isDefault, true)))
.limit(1);
const defaultAddress = await userAddressDbService.getDefaultAddress(userId)
return { success: true, data: defaultAddress || null };
}),
getUserAddresses: protectedProcedure
.query(async ({ ctx }) => {
const userId = ctx.user.userId;
const userAddresses = await db.select().from(addresses).where(eq(addresses.userId, userId));
const userId = ctx.user?.userId;
if (!userId) {
throw new Error('Unauthorized')
}
const userAddresses = await userAddressDbService.getUserAddresses(userId)
return { success: true, data: userAddresses };
}),
@ -42,7 +41,10 @@ export const addressRouter = router({
googleMapsUrl: z.string().optional(),
}))
.mutation(async ({ input, ctx }) => {
const userId = ctx.user.userId;
const userId = ctx.user?.userId;
if (!userId) {
throw new Error('Unauthorized')
}
const { name, phone, addressLine1, addressLine2, city, state, pincode, isDefault, googleMapsUrl } = input;
let { latitude, longitude } = input;
@ -62,10 +64,10 @@ export const addressRouter = router({
// If setting as default, unset other defaults
if (isDefault) {
await db.update(addresses).set({ isDefault: false }).where(eq(addresses.userId, userId));
await userAddressDbService.unsetDefaultForUser(userId)
}
const [newAddress] = await db.insert(addresses).values({
const newAddress = await userAddressDbService.createAddress({
userId,
name,
phone,
@ -78,7 +80,7 @@ export const addressRouter = router({
latitude,
longitude,
googleMapsUrl,
}).returning();
})
return { success: true, data: newAddress };
}),
@ -99,7 +101,10 @@ export const addressRouter = router({
googleMapsUrl: z.string().optional(),
}))
.mutation(async ({ input, ctx }) => {
const userId = ctx.user.userId;
const userId = ctx.user?.userId;
if (!userId) {
throw new Error('Unauthorized')
}
const { id, name, phone, addressLine1, addressLine2, city, state, pincode, isDefault, googleMapsUrl } = input;
let { latitude, longitude } = input;
@ -113,14 +118,14 @@ export const addressRouter = router({
}
// Check if address exists and belongs to user
const existingAddress = await db.select().from(addresses).where(and(eq(addresses.id, id), eq(addresses.userId, userId))).limit(1);
if (existingAddress.length === 0) {
const existingAddress = await userAddressDbService.getAddressByIdForUser(id, userId)
if (!existingAddress) {
throw new Error('Address not found');
}
// If setting as default, unset other defaults
if (isDefault) {
await db.update(addresses).set({ isDefault: false }).where(eq(addresses.userId, userId));
await userAddressDbService.unsetDefaultForUser(userId)
}
const updateData: any = {
@ -142,7 +147,7 @@ export const addressRouter = router({
updateData.longitude = longitude;
}
const [updatedAddress] = await db.update(addresses).set(updateData).where(and(eq(addresses.id, id), eq(addresses.userId, userId))).returning();
const updatedAddress = await userAddressDbService.updateAddressForUser(id, userId, updateData)
return { success: true, data: updatedAddress };
}),
@ -152,42 +157,32 @@ export const addressRouter = router({
id: z.number().int().positive(),
}))
.mutation(async ({ input, ctx }) => {
const userId = ctx.user.userId;
const userId = ctx.user?.userId;
if (!userId) {
throw new Error('Unauthorized')
}
const { id } = input;
// Check if address exists and belongs to user
const existingAddress = await db.select().from(addresses).where(and(eq(addresses.id, id), eq(addresses.userId, userId))).limit(1);
if (existingAddress.length === 0) {
const existingAddress = await userAddressDbService.getAddressByIdForUser(id, userId)
if (!existingAddress) {
throw new Error('Address not found or does not belong to user');
}
// Check if address is attached to any ongoing orders using joins
const ongoingOrders = await db.select({
order: orders,
status: orderStatus,
slot: deliverySlotInfo
})
.from(orders)
.innerJoin(orderStatus, eq(orders.id, orderStatus.orderId))
.innerJoin(deliverySlotInfo, eq(orders.slotId, deliverySlotInfo.id))
.where(and(
eq(orders.addressId, id),
eq(orderStatus.isCancelled, false),
gte(deliverySlotInfo.deliveryTime, new Date())
))
.limit(1);
const hasOngoingOrders = await userAddressDbService.hasOngoingOrdersForAddress(id)
if (ongoingOrders.length > 0) {
if (hasOngoingOrders) {
throw new Error('Address is attached to an ongoing order. Please cancel the order first.');
}
// Prevent deletion of default address
if (existingAddress[0].isDefault) {
if (existingAddress.isDefault) {
throw new Error('Cannot delete default address. Please set another address as default first.');
}
// Delete the address
await db.delete(addresses).where(and(eq(addresses.id, id), eq(addresses.userId, userId)));
await userAddressDbService.deleteAddressForUser(id, userId)
return { success: true, message: 'Address deleted successfully' };
}),

View file

@ -1,20 +1,12 @@
import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index';
import { z } from 'zod';
import bcrypt from 'bcryptjs';
import { eq } from 'drizzle-orm';
import { db } from '@/src/db/db_index';
import {
users, userCreds, userDetails, addresses, cartItems, complaints,
couponApplicableUsers, couponUsage, notifCreds, notifications,
orderItems, orderStatus, orders, payments, refunds,
productReviews, reservedCoupons
} from '@/src/db/schema';
import { generateSignedUrlFromS3Url, claimUploadUrl, scaffoldAssetUrl } from '@/src/lib/s3-client';
import { deleteS3Image } from '@/src/lib/delete-image';
import { ApiError } from '@/src/lib/api-error';
import catchAsync from '@/src/lib/catch-async';
import { sendOtp, verifyOtpUtil, getOtpCreds } from '@/src/lib/otp-utils';
import { signToken } from '@/src/lib/jwt-utils';
import { userAuthDbService } from '@/src/trpc/apis/user-apis/dataAccessors/main'
interface LoginRequest {
identifier: string; // email or mobile
@ -64,22 +56,11 @@ export const authRouter = router({
}
// Find user by email or mobile
const [user] = await db
.select()
.from(users)
.where(eq(users.email, identifier.toLowerCase()))
.limit(1);
let foundUser = user;
let foundUser = await userAuthDbService.getUserByEmail(identifier.toLowerCase())
if (!foundUser) {
// Try mobile if email didn't work
const [userByMobile] = await db
.select()
.from(users)
.where(eq(users.mobile, identifier))
.limit(1);
foundUser = userByMobile;
foundUser = await userAuthDbService.getUserByMobile(identifier)
}
if (!foundUser) {
@ -87,22 +68,14 @@ export const authRouter = router({
}
// Get user credentials
const [userCredentials] = await db
.select()
.from(userCreds)
.where(eq(userCreds.userId, foundUser.id))
.limit(1);
const userCredentials = await userAuthDbService.getUserCredsByUserId(foundUser.id)
if (!userCredentials) {
throw new ApiError('Account setup incomplete. Please contact support.', 401);
}
// Get user details for profile image
const [userDetail] = await db
.select()
.from(userDetails)
.where(eq(userDetails.userId, foundUser.id))
.limit(1);
const userDetail = await userAuthDbService.getUserDetailsByUserId(foundUser.id)
// Generate signed URL for profile image if it exists
const profileImageSignedUrl = userDetail?.profileImage
@ -167,22 +140,14 @@ export const authRouter = router({
}
// Check if email already exists
const [existingEmail] = await db
.select()
.from(users)
.where(eq(users.email, email.toLowerCase()))
.limit(1);
const existingEmail = await userAuthDbService.getUserByEmail(email.toLowerCase())
if (existingEmail) {
throw new ApiError('Email already registered', 409);
}
// Check if mobile already exists
const [existingMobile] = await db
.select()
.from(users)
.where(eq(users.mobile, cleanMobile))
.limit(1);
const existingMobile = await userAuthDbService.getUserByMobile(cleanMobile)
if (existingMobile) {
throw new ApiError('Mobile number already registered', 409);
@ -192,35 +157,13 @@ export const authRouter = router({
const hashedPassword = await bcrypt.hash(password, 12);
// Create user and credentials in a transaction
const newUser = await db.transaction(async (tx) => {
// Create user
const [user] = await tx
.insert(users)
.values({
const newUser = await userAuthDbService.createUserWithCredsAndDetails({
name: name.trim(),
email: email.toLowerCase().trim(),
mobile: cleanMobile,
passwordHash: hashedPassword,
imageKey: imageKey || null,
})
.returning();
// Create user credentials
await tx
.insert(userCreds)
.values({
userId: user.id,
userPassword: hashedPassword,
});
// Create user details with profile image if provided
if (imageKey) {
await tx.insert(userDetails).values({
userId: user.id,
profileImage: imageKey,
});
}
return user;
});
// Claim upload URL if image was provided
if (imageKey) {
@ -234,11 +177,7 @@ export const authRouter = router({
const token = await generateToken(newUser.id);
// Get user details for profile image
const [userDetail] = await db
.select()
.from(userDetails)
.where(eq(userDetails.userId, newUser.id))
.limit(1);
const userDetail = await userAuthDbService.getUserDetailsByUserId(newUser.id)
const profileImageUrl = userDetail?.profileImage
? scaffoldAssetUrl(userDetail.profileImage)
@ -288,21 +227,15 @@ export const authRouter = router({
}
// Find user
let user = await db.query.users.findFirst({
where: eq(users.mobile, input.mobile),
});
let user = await userAuthDbService.getUserByMobile(input.mobile)
// If user doesn't exist, create one
if (!user) {
const [newUser] = await db
.insert(users)
.values({
user = await userAuthDbService.createUser({
name: null,
email: null,
mobile: input.mobile,
})
.returning();
user = newUser;
}
// Generate JWT
@ -327,60 +260,34 @@ export const authRouter = router({
password: z.string().min(6, 'Password must be at least 6 characters'),
}))
.mutation(async ({ input, ctx }) => {
const userId = ctx.user.userId;
const userId = ctx.user?.userId;
if (!userId) {
throw new ApiError('User not authenticated', 401);
}
const hashedPassword = await bcrypt.hash(input.password, 10);
// Insert if not exists, then update if exists
try {
await db.insert(userCreds).values({
userId: userId,
userPassword: hashedPassword,
});
// Insert succeeded - new credentials created
} catch (error: any) {
// Insert failed - check if it's a unique constraint violation
if (error.code === '23505') { // PostgreSQL unique constraint violation
// Update existing credentials
await db.update(userCreds).set({
userPassword: hashedPassword,
}).where(eq(userCreds.userId, userId));
} else {
// Re-throw if it's a different error
throw error;
}
}
await userAuthDbService.upsertUserCreds(userId, hashedPassword)
return { success: true, message: 'Password updated successfully' };
}),
getProfile: protectedProcedure
.query(async ({ ctx }) => {
const userId = ctx.user.userId;
const userId = ctx.user?.userId;
if (!userId) {
throw new ApiError('User not authenticated', 401);
}
const [user] = await db
.select()
.from(users)
.where(eq(users.id, userId))
.limit(1);
const user = await userAuthDbService.getUserById(userId)
if (!user) {
throw new ApiError('User not found', 404);
}
// Get user details for profile image
const [userDetail] = await db
.select()
.from(userDetails)
.where(eq(userDetails.userId, userId))
.limit(1);
const userDetail = await userAuthDbService.getUserDetailsByUserId(userId)
const profileImageUrl = userDetail?.profileImage
? scaffoldAssetUrl(userDetail.profileImage)
@ -413,7 +320,7 @@ export const authRouter = router({
imageKey: z.string().optional(),
}))
.mutation(async ({ input, ctx }) => {
const userId = ctx.user.userId;
const userId = ctx.user?.userId;
const { imageKey, ...updateData } = input;
if (!userId) {
@ -421,9 +328,7 @@ export const authRouter = router({
}
// Get current user details
const currentDetail = await db.query.userDetails.findFirst({
where: eq(userDetails.userId, userId),
});
const currentDetail = await userAuthDbService.getUserDetailsByUserId(userId)
let newImageUrl: string | null | undefined = currentDetail?.profileImage;
@ -449,46 +354,26 @@ export const authRouter = router({
// Update user name if provided
if (updateData.name) {
await db.update(users)
.set({ name: updateData.name.trim() })
.where(eq(users.id, userId));
await userAuthDbService.updateUserName(userId, updateData.name.trim())
}
// Update user email if provided
if (updateData.email) {
// Check if email already exists (but belongs to different user)
const [existingUser] = await db
.select()
.from(users)
.where(eq(users.email, updateData.email.toLowerCase().trim()))
.limit(1);
const existingUser = await userAuthDbService.getUserByEmail(updateData.email.toLowerCase().trim())
if (existingUser && existingUser.id !== userId) {
throw new ApiError('Email already in use by another account', 409);
}
await db.update(users)
.set({ email: updateData.email.toLowerCase().trim() })
.where(eq(users.id, userId));
await userAuthDbService.updateUserEmail(userId, updateData.email.toLowerCase().trim())
}
// Upsert user details
if (currentDetail) {
// Update existing
await db.update(userDetails)
.set({
await userAuthDbService.upsertUserDetails(userId, {
...updateData,
profileImage: newImageUrl,
})
.where(eq(userDetails.userId, userId));
} else {
// Insert new
await db.insert(userDetails).values({
userId: userId,
...updateData,
profileImage: newImageUrl,
});
}
return {
success: true,
@ -501,7 +386,7 @@ export const authRouter = router({
mobile: z.string().min(10, 'Mobile number is required'),
}))
.mutation(async ({ ctx, input }) => {
const userId = ctx.user.userId;
const userId = ctx.user?.userId;
const { mobile } = input;
if (!userId) {
@ -509,10 +394,7 @@ export const authRouter = router({
}
// Double-check: verify user exists and is the authenticated user
const existingUser = await db.query.users.findFirst({
where: eq(users.id, userId),
columns: { id: true, mobile: true },
});
const existingUser = await userAuthDbService.getUserById(userId)
if (!existingUser) {
throw new ApiError('User not found', 404);
@ -533,48 +415,7 @@ export const authRouter = router({
}
// Use transaction for atomic deletion
await db.transaction(async (tx) => {
// Phase 1: Direct references (safe to delete first)
await tx.delete(notifCreds).where(eq(notifCreds.userId, userId));
await tx.delete(couponApplicableUsers).where(eq(couponApplicableUsers.userId, userId));
await tx.delete(couponUsage).where(eq(couponUsage.userId, userId));
await tx.delete(complaints).where(eq(complaints.userId, userId));
await tx.delete(cartItems).where(eq(cartItems.userId, userId));
await tx.delete(notifications).where(eq(notifications.userId, userId));
await tx.delete(productReviews).where(eq(productReviews.userId, userId));
// Update reserved coupons (set redeemedBy to null)
await tx.update(reservedCoupons)
.set({ redeemedBy: null })
.where(eq(reservedCoupons.redeemedBy, userId));
// Phase 2: Order dependencies
const userOrders = await tx
.select({ id: orders.id })
.from(orders)
.where(eq(orders.userId, userId));
for (const order of userOrders) {
await tx.delete(orderItems).where(eq(orderItems.orderId, order.id));
await tx.delete(orderStatus).where(eq(orderStatus.orderId, order.id));
await tx.delete(payments).where(eq(payments.orderId, order.id));
await tx.delete(refunds).where(eq(refunds.orderId, order.id));
// Additional coupon usage entries linked to specific orders
await tx.delete(couponUsage).where(eq(couponUsage.orderId, order.id));
await tx.delete(complaints).where(eq(complaints.orderId, order.id));
}
// Delete orders
await tx.delete(orders).where(eq(orders.userId, userId));
// Phase 3: Addresses (now safe since orders are deleted)
await tx.delete(addresses).where(eq(addresses.userId, userId));
// Phase 4: Core user data
await tx.delete(userDetails).where(eq(userDetails.userId, userId));
await tx.delete(userCreds).where(eq(userCreds.userId, userId));
await tx.delete(users).where(eq(users.id, userId));
});
await userAuthDbService.deleteAccountByUserId(userId)
return { success: true, message: 'Account deleted successfully' };
}),

View file

@ -1,14 +1,9 @@
import { db } from '@/src/db/db_index';
import { homeBanners } from '@/src/db/schema';
import { publicProcedure, router } from '@/src/trpc/trpc-index';
import { scaffoldAssetUrl } from '@/src/lib/s3-client';
import { isNotNull, asc } from 'drizzle-orm';
import { userBannerDbService } from '@/src/trpc/apis/user-apis/dataAccessors/main'
export async function scaffoldBanners() {
const banners = await db.query.homeBanners.findMany({
where: isNotNull(homeBanners.serialNum), // Only show assigned banners
orderBy: asc(homeBanners.serialNum), // Order by slot number 1-4
});
const banners = await userBannerDbService.getActiveBanners()
// Convert S3 keys to signed URLs for client
const bannersWithSignedUrls = banners.map((banner) => ({

View file

@ -1,11 +1,9 @@
import { router, protectedProcedure, publicProcedure } from '@/src/trpc/trpc-index';
import { z } from 'zod';
import { db } from '@/src/db/db_index';
import { cartItems, productInfo, units, productSlots, deliverySlotInfo } from '@/src/db/schema';
import { eq, and, sql, inArray, gt } from 'drizzle-orm';
import { ApiError } from '@/src/lib/api-error';
import { generateSignedUrlsFromS3Urls, scaffoldAssetUrl } from '@/src/lib/s3-client';
import { getProductSlots, getMultipleProductsSlots } from '@/src/stores/slot-store';
import { scaffoldAssetUrl } from '@/src/lib/s3-client';
import { getMultipleProductsSlots } from '@/src/stores/slot-store';
import { userCartDbService } from '@/src/trpc/apis/user-apis/dataAccessors/main'
interface CartResponse {
items: any[];
@ -14,23 +12,7 @@ interface CartResponse {
}
const getCartData = async (userId: number): Promise<CartResponse> => {
const cartItemsWithProducts = await db
.select({
cartId: cartItems.id,
productId: productInfo.id,
productName: productInfo.name,
productPrice: productInfo.price,
productImages: productInfo.images,
productQuantity: productInfo.productQuantity,
isOutOfStock: productInfo.isOutOfStock,
unitShortNotation: units.shortNotation,
quantity: cartItems.quantity,
addedAt: cartItems.addedAt,
})
.from(cartItems)
.innerJoin(productInfo, eq(cartItems.productId, productInfo.id))
.innerJoin(units, eq(productInfo.unitId, units.id))
.where(eq(cartItems.userId, userId));
const cartItemsWithProducts = await userCartDbService.getCartItemsWithProducts(userId)
// Generate signed URLs for images
const cartWithSignedUrls = await Promise.all(
@ -64,7 +46,10 @@ const getCartData = async (userId: number): Promise<CartResponse> => {
export const cartRouter = router({
getCart: protectedProcedure
.query(async ({ ctx }): Promise<CartResponse> => {
const userId = ctx.user.userId;
const userId = ctx.user?.userId;
if (!userId) {
throw new ApiError('Unauthorized', 401)
}
return await getCartData(userId);
}),
@ -74,7 +59,10 @@ export const cartRouter = router({
quantity: z.number().int().positive(),
}))
.mutation(async ({ input, ctx }): Promise<CartResponse> => {
const userId = ctx.user.userId;
const userId = ctx.user?.userId;
if (!userId) {
throw new ApiError('Unauthorized', 401)
}
const { productId, quantity } = input;
// Validate input
@ -83,33 +71,21 @@ export const cartRouter = router({
}
// Check if product exists
const product = await db.query.productInfo.findFirst({
where: eq(productInfo.id, productId),
});
const product = await userCartDbService.getProductById(productId)
if (!product) {
throw new ApiError("Product not found", 404);
}
// Check if item already exists in cart
const existingItem = await db.query.cartItems.findFirst({
where: and(eq(cartItems.userId, userId), eq(cartItems.productId, productId)),
});
const existingItem = await userCartDbService.getCartItemByUserAndProduct(userId, productId)
if (existingItem) {
// Update quantity
await db.update(cartItems)
.set({
quantity: sql`${cartItems.quantity} + ${quantity}`,
})
.where(eq(cartItems.id, existingItem.id));
await userCartDbService.incrementCartItemQuantity(existingItem.id, quantity)
} else {
// Insert new item
await db.insert(cartItems).values({
userId,
productId,
quantity: quantity.toString(),
});
await userCartDbService.createCartItem(userId, productId, quantity)
}
// Return updated cart
@ -122,20 +98,17 @@ export const cartRouter = router({
quantity: z.number().int().min(0),
}))
.mutation(async ({ input, ctx }): Promise<CartResponse> => {
const userId = ctx.user.userId;
const userId = ctx.user?.userId;
if (!userId) {
throw new ApiError('Unauthorized', 401)
}
const { itemId, quantity } = input;
if (!quantity || quantity <= 0) {
throw new ApiError("Positive quantity required", 400);
}
const [updatedItem] = await db.update(cartItems)
.set({ quantity: quantity.toString() })
.where(and(
eq(cartItems.id, itemId),
eq(cartItems.userId, userId)
))
.returning();
const updatedItem = await userCartDbService.updateCartItemQuantity(itemId, userId, quantity)
if (!updatedItem) {
throw new ApiError("Cart item not found", 404);
@ -150,15 +123,13 @@ export const cartRouter = router({
itemId: z.number().int().positive(),
}))
.mutation(async ({ input, ctx }): Promise<CartResponse> => {
const userId = ctx.user.userId;
const userId = ctx.user?.userId;
if (!userId) {
throw new ApiError('Unauthorized', 401)
}
const { itemId } = input;
const [deletedItem] = await db.delete(cartItems)
.where(and(
eq(cartItems.id, itemId),
eq(cartItems.userId, userId)
))
.returning();
const deletedItem = await userCartDbService.deleteCartItem(itemId, userId)
if (!deletedItem) {
throw new ApiError("Cart item not found", 404);
@ -170,9 +141,12 @@ export const cartRouter = router({
clearCart: protectedProcedure
.mutation(async ({ ctx }) => {
const userId = ctx.user.userId;
const userId = ctx.user?.userId;
if (!userId) {
throw new ApiError('Unauthorized', 401)
}
await db.delete(cartItems).where(eq(cartItems.userId, userId));
await userCartDbService.clearCart(userId)
return {
items: [],

View file

@ -1,28 +1,17 @@
import { router, protectedProcedure } from '@/src/trpc/trpc-index';
import { z } from 'zod';
import { db } from '@/src/db/db_index';
import { complaints } from '@/src/db/schema';
import { eq } from 'drizzle-orm';
import { scaffoldAssetUrl, claimUploadUrl } from '@/src/lib/s3-client';
import { userComplaintDbService } from '@/src/trpc/apis/user-apis/dataAccessors/main'
export const complaintRouter = router({
getAll: protectedProcedure
.query(async ({ ctx }) => {
const userId = ctx.user.userId;
const userId = ctx.user?.userId;
if (!userId) {
throw new Error('Unauthorized')
}
const userComplaints = await db
.select({
id: complaints.id,
complaintBody: complaints.complaintBody,
response: complaints.response,
isResolved: complaints.isResolved,
createdAt: complaints.createdAt,
orderId: complaints.orderId,
images: complaints.images,
})
.from(complaints)
.where(eq(complaints.userId, userId))
.orderBy(complaints.createdAt);
const userComplaints = await userComplaintDbService.getComplaintsByUserId(userId)
return {
complaints: userComplaints.map(c => ({
@ -44,10 +33,13 @@ export const complaintRouter = router({
imageKeys: z.array(z.string()).optional(),
}))
.mutation(async ({ input, ctx }) => {
const userId = ctx.user.userId;
const userId = ctx.user?.userId;
if (!userId) {
throw new Error('Unauthorized')
}
const { orderId, complaintBody, imageKeys } = input;
await db.insert(complaints).values({
await userComplaintDbService.createComplaint({
userId,
orderId: orderId || null,
complaintBody: complaintBody.trim(),

View file

@ -1,16 +1,9 @@
import { router, protectedProcedure } from '@/src/trpc/trpc-index';
import { z } from 'zod';
import { db } from '@/src/db/db_index';
import { coupons, couponUsage, couponApplicableUsers, reservedCoupons, couponApplicableProducts } from '@/src/db/schema';
import { eq, and, or, gt, isNull, sql } from 'drizzle-orm';
import { ApiError } from '@/src/lib/api-error';
import { userCouponDbService } from '@/src/trpc/apis/user-apis/dataAccessors/main'
import { users } from '@/src/db/schema';
type CouponWithRelations = typeof coupons.$inferSelect & {
applicableUsers: (typeof couponApplicableUsers.$inferSelect & { user: typeof users.$inferSelect })[];
usages: typeof couponUsage.$inferSelect[];
};
type CouponWithRelations = import('@/src/trpc/apis/user-apis/dataAccessors/interfaces/user-coupon-db-service.interface').CouponWithRelations
export interface EligibleCoupon {
id: number;
@ -65,33 +58,13 @@ export const userCouponRouter = router({
.query(async ({ ctx }) => {
try {
const userId = ctx.user.userId;
const userId = ctx.user?.userId;
if (!userId) {
throw new ApiError('User not authenticated', 401)
}
// Get all active, non-expired coupons
const allCoupons = await db.query.coupons.findMany({
where: and(
eq(coupons.isInvalidated, false),
or(
isNull(coupons.validTill),
gt(coupons.validTill, new Date())
)
),
with: {
usages: {
where: eq(couponUsage.userId, userId)
},
applicableUsers: {
with: {
user: true
}
},
applicableProducts: {
with: {
product: true
}
},
}
});
const allCoupons = await userCouponDbService.getActiveCouponsForUser(userId)
// Filter to only coupons applicable to current user
const applicableCoupons = allCoupons.filter(coupon => {
@ -111,34 +84,14 @@ export const userCouponRouter = router({
getProductCoupons: protectedProcedure
.input(z.object({ productId: z.number().int().positive() }))
.query(async ({ input, ctx }) => {
const userId = ctx.user.userId;
const userId = ctx.user?.userId;
if (!userId) {
throw new ApiError('User not authenticated', 401)
}
const { productId } = input;
// Get all active, non-expired coupons
const allCoupons = await db.query.coupons.findMany({
where: and(
eq(coupons.isInvalidated, false),
or(
isNull(coupons.validTill),
gt(coupons.validTill, new Date())
)
),
with: {
usages: {
where: eq(couponUsage.userId, userId)
},
applicableUsers: {
with: {
user: true
}
},
applicableProducts: {
with: {
product: true
}
},
}
});
const allCoupons = await userCouponDbService.getActiveCouponsForUser(userId)
// Filter to only coupons applicable to current user and product
const applicableCoupons = allCoupons.filter(coupon => {
@ -156,21 +109,13 @@ export const userCouponRouter = router({
getMyCoupons: protectedProcedure
.query(async ({ ctx }) => {
const userId = ctx.user.userId;
const userId = ctx.user?.userId;
if (!userId) {
throw new ApiError('User not authenticated', 401)
}
// Get all coupons
const allCoupons = await db.query.coupons.findMany({
with: {
usages: {
where: eq(couponUsage.userId, userId)
},
applicableUsers: {
with: {
user: true
}
}
}
});
const allCoupons = await userCouponDbService.getAllCouponsForUser(userId)
// Filter coupons in JS: not invalidated, applicable to user, and not expired
const applicableCoupons = (allCoupons as CouponWithRelations[]).filter(coupon => {
@ -226,16 +171,14 @@ export const userCouponRouter = router({
redeemReservedCoupon: protectedProcedure
.input(z.object({ secretCode: z.string() }))
.mutation(async ({ input, ctx }) => {
const userId = ctx.user.userId;
const userId = ctx.user?.userId;
if (!userId) {
throw new ApiError('User not authenticated', 401)
}
const { secretCode } = input;
// Find the reserved coupon
const reservedCoupon = await db.query.reservedCoupons.findFirst({
where: and(
eq(reservedCoupons.secretCode, secretCode.toUpperCase()),
eq(reservedCoupons.isRedeemed, false)
),
});
const reservedCoupon = await userCouponDbService.getReservedCouponBySecretCode(secretCode)
if (!reservedCoupon) {
throw new ApiError("Invalid or already redeemed coupon code", 400);
@ -247,49 +190,7 @@ export const userCouponRouter = router({
}
// Create the coupon in the main table
const couponResult = await db.transaction(async (tx) => {
// Insert into coupons
const couponInsert = await tx.insert(coupons).values({
couponCode: reservedCoupon.couponCode,
isUserBased: true,
discountPercent: reservedCoupon.discountPercent,
flatDiscount: reservedCoupon.flatDiscount,
minOrder: reservedCoupon.minOrder,
productIds: reservedCoupon.productIds,
maxValue: reservedCoupon.maxValue,
isApplyForAll: false,
validTill: reservedCoupon.validTill,
maxLimitForUser: reservedCoupon.maxLimitForUser,
exclusiveApply: reservedCoupon.exclusiveApply,
createdBy: reservedCoupon.createdBy,
}).returning();
const coupon = couponInsert[0];
// Insert into couponApplicableUsers
await tx.insert(couponApplicableUsers).values({
couponId: coupon.id,
userId,
});
// Copy applicable products
if (reservedCoupon.productIds && Array.isArray(reservedCoupon.productIds) && reservedCoupon.productIds.length > 0) {
// Assuming productIds are the IDs, but wait, in schema, productIds is jsonb, but in relations, couponApplicableProducts has productId
// For simplicity, since reservedCoupons has productIds as jsonb, but to match, perhaps insert into couponApplicableProducts if needed
// But in createReservedCoupon, I inserted applicableProducts into couponApplicableProducts
// So for reserved, perhaps do the same, but since it's jsonb, maybe not.
// For now, skip, as the coupon will have productIds in coupons table.
}
// Update reserved coupon as redeemed
await tx.update(reservedCoupons).set({
isRedeemed: true,
redeemedBy: userId,
redeemedAt: new Date(),
}).where(eq(reservedCoupons.id, reservedCoupon.id));
return coupon;
});
const couponResult = await userCouponDbService.redeemReservedCoupon(userId, reservedCoupon)
return { success: true, coupon: couponResult };
}),

View file

@ -1,33 +1,22 @@
import { router, protectedProcedure } from "@/src/trpc/trpc-index";
import { z } from "zod";
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, lte } from "drizzle-orm";
import { scaffoldAssetUrl } from "@/src/lib/s3-client";
import { ApiError } from "@/src/lib/api-error";
import { z } from 'zod'
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { userOrderDbService } from '@/src/trpc/apis/user-apis/dataAccessors/main'
import type {
OrderCoupon,
OrderInsert,
OrderItemInsert,
OrderStatusInsert,
} from '@/src/trpc/apis/user-apis/dataAccessors/main'
import { scaffoldAssetUrl } from '@/src/lib/s3-client'
import { ApiError } from '@/src/lib/api-error'
import {
sendOrderPlacedNotification,
sendOrderCancelledNotification,
} from "@/src/lib/notif-job";
import { RazorpayPaymentService } from "@/src/lib/payments-utils";
import { getNextDeliveryDate } from "@/src/trpc/apis/common-apis/common";
import { CONST_KEYS, getConstant, getConstants } from "@/src/lib/const-store";
import { publishFormattedOrder, publishCancellation } from "@/src/lib/post-order-handler";
import { getSlotById } from "@/src/stores/slot-store";
} from '@/src/lib/notif-job'
import { getNextDeliveryDate } from '@/src/trpc/apis/common-apis/common'
import { CONST_KEYS, getConstant, getConstants } from '@/src/lib/const-store'
import { publishFormattedOrder, publishCancellation } from '@/src/lib/post-order-handler'
import { getSlotById } from '@/src/stores/slot-store'
const validateAndGetCoupon = async (
@ -35,40 +24,35 @@ const validateAndGetCoupon = async (
userId: number,
totalAmount: number
) => {
if (!couponId) return null;
if (!couponId) return null
const coupon = await db.query.coupons.findFirst({
where: eq(coupons.id, couponId),
with: {
usages: { where: eq(couponUsage.userId, userId) },
},
});
const coupon = await userOrderDbService.getCouponWithUsage(couponId, userId)
if (!coupon) throw new ApiError("Invalid coupon", 400);
if (!coupon) throw new ApiError('Invalid coupon', 400)
if (coupon.isInvalidated)
throw new ApiError("Coupon is no longer valid", 400);
throw new ApiError('Coupon is no longer valid', 400)
if (coupon.validTill && new Date(coupon.validTill) < new Date())
throw new ApiError("Coupon has expired", 400);
throw new ApiError('Coupon has expired', 400)
if (
coupon.maxLimitForUser &&
coupon.usages.length >= coupon.maxLimitForUser
)
throw new ApiError("Coupon usage limit exceeded", 400);
throw new ApiError('Coupon usage limit exceeded', 400)
if (
coupon.minOrder &&
parseFloat(coupon.minOrder.toString()) > totalAmount
)
throw new ApiError(
"Order amount does not meet coupon minimum requirement",
'Order amount does not meet coupon minimum requirement',
400
);
)
return coupon;
return coupon
};
const applyDiscountToOrder = (
orderTotal: number,
appliedCoupon: typeof coupons.$inferSelect | null,
appliedCoupon: OrderCoupon | null,
proportion: number
) => {
let finalOrderTotal = orderTotal;
@ -140,11 +124,9 @@ const placeOrderUtil = async (params: {
const orderGroupId = `${Date.now()}-${userId}`;
const address = await db.query.addresses.findFirst({
where: and(eq(addresses.userId, userId), eq(addresses.id, addressId)),
});
const address = await userOrderDbService.getAddressByUserId(userId, addressId)
if (!address) {
throw new ApiError("Invalid address", 400);
throw new ApiError('Invalid address', 400)
}
const ordersBySlot = new Map<
@ -158,11 +140,9 @@ const placeOrderUtil = async (params: {
>();
for (const item of selectedItems) {
const product = await db.query.productInfo.findFirst({
where: eq(productInfo.id, item.productId),
});
const product = await userOrderDbService.getProductById(item.productId)
if (!product) {
throw new ApiError(`Product ${item.productId} not found`, 400);
throw new ApiError(`Product ${item.productId} not found`, 400)
}
if (!ordersBySlot.has(item.slotId)) {
@ -173,11 +153,12 @@ const placeOrderUtil = async (params: {
if (params.isFlash) {
for (const item of selectedItems) {
const product = await db.query.productInfo.findFirst({
where: eq(productInfo.id, item.productId),
});
const product = await userOrderDbService.getProductById(item.productId)
if (!product?.isFlashAvailable) {
throw new ApiError(`Product ${item.productId} is not available for flash delivery`, 400);
throw new ApiError(
`Product ${item.productId} is not available for flash delivery`,
400
)
}
}
}
@ -204,10 +185,10 @@ const placeOrderUtil = async (params: {
const totalWithDelivery = totalAmount + expectedDeliveryCharge;
type OrderData = {
order: Omit<typeof orders.$inferInsert, "id">;
orderItems: Omit<typeof orderItems.$inferInsert, "id">[];
orderStatus: Omit<typeof orderStatus.$inferInsert, "id">;
};
order: Omit<OrderInsert, 'id'>
orderItems: Omit<OrderItemInsert, 'id'>[]
orderStatus: Omit<OrderStatusInsert, 'id'>
}
const ordersData: OrderData[] = [];
let isFirstOrder = true;
@ -233,7 +214,7 @@ const placeOrderUtil = async (params: {
orderGroupProportion
);
const order: Omit<typeof orders.$inferInsert, "id"> = {
const order: Omit<OrderInsert, 'id'> = {
userId,
addressId,
slotId: params.isFlash ? null : slotId,
@ -249,7 +230,7 @@ const placeOrderUtil = async (params: {
isFlashDelivery: params.isFlash,
};
const orderItemsData: Omit<typeof orderItems.$inferInsert, "id">[] = items.map(
const orderItemsData: Omit<OrderItemInsert, 'id'>[] = items.map(
(item) => ({
orderId: 0,
productId: item.productId,
@ -265,7 +246,7 @@ const placeOrderUtil = async (params: {
})
);
const orderStatusData: Omit<typeof orderStatus.$inferInsert, "id"> = {
const orderStatusData: Omit<OrderStatusInsert, 'id'> = {
userId,
orderId: 0,
paymentStatus: paymentMethod === "cod" ? "cod" : "pending",
@ -275,79 +256,22 @@ const placeOrderUtil = async (params: {
isFirstOrder = false;
}
const createdOrders = await 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()}`,
const createdOrders = await userOrderDbService.createOrdersWithItems({
ordersData,
paymentMethod,
})
.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, index) => {
const od = ordersData[index];
od.orderItems.forEach((item) => {
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) {
const razorpayOrder = await RazorpayPaymentService.createOrder(
sharedPaymentInfoId,
totalWithDelivery.toString()
);
await RazorpayPaymentService.insertPaymentRecord(
sharedPaymentInfoId,
razorpayOrder,
tx
);
}
return insertedOrders;
});
await db.delete(cartItems).where(
and(
eq(cartItems.userId, userId),
inArray(
cartItems.productId,
await userOrderDbService.deleteCartItemsByUserAndProductIds(
userId,
selectedItems.map((item) => item.productId)
)
)
);
if (appliedCoupon && createdOrders.length > 0) {
await db.insert(couponUsage).values({
await userOrderDbService.createCouponUsage({
userId,
couponId: appliedCoupon.id,
orderId: createdOrders[0].id as number,
orderItemId: null,
usedAt: new Date(),
});
})
}
for (const order of createdOrders) {
@ -378,12 +302,13 @@ export const orderRouter = router({
})
)
.mutation(async ({ input, ctx }) => {
const userId = ctx.user.userId;
const userId = ctx.user?.userId
if (!userId) {
throw new ApiError('Unauthorized', 401)
}
// Check if user is suspended from placing orders
const userDetail = await db.query.userDetails.findFirst({
where: eq(userDetails.userId, userId),
});
const userDetail = await userOrderDbService.getUserDetailByUserId(userId)
if (userDetail?.isSuspended) {
throw new ApiError("Unable to place order", 403);
@ -402,7 +327,10 @@ export const orderRouter = router({
if (isFlashDelivery) {
const isFlashDeliveryEnabled = await getConstant<boolean>(CONST_KEYS.isFlashDeliveryEnabled);
if (!isFlashDeliveryEnabled) {
throw new ApiError("Flash delivery is currently unavailable. Please opt for scheduled delivery.", 403);
throw new ApiError(
'Flash delivery is currently unavailable. Please opt for scheduled delivery.',
403
)
}
}
@ -410,9 +338,12 @@ export const orderRouter = router({
if (!isFlashDelivery) {
const slotIds = [...new Set(selectedItems.filter(i => i.slotId !== null).map(i => i.slotId as number))];
for (const slotId of slotIds) {
const slot = await getSlotById(slotId);
const slot = await getSlotById(slotId)
if (slot?.isCapacityFull) {
throw new ApiError("Selected delivery slot is at full capacity. Please choose another slot.", 403);
throw new ApiError(
'Selected delivery slot is at full capacity. Please choose another slot.',
403
)
}
}
}
@ -422,10 +353,10 @@ export const orderRouter = router({
// Handle flash delivery slot resolution
if (isFlashDelivery) {
// For flash delivery, set slotId to null (no specific slot assigned)
processedItems = selectedItems.map(item => ({
processedItems = selectedItems.map((item) => ({
...item,
slotId: null as any, // Type override for flash delivery
}));
}))
}
return await placeOrderUtil({
@ -450,33 +381,20 @@ export const orderRouter = router({
)
.query(async ({ input, ctx }) => {
const { page = 1, pageSize = 10 } = input || {};
const userId = ctx.user.userId;
const userId = ctx.user?.userId
if (!userId) {
throw new ApiError('Unauthorized', 401)
}
const offset = (page - 1) * pageSize;
// Get total count for pagination
const totalCountResult = await db.$count(
orders,
eq(orders.userId, userId)
);
const totalCount = totalCountResult;
const totalCount = await userOrderDbService.getOrdersCount(userId)
const userOrders = await 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: pageSize,
offset: offset,
});
const userOrders = await userOrderDbService.getOrdersWithRelations(
userId,
pageSize,
offset
)
const mappedOrders = await Promise.all(
userOrders.map(async (order) => {
@ -574,38 +492,24 @@ export const orderRouter = router({
.input(z.object({ orderId: z.string() }))
.query(async ({ input, ctx }) => {
const { orderId } = input;
const userId = ctx.user.userId;
const userId = ctx.user?.userId
if (!userId) {
throw new ApiError('Unauthorized', 401)
}
const order = await db.query.orders.findFirst({
where: and(eq(orders.id, parseInt(orderId)), eq(orders.userId, userId)),
with: {
orderItems: {
with: {
product: true,
},
},
slot: true,
paymentInfo: true,
orderStatus: {
with: {
refundCoupon: true,
},
},
refunds: true,
},
});
const order = await userOrderDbService.getOrderWithDetailsById(
parseInt(orderId),
userId
)
if (!order) {
throw new Error("Order not found");
}
// Get coupon usage for this specific order using new orderId field
const couponUsageData = await db.query.couponUsage.findMany({
where: eq(couponUsage.orderId, order.id), // Use new orderId field
with: {
coupon: true,
},
});
const couponUsageData = await userOrderDbService.getCouponUsagesByOrderId(
order.id
)
let couponData = null;
if (couponUsageData.length > 0) {
@ -734,16 +638,14 @@ export const orderRouter = router({
)
.mutation(async ({ input, ctx }) => {
try {
const userId = ctx.user.userId;
const userId = ctx.user?.userId
if (!userId) {
throw new ApiError('Unauthorized', 401)
}
const { id, reason } = input;
// Check if order exists and belongs to user
const order = await db.query.orders.findFirst({
where: eq(orders.id, Number(id)),
with: {
orderStatus: true,
},
});
const order = await userOrderDbService.getOrderWithStatus(Number(id))
if (!order) {
console.error("Order not found:", id);
@ -777,29 +679,17 @@ export const orderRouter = router({
}
// Perform database operations in transaction
const result = await db.transaction(async (tx) => {
// Update order status
await tx
.update(orderStatus)
.set({
isCancelled: true,
cancelReason: reason,
cancellationUserNotes: reason,
cancellationReviewed: false,
})
.where(eq(orderStatus.id, status.id));
// Determine refund status based on payment method
const refundStatus = order.isCod ? "na" : "pending";
const refundStatus = order.isCod ? 'na' : 'pending'
// Insert refund record
await tx.insert(refunds).values({
await userOrderDbService.cancelOrderTransaction({
statusId: status.id,
reason,
orderId: order.id,
refundStatus,
});
})
return { orderId: order.id, userId };
});
const result = { orderId: order.id, userId }
// Send notification outside transaction (idempotent operation)
await sendOrderCancelledNotification(
@ -810,10 +700,10 @@ export const orderRouter = router({
// Publish to Redis for Telegram notification
await publishCancellation(result.orderId, 'user', reason);
return { success: true, message: "Order cancelled successfully" };
return { success: true, message: 'Order cancelled successfully' }
} catch (e) {
console.log(e);
throw new ApiError("failed to cancel order");
throw new ApiError('failed to cancel order')
}
}),
@ -825,7 +715,10 @@ export const orderRouter = router({
})
)
.mutation(async ({ input, ctx }) => {
const userId = ctx.user.userId;
const userId = ctx.user?.userId
if (!userId) {
throw new ApiError('Unauthorized', 401)
}
const { id, userNotes } = input;
// Extract readable ID from orderId (e.g., ORD001 -> 1)
@ -837,12 +730,7 @@ export const orderRouter = router({
// const readableId = parseInt(readableIdMatch[1]);
// Check if order exists and belongs to user
const order = await db.query.orders.findFirst({
where: eq(orders.id, Number(id)),
with: {
orderStatus: true,
},
});
const order = await userOrderDbService.getOrderWithStatus(Number(id))
if (!order) {
console.error("Order not found:", id);
@ -876,14 +764,9 @@ export const orderRouter = router({
}
// Update user notes
await db
.update(orders)
.set({
userNotes: userNotes || null,
})
.where(eq(orders.id, order.id));
await userOrderDbService.updateOrderNotes(order.id, userNotes || null)
return { success: true, message: "Notes updated successfully" };
return { success: true, message: 'Notes updated successfully' }
}),
getRecentlyOrderedProducts: protectedProcedure
@ -896,25 +779,20 @@ export const orderRouter = router({
)
.query(async ({ input, ctx }) => {
const { limit = 20 } = input || {};
const userId = ctx.user.userId;
const userId = ctx.user?.userId
if (!userId) {
throw new ApiError('Unauthorized', 401)
}
// Get user's recent delivered orders (last 30 days)
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const recentOrders = await 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, thirtyDaysAgo)
const recentOrders = await userOrderDbService.getRecentDeliveredOrderIds(
userId,
thirtyDaysAgo,
10
)
)
.orderBy(desc(orders.createdAt))
.limit(10); // Get last 10 orders
if (recentOrders.length === 0) {
return { success: true, products: [] };
@ -923,10 +801,9 @@ export const orderRouter = router({
const orderIds = recentOrders.map((order) => order.id);
// Get unique product IDs from recent orders
const orderItemsResult = await db
.select({ productId: orderItems.productId })
.from(orderItems)
.where(inArray(orderItems.orderId, orderIds));
const orderItemsResult = await userOrderDbService.getProductIdsByOrderIds(
orderIds
)
const productIds = [
...new Set(orderItemsResult.map((item) => item.productId)),
@ -937,27 +814,10 @@ export const orderRouter = router({
}
// Get product details
const productsWithUnits = await 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)
const productsWithUnits = await userOrderDbService.getProductsWithUnitsByIds(
productIds,
limit
)
)
.orderBy(desc(productInfo.createdAt))
.limit(limit);
// Generate signed URLs for product images
const formattedProducts = await Promise.all(

View file

@ -1,158 +0,0 @@
import { router, protectedProcedure } from '@/src/trpc/trpc-index';
import { z } from 'zod';
import { db } from '@/src/db/db_index';
import { orders, payments, orderStatus } from '@/src/db/schema';
import { eq } from 'drizzle-orm';
import { ApiError } from '@/src/lib/api-error';
import crypto from 'crypto';
import { razorpayId, razorpaySecret } from "@/src/lib/env-exporter";
import { DiskPersistedSet } from "@/src/lib/disk-persisted-set";
import { RazorpayPaymentService } from "@/src/lib/payments-utils";
export const paymentRouter = router({
createRazorpayOrder: protectedProcedure //either create a new payment order or return the existing one
.input(z.object({
orderId: z.string(),
}))
.mutation(async ({ input, ctx }) => {
const userId = ctx.user.userId;
const { orderId } = input;
// Validate order exists and belongs to user
const order = await db.query.orders.findFirst({
where: eq(orders.id, parseInt(orderId)),
});
if (!order) {
throw new ApiError("Order not found", 404);
}
if (order.userId !== userId) {
throw new ApiError("Order does not belong to user", 403);
}
// Check for existing pending payment
const existingPayment = await db.query.payments.findFirst({
where: eq(payments.orderId, parseInt(orderId)),
});
if (existingPayment && existingPayment.status === 'pending') {
return {
razorpayOrderId: existingPayment.merchantOrderId,
key: razorpayId,
};
}
// Create Razorpay order and insert payment record
const razorpayOrder = await RazorpayPaymentService.createOrder(parseInt(orderId), order.totalAmount);
await RazorpayPaymentService.insertPaymentRecord(parseInt(orderId), razorpayOrder);
return {
razorpayOrderId: razorpayOrder.id,
key: razorpayId,
};
}),
verifyPayment: protectedProcedure
.input(z.object({
razorpay_payment_id: z.string(),
razorpay_order_id: z.string(),
razorpay_signature: z.string(),
}))
.mutation(async ({ input, ctx }) => {
const { razorpay_payment_id, razorpay_order_id, razorpay_signature } = input;
// Verify signature
const expectedSignature = crypto
.createHmac('sha256', razorpaySecret)
.update(razorpay_order_id + '|' + razorpay_payment_id)
.digest('hex');
if (expectedSignature !== razorpay_signature) {
throw new ApiError("Invalid payment signature", 400);
}
// Get current payment record
const currentPayment = await db.query.payments.findFirst({
where: eq(payments.merchantOrderId, razorpay_order_id),
});
if (!currentPayment) {
throw new ApiError("Payment record not found", 404);
}
// Update payment status and payload
const updatedPayload = {
...((currentPayment.payload as any) || {}),
payment_id: razorpay_payment_id,
signature: razorpay_signature,
};
const [updatedPayment] = await db
.update(payments)
.set({
status: 'success',
payload: updatedPayload,
})
.where(eq(payments.merchantOrderId, razorpay_order_id))
.returning();
// Update order status to mark payment as processed
await db
.update(orderStatus)
.set({
paymentStatus: 'success',
})
.where(eq(orderStatus.orderId, updatedPayment.orderId));
return {
success: true,
message: "Payment verified successfully",
};
}),
markPaymentFailed: protectedProcedure
.input(z.object({
merchantOrderId: z.string(),
}))
.mutation(async ({ input, ctx }) => {
const userId = ctx.user.userId;
const { merchantOrderId } = input;
// Find payment by merchantOrderId
const payment = await db.query.payments.findFirst({
where: eq(payments.merchantOrderId, merchantOrderId),
});
if (!payment) {
throw new ApiError("Payment not found", 404);
}
// Check if payment belongs to user's order
const order = await db.query.orders.findFirst({
where: eq(orders.id, payment.orderId),
});
if (!order || order.userId !== userId) {
throw new ApiError("Payment does not belong to user", 403);
}
// Update payment status to failed
await db
.update(payments)
.set({ status: 'failed' })
.where(eq(payments.id, payment.id));
return {
success: true,
message: "Payment marked as failed",
};
}),
});

View file

@ -1,12 +1,10 @@
import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index';
import { z } from 'zod';
import { db } from '@/src/db/db_index';
import { productInfo, units, productSlots, deliverySlotInfo, specialDeals, storeInfo, productTagInfo, productTags, productReviews, users } from '@/src/db/schema';
import { claimUploadUrl, extractKeyFromPresignedUrl, scaffoldAssetUrl } from '@/src/lib/s3-client';
import { ApiError } from '@/src/lib/api-error';
import { eq, and, gt, sql, inArray, desc } from 'drizzle-orm';
import { getProductById as getProductByIdFromCache, getAllProducts as getAllProductsFromCache } from '@/src/stores/product-store';
import dayjs from 'dayjs';
import { userProductDbService } from '@/src/trpc/apis/user-apis/dataAccessors/main'
// Uniform Product Type
interface Product {
@ -60,75 +58,20 @@ export const productRouter = router({
}
// If not in cache, fetch from database (fallback)
const productData = await 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);
const product = await userProductDbService.getProductById(productId)
if (productData.length === 0) {
if (!product) {
throw new Error('Product not found');
}
const product = productData[0];
// Fetch store info for this product
const storeData = product.storeId ? await db.query.storeInfo.findFirst({
where: eq(storeInfo.id, product.storeId),
columns: { id: true, name: true, description: true },
}) : null;
const storeData = product.storeId ? await userProductDbService.getStoreBasicById(product.storeId) : null
// Fetch delivery slots for this product
const deliverySlotsData = await 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),
eq(deliverySlotInfo.isCapacityFull, false),
gt(deliverySlotInfo.deliveryTime, sql`NOW()`),
gt(deliverySlotInfo.freezeTime, sql`NOW()`)
)
)
.orderBy(deliverySlotInfo.deliveryTime);
const deliverySlotsData = await userProductDbService.getDeliverySlotsForProduct(productId)
// Fetch special deals for this product
const specialDealsData = await 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);
const specialDealsData = await userProductDbService.getSpecialDealsForProduct(productId)
// Generate signed URLs for images
const signedImages = scaffoldAssetUrl((product.images as string[]) || []);
@ -140,7 +83,7 @@ export const productRouter = router({
longDescription: product.longDescription,
price: product.price.toString(),
marketPrice: product.marketPrice?.toString() || null,
unitNotation: product.unitShortNotation,
unitNotation: product.unitShortNotation || '',
images: signedImages,
isOutOfStock: product.isOutOfStock,
store: storeData ? {
@ -168,21 +111,7 @@ export const productRouter = router({
.query(async ({ input }) => {
const { productId, limit, offset } = input;
const reviews = await 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);
const reviews = await userProductDbService.getProductReviews(productId, limit, offset)
// Generate signed URLs for images
const reviewsWithSignedUrls = await Promise.all(
@ -193,12 +122,7 @@ export const productRouter = router({
);
// Check if more reviews exist
const totalCountResult = await db
.select({ count: sql`count(*)` })
.from(productReviews)
.where(eq(productReviews.productId, productId));
const totalCount = Number(totalCountResult[0].count);
const totalCount = await userProductDbService.getReviewCount(productId)
const hasMore = offset + limit < totalCount;
return { reviews: reviewsWithSignedUrls, hasMore };
@ -214,24 +138,25 @@ export const productRouter = router({
}))
.mutation(async ({ input, ctx }) => {
const { productId, reviewBody, ratings, imageUrls, uploadUrls } = input;
const userId = ctx.user.userId;
const userId = ctx.user?.userId;
if (!userId) {
throw new ApiError('User not authenticated', 401)
}
// Optional: Check if product exists
const product = await db.query.productInfo.findFirst({
where: eq(productInfo.id, productId),
});
const product = await userProductDbService.getProductById(productId)
if (!product) {
throw new ApiError('Product not found', 404);
}
// Insert review
const [newReview] = await db.insert(productReviews).values({
const newReview = await userProductDbService.createReview({
userId,
productId,
reviewBody,
ratings,
imageUrls: uploadUrls.map(item => extractKeyFromPresignedUrl(item)),
}).returning();
})
// Claim upload URLs
if (uploadUrls && uploadUrls.length > 0) {

View file

@ -1,15 +1,8 @@
import { router, publicProcedure } from "@/src/trpc/trpc-index";
import { z } from "zod";
import { db } from "@/src/db/db_index";
import {
deliverySlotInfo,
productSlots,
productInfo,
units,
} from "@/src/db/schema";
import { eq, and } from "drizzle-orm";
import { getAllSlots as getAllSlotsFromCache, getSlotById as getSlotByIdFromCache } from "@/src/stores/slot-store";
import dayjs from 'dayjs';
import { userSlotDbService } from '@/src/trpc/apis/user-apis/dataAccessors/main'
// Helper method to get formatted slot data by ID
async function getSlotData(slotId: number) {
@ -44,15 +37,7 @@ export async function scaffoldSlotsWithProducts() {
.sort((a, b) => dayjs(a.deliveryTime).valueOf() - dayjs(b.deliveryTime).valueOf());
// Fetch all products for availability info
const allProducts = await db
.select({
id: productInfo.id,
name: productInfo.name,
isOutOfStock: productInfo.isOutOfStock,
isFlashAvailable: productInfo.isFlashAvailable,
})
.from(productInfo)
.where(eq(productInfo.isSuspended, false));
const allProducts = await userSlotDbService.getProductAvailability()
const productAvailability = allProducts.map(product => ({
id: product.id,
@ -70,9 +55,7 @@ export async function scaffoldSlotsWithProducts() {
export const slotsRouter = router({
getSlots: publicProcedure.query(async () => {
const slots = await db.query.deliverySlotInfo.findMany({
where: eq(deliverySlotInfo.isActive, true),
});
const slots = await userSlotDbService.getActiveSlots()
return {
slots,
count: slots.length,

View file

@ -1,27 +1,12 @@
import { router, publicProcedure } from '@/src/trpc/trpc-index';
import { z } from 'zod';
import { db } from '@/src/db/db_index';
import { storeInfo, productInfo, units } from '@/src/db/schema';
import { eq, and, sql } from 'drizzle-orm';
import { scaffoldAssetUrl } from '@/src/lib/s3-client';
import { ApiError } from '@/src/lib/api-error';
import { getTagsByStoreId } from '@/src/stores/product-tag-store';
import { userStoreDbService } from '@/src/trpc/apis/user-apis/dataAccessors/main'
export async function scaffoldStores() {
const storesData = await db
.select({
id: storeInfo.id,
name: storeInfo.name,
description: storeInfo.description,
imageUrl: storeInfo.imageUrl,
productCount: sql<number>`count(${productInfo.id})`.as('productCount'),
})
.from(storeInfo)
.leftJoin(
productInfo,
and(eq(productInfo.storeId, storeInfo.id), eq(productInfo.isSuspended, false))
)
.groupBy(storeInfo.id);
const storesData = await userStoreDbService.getStoresWithProductCount()
// Generate signed URLs for store images and fetch sample products
const storesWithDetails = await Promise.all(
@ -29,15 +14,7 @@ export async function scaffoldStores() {
const signedImageUrl = store.imageUrl ? scaffoldAssetUrl(store.imageUrl) : null;
// Fetch up to 3 products for this store
const sampleProducts = await db
.select({
id: productInfo.id,
name: productInfo.name,
images: productInfo.images,
})
.from(productInfo)
.where(and(eq(productInfo.storeId, store.id), eq(productInfo.isSuspended, false)))
.limit(3);
const sampleProducts = await userStoreDbService.getSampleProductsByStoreId(store.id, 3)
// Generate signed URLs for product images
const productsWithSignedUrls = await Promise.all(
@ -69,15 +46,7 @@ export async function scaffoldStores() {
export async function scaffoldStoreWithProducts(storeId: number) {
// Fetch store info
const storeData = await db.query.storeInfo.findFirst({
where: eq(storeInfo.id, storeId),
columns: {
id: true,
name: true,
description: true,
imageUrl: true,
},
});
const storeData = await userStoreDbService.getStoreById(storeId)
if (!storeData) {
throw new ApiError('Store not found', 404);
@ -87,23 +56,7 @@ export async function scaffoldStoreWithProducts(storeId: number) {
const signedImageUrl = storeData.imageUrl ? scaffoldAssetUrl(storeData.imageUrl) : null;
// Fetch products for this store
const productsData = await db
.select({
id: productInfo.id,
name: productInfo.name,
shortDescription: productInfo.shortDescription,
price: productInfo.price,
marketPrice: productInfo.marketPrice,
images: productInfo.images,
isOutOfStock: productInfo.isOutOfStock,
incrementStep: productInfo.incrementStep,
unitShortNotation: units.shortNotation,
unitNotation: units.shortNotation,
productQuantity: productInfo.productQuantity,
})
.from(productInfo)
.innerJoin(units, eq(productInfo.unitId, units.id))
.where(and(eq(productInfo.storeId, storeId), eq(productInfo.isSuspended, false)));
const productsData = await userStoreDbService.getStoreProductsWithUnits(storeId)
// Generate signed URLs for product images

View file

@ -9,7 +9,6 @@ import { productRouter } from '@/src/trpc/apis/user-apis/apis/product';
import { slotsRouter } from '@/src/trpc/apis/user-apis/apis/slots';
import { userRouter as userDataRouter } from '@/src/trpc/apis/user-apis/apis/user';
import { userCouponRouter } from '@/src/trpc/apis/user-apis/apis/coupon';
import { paymentRouter } from '@/src/trpc/apis/user-apis/apis/payments';
import { storesRouter } from '@/src/trpc/apis/user-apis/apis/stores';
import { fileUploadRouter } from '@/src/trpc/apis/user-apis/apis/file-upload';
import { tagsRouter } from '@/src/trpc/apis/user-apis/apis/tags';
@ -25,7 +24,6 @@ export const userRouter = router({
slots: slotsRouter,
user: userDataRouter,
coupon: userCouponRouter,
payment: paymentRouter,
stores: storesRouter,
fileUpload: fileUploadRouter,
tags: tagsRouter,

View file

@ -1,11 +1,9 @@
import { router, protectedProcedure, publicProcedure } from '@/src/trpc/trpc-index';
import { eq, and } from 'drizzle-orm';
import { z } from 'zod';
import { db } from '@/src/db/db_index';
import { users, userDetails, userCreds, notifCreds, unloggedUserTokens } from '@/src/db/schema';
import { ApiError } from '@/src/lib/api-error';
import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client';
import { signToken } from '@/src/lib/jwt-utils';
import { userProfileDbService } from '@/src/trpc/apis/user-apis/dataAccessors/main'
interface AuthResponse {
token: string;
@ -29,28 +27,20 @@ const generateToken = async (userId: number): Promise<string> => {
export const userRouter = router({
getSelfData: protectedProcedure
.query(async ({ ctx }) => {
const userId = ctx.user.userId;
const userId = ctx.user?.userId;
if (!userId) {
throw new ApiError('User not authenticated', 401);
}
const [user] = await db
.select()
.from(users)
.where(eq(users.id, userId))
.limit(1);
const user = await userProfileDbService.getUserById(userId)
if (!user) {
throw new ApiError('User not found', 404);
}
// Get user details for profile image
const [userDetail] = await db
.select()
.from(userDetails)
.where(eq(userDetails.userId, userId))
.limit(1);
const userDetail = await userProfileDbService.getUserDetailByUserId(userId)
// Generate signed URL for profile image if it exists
const profileImageSignedUrl = userDetail?.profileImage
@ -79,24 +69,19 @@ export const userRouter = router({
checkProfileComplete: protectedProcedure
.query(async ({ ctx }) => {
const userId = ctx.user.userId;
const userId = ctx.user?.userId;
if (!userId) {
throw new ApiError('User not authenticated', 401);
}
const result = await db
.select()
.from(users)
.leftJoin(userCreds, eq(users.id, userCreds.userId))
.where(eq(users.id, userId))
.limit(1);
const result = await userProfileDbService.getUserWithCreds(userId)
if (result.length === 0) {
if (!result) {
throw new ApiError('User not found', 404);
}
const { users: user, user_creds: creds } = result[0];
const { user, creds } = result
return {
isComplete: !!(user.name && user.email && creds),
@ -112,50 +97,28 @@ export const userRouter = router({
if (userId) {
// AUTHENTICATED USER
// Check if token exists in notif_creds for this user
const existing = await db.query.notifCreds.findFirst({
where: and(
eq(notifCreds.userId, userId),
eq(notifCreds.token, token)
),
});
const existing = await userProfileDbService.getNotifCredByUserAndToken(userId, token)
if (existing) {
// Update lastVerified timestamp
await db
.update(notifCreds)
.set({ lastVerified: new Date() })
.where(eq(notifCreds.id, existing.id));
await userProfileDbService.updateNotifCredLastVerified(existing.id)
} else {
// Insert new token into notif_creds
await db.insert(notifCreds).values({
userId,
token,
lastVerified: new Date(),
});
await userProfileDbService.insertNotifCred(userId, token)
}
// Remove from unlogged_user_tokens if it exists
await db
.delete(unloggedUserTokens)
.where(eq(unloggedUserTokens.token, token));
await userProfileDbService.deleteUnloggedToken(token)
} else {
// UNAUTHENTICATED USER
// Save/update in unlogged_user_tokens
const existing = await db.query.unloggedUserTokens.findFirst({
where: eq(unloggedUserTokens.token, token),
});
const existing = await userProfileDbService.getUnloggedToken(token)
if (existing) {
await db
.update(unloggedUserTokens)
.set({ lastVerified: new Date() })
.where(eq(unloggedUserTokens.id, existing.id));
await userProfileDbService.updateUnloggedTokenLastVerified(existing.id)
} else {
await db.insert(unloggedUserTokens).values({
token,
lastVerified: new Date(),
});
await userProfileDbService.insertUnloggedToken(token)
}
}

View file

@ -0,0 +1,15 @@
import { addresses, orders, orderStatus, deliverySlotInfo } from '@/src/db/schema'
export type Address = typeof addresses.$inferSelect
export type NewAddress = typeof addresses.$inferInsert
export interface IUserAddressDbService {
getDefaultAddress(userId: number): Promise<Address | undefined>
getUserAddresses(userId: number): Promise<Address[]>
unsetDefaultForUser(userId: number): Promise<void>
createAddress(data: NewAddress): Promise<Address>
getAddressByIdForUser(addressId: number, userId: number): Promise<Address | undefined>
updateAddressForUser(addressId: number, userId: number, data: Partial<NewAddress>): Promise<Address>
deleteAddressForUser(addressId: number, userId: number): Promise<void>
hasOngoingOrdersForAddress(addressId: number): Promise<boolean>
}

View file

@ -0,0 +1,23 @@
import { users, userCreds, userDetails, addresses, cartItems, complaints, couponApplicableUsers, couponUsage, notifCreds, notifications, orderItems, orderStatus, orders, payments, refunds, productReviews, reservedCoupons } from '@/src/db/schema'
export type User = typeof users.$inferSelect
export type UserCred = typeof userCreds.$inferSelect
export type UserDetail = typeof userDetails.$inferSelect
export interface IUserAuthDbService {
getUserByEmail(email: string): Promise<User | undefined>
getUserByMobile(mobile: string): Promise<User | undefined>
getUserById(userId: number): Promise<User | undefined>
getUserCredsByUserId(userId: number): Promise<UserCred | undefined>
getUserDetailsByUserId(userId: number): Promise<UserDetail | undefined>
createUserWithCredsAndDetails(data: { name: string | null; email: string | null; mobile: string; passwordHash: string; imageKey?: string | null }): Promise<User>
createUser(data: { name: string | null; email: string | null; mobile: string }): Promise<User>
upsertUserCreds(userId: number, passwordHash: string): Promise<void>
updateUserName(userId: number, name: string): Promise<void>
updateUserEmail(userId: number, email: string): Promise<void>
upsertUserDetails(userId: number, data: Partial<UserDetail>): Promise<void>
deleteAccountByUserId(userId: number): Promise<void>
}

View file

@ -0,0 +1,7 @@
import { homeBanners } from '@/src/db/schema'
export type UserBanner = typeof homeBanners.$inferSelect
export interface IUserBannerDbService {
getActiveBanners(): Promise<UserBanner[]>
}

View file

@ -0,0 +1,25 @@
import { cartItems, productInfo, units } from '@/src/db/schema'
export type CartItem = typeof cartItems.$inferSelect
export interface IUserCartDbService {
getCartItemsWithProducts(userId: number): Promise<Array<{
cartId: number
productId: number
productName: string
productPrice: string
productImages: unknown
productQuantity: number
isOutOfStock: boolean
unitShortNotation: string | null
quantity: string
addedAt: Date
}>>
getProductById(productId: number): Promise<typeof productInfo.$inferSelect | undefined>
getCartItemByUserAndProduct(userId: number, productId: number): Promise<CartItem | undefined>
incrementCartItemQuantity(cartItemId: number, quantity: number): Promise<void>
createCartItem(userId: number, productId: number, quantity: number): Promise<void>
updateCartItemQuantity(itemId: number, userId: number, quantity: number): Promise<CartItem | undefined>
deleteCartItem(itemId: number, userId: number): Promise<CartItem | undefined>
clearCart(userId: number): Promise<void>
}

View file

@ -0,0 +1,17 @@
import { complaints } from '@/src/db/schema'
export type Complaint = typeof complaints.$inferSelect
export type NewComplaint = typeof complaints.$inferInsert
export interface IUserComplaintDbService {
getComplaintsByUserId(userId: number): Promise<Array<{
id: number
complaintBody: string
response: string | null
isResolved: boolean
createdAt: Date
orderId: number | null
images: unknown
}>>
createComplaint(data: NewComplaint): Promise<void>
}

View file

@ -0,0 +1,20 @@
import { coupons, couponUsage, couponApplicableUsers, couponApplicableProducts, reservedCoupons } from '@/src/db/schema'
export type Coupon = typeof coupons.$inferSelect
export type CouponUsage = typeof couponUsage.$inferSelect
export type CouponApplicableUser = typeof couponApplicableUsers.$inferSelect
export type CouponApplicableProduct = typeof couponApplicableProducts.$inferSelect
export type ReservedCoupon = typeof reservedCoupons.$inferSelect
export type CouponWithRelations = Coupon & {
usages: CouponUsage[]
applicableUsers: Array<CouponApplicableUser & { user: any }>
applicableProducts: Array<CouponApplicableProduct & { product: any }>
}
export interface IUserCouponDbService {
getActiveCouponsForUser(userId: number): Promise<CouponWithRelations[]>
getAllCouponsForUser(userId: number): Promise<CouponWithRelations[]>
getReservedCouponBySecretCode(secretCode: string): Promise<ReservedCoupon | undefined>
redeemReservedCoupon(userId: number, reservedCoupon: ReservedCoupon): Promise<Coupon>
}

View file

@ -0,0 +1,95 @@
import {
orders,
orderItems,
orderStatus,
addresses,
productInfo,
paymentInfoTable,
coupons,
couponUsage,
refunds,
units,
userDetails,
deliverySlotInfo,
} from '@/src/db/schema'
export type Order = typeof orders.$inferSelect
export type OrderInsert = typeof orders.$inferInsert
export type OrderItem = typeof orderItems.$inferSelect
export type OrderItemInsert = typeof orderItems.$inferInsert
export type OrderStatus = typeof orderStatus.$inferSelect
export type OrderStatusInsert = typeof orderStatus.$inferInsert
export type Address = typeof addresses.$inferSelect
export type Product = typeof productInfo.$inferSelect
export type PaymentInfo = typeof paymentInfoTable.$inferSelect
export type Coupon = typeof coupons.$inferSelect
export type CouponUsage = typeof couponUsage.$inferSelect
export type Refund = typeof refunds.$inferSelect
export type Unit = typeof units.$inferSelect
export type UserDetail = typeof userDetails.$inferSelect
export type Slot = typeof deliverySlotInfo.$inferSelect
export type CouponWithUsages = Coupon & {
usages: CouponUsage[]
}
export type OrderWithRelations = Order & {
orderItems: Array<OrderItem & { product: Product }>
slot: Slot | null
paymentInfo: PaymentInfo | null
orderStatus: OrderStatus[]
refunds: Refund[]
}
export type OrderWithDetails = Order & {
orderItems: Array<OrderItem & { product: Product }>
slot: Slot | null
paymentInfo: PaymentInfo | null
orderStatus: Array<OrderStatus & { refundCoupon: Coupon | null }>
refunds: Refund[]
}
export interface IUserOrderDbService {
getUserDetailByUserId(userId: number): Promise<UserDetail | undefined>
getAddressByUserId(userId: number, addressId: number): Promise<Address | undefined>
getProductById(productId: number): Promise<Product | undefined>
getCouponWithUsage(couponId: number, userId: number): Promise<CouponWithUsages | undefined>
createOrdersWithItems(params: {
ordersData: Array<{
order: Omit<OrderInsert, 'id'>
orderItems: Omit<OrderItemInsert, 'id'>[]
orderStatus: Omit<OrderStatusInsert, 'id'>
}>
paymentMethod: 'online' | 'cod'
}): Promise<Order[]>
deleteCartItemsByUserAndProductIds(userId: number, productIds: number[]): Promise<void>
createCouponUsage(params: {
userId: number
couponId: number
orderId: number
}): Promise<void>
getOrdersCount(userId: number): Promise<number>
getOrdersWithRelations(userId: number, limit: number, offset: number): Promise<OrderWithRelations[]>
getOrderWithDetailsById(orderId: number, userId: number): Promise<OrderWithDetails | undefined>
getCouponUsagesByOrderId(orderId: number): Promise<Array<CouponUsage & { coupon: Coupon }>>
getOrderWithStatus(orderId: number): Promise<(Order & { orderStatus: OrderStatus[] }) | undefined>
cancelOrderTransaction(params: {
statusId: number
reason: string
orderId: number
refundStatus: string
}): Promise<void>
updateOrderNotes(orderId: number, userNotes: string | null): Promise<void>
getRecentDeliveredOrderIds(userId: number, since: Date, limit: number): Promise<Array<{ id: number }>>
getProductIdsByOrderIds(orderIds: number[]): Promise<Array<{ productId: number }>>
getProductsWithUnitsByIds(productIds: number[], limit: number): Promise<Array<{
id: number
name: string
shortDescription: string | null
price: string
images: unknown
isOutOfStock: boolean
unitShortNotation: string | null
incrementStep: number | null
}>>
}

View file

@ -0,0 +1,32 @@
import { productInfo, units, storeInfo, productSlots, deliverySlotInfo, specialDeals, productReviews, users } from '@/src/db/schema'
export type Product = typeof productInfo.$inferSelect
export type Store = typeof storeInfo.$inferSelect
export type Review = typeof productReviews.$inferSelect
export type ProductWithUnit = {
id: number
name: string
shortDescription: string | null
longDescription: string | null
price: string
marketPrice: string | null
images: unknown
isOutOfStock: boolean
storeId: number | null
unitShortNotation: string | null
incrementStep: number
productQuantity: number
isFlashAvailable: boolean
flashPrice: string | null
}
export interface IUserProductDbService {
getProductById(productId: number): Promise<ProductWithUnit | undefined>
getStoreBasicById(storeId: number): Promise<{ id: number; name: string; description: string | null } | undefined>
getDeliverySlotsForProduct(productId: number): Promise<Array<{ id: number; deliveryTime: Date; freezeTime: Date }>>
getSpecialDealsForProduct(productId: number): Promise<Array<{ quantity: string; price: string; validTill: Date }>>
getProductReviews(productId: number, limit: number, offset: number): Promise<Array<{ id: number; reviewBody: string; ratings: number; imageUrls: unknown; reviewTime: Date; userName: string | null }>>
getReviewCount(productId: number): Promise<number>
createReview(data: { userId: number; productId: number; reviewBody: string; ratings: number; imageUrls: string[] }): Promise<Review>
}

View file

@ -0,0 +1,22 @@
import { users, userDetails, userCreds, notifCreds, unloggedUserTokens } from '@/src/db/schema'
export type User = typeof users.$inferSelect
export type UserDetail = typeof userDetails.$inferSelect
export type UserCred = typeof userCreds.$inferSelect
export type NotifCred = typeof notifCreds.$inferSelect
export type UnloggedToken = typeof unloggedUserTokens.$inferSelect
export interface IUserProfileDbService {
getUserById(userId: number): Promise<User | undefined>
getUserDetailByUserId(userId: number): Promise<UserDetail | undefined>
getUserWithCreds(userId: number): Promise<{ user: User; creds: UserCred | null } | undefined>
getNotifCredByUserAndToken(userId: number, token: string): Promise<NotifCred | undefined>
updateNotifCredLastVerified(id: number): Promise<void>
insertNotifCred(userId: number, token: string): Promise<void>
deleteUnloggedToken(token: string): Promise<void>
getUnloggedToken(token: string): Promise<UnloggedToken | undefined>
updateUnloggedTokenLastVerified(id: number): Promise<void>
insertUnloggedToken(token: string): Promise<void>
}

View file

@ -0,0 +1,8 @@
import { deliverySlotInfo, productInfo } from '@/src/db/schema'
export type Slot = typeof deliverySlotInfo.$inferSelect
export interface IUserSlotDbService {
getActiveSlots(): Promise<Slot[]>
getProductAvailability(): Promise<Array<{ id: number; name: string; isOutOfStock: boolean; isFlashAvailable: boolean }>>
}

View file

@ -0,0 +1,28 @@
import { storeInfo } from '@/src/db/schema'
export type Store = typeof storeInfo.$inferSelect
export type StoreBasic = {
id: number
name: string
description: string | null
imageUrl: string | null
}
export interface IUserStoreDbService {
getStoresWithProductCount(): Promise<Array<{ id: number; name: string; description: string | null; imageUrl: string | null; productCount: number }>>
getStoreById(storeId: number): Promise<StoreBasic | undefined>
getSampleProductsByStoreId(storeId: number, limit: number): Promise<Array<{ id: number; name: string; images: unknown }>>
getStoreProductsWithUnits(storeId: number): Promise<Array<{
id: number
name: string
shortDescription: string | null
price: string
marketPrice: string | null
images: unknown
isOutOfStock: boolean
incrementStep: number
unitShortNotation: string | null
unitNotation: string | null
productQuantity: number
}>>
}

View file

@ -0,0 +1,32 @@
export type { IUserBannerDbService, UserBanner } from '@/src/trpc/apis/user-apis/dataAccessors/interfaces/user-banner-db-service.interface'
export { userBannerDbService } from '@/src/trpc/apis/user-apis/dataAccessors/postgres/user-banner-queries'
export type { IUserStoreDbService, Store as UserStore, StoreBasic } from '@/src/trpc/apis/user-apis/dataAccessors/interfaces/user-store-db-service.interface'
export { userStoreDbService } from '@/src/trpc/apis/user-apis/dataAccessors/postgres/user-store-queries'
export type { IUserAddressDbService, Address, NewAddress } from '@/src/trpc/apis/user-apis/dataAccessors/interfaces/user-address-db-service.interface'
export { userAddressDbService } from '@/src/trpc/apis/user-apis/dataAccessors/postgres/user-address-queries'
export type { IUserCartDbService, CartItem } from '@/src/trpc/apis/user-apis/dataAccessors/interfaces/user-cart-db-service.interface'
export { userCartDbService } from '@/src/trpc/apis/user-apis/dataAccessors/postgres/user-cart-queries'
export type { IUserComplaintDbService, Complaint, NewComplaint } from '@/src/trpc/apis/user-apis/dataAccessors/interfaces/user-complaint-db-service.interface'
export { userComplaintDbService } from '@/src/trpc/apis/user-apis/dataAccessors/postgres/user-complaint-queries'
export type { IUserProductDbService, Product, Store as ProductStore, Review, ProductWithUnit } from '@/src/trpc/apis/user-apis/dataAccessors/interfaces/user-product-db-service.interface'
export { userProductDbService } from '@/src/trpc/apis/user-apis/dataAccessors/postgres/user-product-queries'
export type { IUserAuthDbService, User, UserCred, UserDetail } from '@/src/trpc/apis/user-apis/dataAccessors/interfaces/user-auth-db-service.interface'
export { userAuthDbService } from '@/src/trpc/apis/user-apis/dataAccessors/postgres/user-auth-queries'
export type { IUserProfileDbService, User as ProfileUser, UserDetail as ProfileUserDetail, UserCred as ProfileUserCred, NotifCred, UnloggedToken } from '@/src/trpc/apis/user-apis/dataAccessors/interfaces/user-profile-db-service.interface'
export { userProfileDbService } from '@/src/trpc/apis/user-apis/dataAccessors/postgres/user-profile-queries'
export type { IUserSlotDbService, Slot } from '@/src/trpc/apis/user-apis/dataAccessors/interfaces/user-slot-db-service.interface'
export { userSlotDbService } from '@/src/trpc/apis/user-apis/dataAccessors/postgres/user-slot-queries'
export type { IUserCouponDbService, Coupon, CouponWithRelations, ReservedCoupon } from '@/src/trpc/apis/user-apis/dataAccessors/interfaces/user-coupon-db-service.interface'
export { userCouponDbService } from '@/src/trpc/apis/user-apis/dataAccessors/postgres/user-coupon-queries'
export type { IUserOrderDbService, Order, OrderInsert, OrderItemInsert, OrderStatusInsert, Coupon as OrderCoupon } from '@/src/trpc/apis/user-apis/dataAccessors/interfaces/user-order-db-service.interface'
export { userOrderDbService } from '@/src/trpc/apis/user-apis/dataAccessors/postgres/user-order-queries'

View file

@ -0,0 +1,71 @@
import { db } from '@/src/db/db_index'
import { addresses, orders, orderStatus, deliverySlotInfo } from '@/src/db/schema'
import { eq, and, gte } from 'drizzle-orm'
import { IUserAddressDbService, Address, NewAddress } from '@/src/trpc/apis/user-apis/dataAccessors/interfaces/user-address-db-service.interface'
export class UserAddressDbService implements IUserAddressDbService {
async getDefaultAddress(userId: number): Promise<Address | undefined> {
const [defaultAddress] = await db
.select()
.from(addresses)
.where(and(eq(addresses.userId, userId), eq(addresses.isDefault, true)))
.limit(1)
return defaultAddress
}
async getUserAddresses(userId: number): Promise<Address[]> {
return db.select().from(addresses).where(eq(addresses.userId, userId))
}
async unsetDefaultForUser(userId: number): Promise<void> {
await db.update(addresses).set({ isDefault: false }).where(eq(addresses.userId, userId))
}
async createAddress(data: NewAddress): Promise<Address> {
const [newAddress] = await db.insert(addresses).values(data).returning()
return newAddress
}
async getAddressByIdForUser(addressId: number, userId: number): Promise<Address | undefined> {
const [address] = await db
.select()
.from(addresses)
.where(and(eq(addresses.id, addressId), eq(addresses.userId, userId)))
.limit(1)
return address
}
async updateAddressForUser(addressId: number, userId: number, data: Partial<NewAddress>): Promise<Address> {
const [updated] = await db
.update(addresses)
.set(data)
.where(and(eq(addresses.id, addressId), eq(addresses.userId, userId)))
.returning()
return updated
}
async deleteAddressForUser(addressId: number, userId: number): Promise<void> {
await db.delete(addresses).where(and(eq(addresses.id, addressId), eq(addresses.userId, userId)))
}
async hasOngoingOrdersForAddress(addressId: number): Promise<boolean> {
const ongoingOrders = await db
.select({
orderId: orders.id,
})
.from(orders)
.innerJoin(orderStatus, eq(orders.id, orderStatus.orderId))
.innerJoin(deliverySlotInfo, eq(orders.slotId, deliverySlotInfo.id))
.where(
and(
eq(orders.addressId, addressId),
eq(orderStatus.isCancelled, false),
gte(deliverySlotInfo.deliveryTime, new Date())
)
)
.limit(1)
return ongoingOrders.length > 0
}
}
export const userAddressDbService: IUserAddressDbService = new UserAddressDbService()

View file

@ -0,0 +1,122 @@
import { db } from '@/src/db/db_index'
import { users, userCreds, userDetails, addresses, cartItems, complaints, couponApplicableUsers, couponUsage, notifCreds, notifications, orderItems, orderStatus, orders, payments, refunds, productReviews, reservedCoupons } from '@/src/db/schema'
import { eq } from 'drizzle-orm'
import { IUserAuthDbService, User, UserCred, UserDetail } from '@/src/trpc/apis/user-apis/dataAccessors/interfaces/user-auth-db-service.interface'
export class UserAuthDbService implements IUserAuthDbService {
async getUserByEmail(email: string): Promise<User | undefined> {
const [user] = await db.select().from(users).where(eq(users.email, email)).limit(1)
return user
}
async getUserByMobile(mobile: string): Promise<User | undefined> {
const [user] = await db.select().from(users).where(eq(users.mobile, mobile)).limit(1)
return user
}
async getUserById(userId: number): Promise<User | undefined> {
const [user] = await db.select().from(users).where(eq(users.id, userId)).limit(1)
return user
}
async getUserCredsByUserId(userId: number): Promise<UserCred | undefined> {
const [creds] = await db.select().from(userCreds).where(eq(userCreds.userId, userId)).limit(1)
return creds
}
async getUserDetailsByUserId(userId: number): Promise<UserDetail | undefined> {
const [detail] = await db.select().from(userDetails).where(eq(userDetails.userId, userId)).limit(1)
return detail
}
async createUserWithCredsAndDetails(data: { name: string | null; email: string | null; mobile: string; passwordHash: string; imageKey?: string | null }): Promise<User> {
const { name, email, mobile, passwordHash, imageKey } = data
return db.transaction(async (tx) => {
const [user] = await tx
.insert(users)
.values({ name, email, mobile })
.returning()
await tx
.insert(userCreds)
.values({ userId: user.id, userPassword: passwordHash })
if (imageKey) {
await tx.insert(userDetails).values({ userId: user.id, profileImage: imageKey })
}
return user
})
}
async createUser(data: { name: string | null; email: string | null; mobile: string }): Promise<User> {
const [user] = await db.insert(users).values(data).returning()
return user
}
async upsertUserCreds(userId: number, passwordHash: string): Promise<void> {
await db
.insert(userCreds)
.values({ userId, userPassword: passwordHash })
.onConflictDoUpdate({
target: userCreds.userId,
set: { userPassword: passwordHash },
})
}
async updateUserName(userId: number, name: string): Promise<void> {
await db.update(users).set({ name }).where(eq(users.id, userId))
}
async updateUserEmail(userId: number, email: string): Promise<void> {
await db.update(users).set({ email }).where(eq(users.id, userId))
}
async upsertUserDetails(userId: number, data: Partial<UserDetail>): Promise<void> {
await db
.insert(userDetails)
.values({ userId, ...data })
.onConflictDoUpdate({
target: userDetails.userId,
set: data,
})
}
async deleteAccountByUserId(userId: number): Promise<void> {
await db.transaction(async (tx) => {
await tx.delete(notifCreds).where(eq(notifCreds.userId, userId))
await tx.delete(couponApplicableUsers).where(eq(couponApplicableUsers.userId, userId))
await tx.delete(couponUsage).where(eq(couponUsage.userId, userId))
await tx.delete(complaints).where(eq(complaints.userId, userId))
await tx.delete(cartItems).where(eq(cartItems.userId, userId))
await tx.delete(notifications).where(eq(notifications.userId, userId))
await tx.delete(productReviews).where(eq(productReviews.userId, userId))
await tx.update(reservedCoupons)
.set({ redeemedBy: null })
.where(eq(reservedCoupons.redeemedBy, userId))
const userOrders = await tx
.select({ id: orders.id })
.from(orders)
.where(eq(orders.userId, userId))
for (const order of userOrders) {
await tx.delete(orderItems).where(eq(orderItems.orderId, order.id))
await tx.delete(orderStatus).where(eq(orderStatus.orderId, order.id))
await tx.delete(payments).where(eq(payments.orderId, order.id))
await tx.delete(refunds).where(eq(refunds.orderId, order.id))
await tx.delete(couponUsage).where(eq(couponUsage.orderId, order.id))
await tx.delete(complaints).where(eq(complaints.orderId, order.id))
}
await tx.delete(orders).where(eq(orders.userId, userId))
await tx.delete(addresses).where(eq(addresses.userId, userId))
await tx.delete(userDetails).where(eq(userDetails.userId, userId))
await tx.delete(userCreds).where(eq(userCreds.userId, userId))
await tx.delete(users).where(eq(users.id, userId))
})
}
}
export const userAuthDbService: IUserAuthDbService = new UserAuthDbService()

View file

@ -0,0 +1,15 @@
import { db } from '@/src/db/db_index'
import { homeBanners } from '@/src/db/schema'
import { isNotNull, asc } from 'drizzle-orm'
import { IUserBannerDbService, UserBanner } from '@/src/trpc/apis/user-apis/dataAccessors/interfaces/user-banner-db-service.interface'
export class UserBannerDbService implements IUserBannerDbService {
async getActiveBanners(): Promise<UserBanner[]> {
return db.query.homeBanners.findMany({
where: isNotNull(homeBanners.serialNum),
orderBy: asc(homeBanners.serialNum),
})
}
}
export const userBannerDbService: IUserBannerDbService = new UserBannerDbService()

View file

@ -0,0 +1,75 @@
import { db } from '@/src/db/db_index'
import { cartItems, productInfo, units } from '@/src/db/schema'
import { eq, and, sql } from 'drizzle-orm'
import { IUserCartDbService, CartItem } from '@/src/trpc/apis/user-apis/dataAccessors/interfaces/user-cart-db-service.interface'
export class UserCartDbService implements IUserCartDbService {
async getCartItemsWithProducts(userId: number) {
return db
.select({
cartId: cartItems.id,
productId: productInfo.id,
productName: productInfo.name,
productPrice: productInfo.price,
productImages: productInfo.images,
productQuantity: productInfo.productQuantity,
isOutOfStock: productInfo.isOutOfStock,
unitShortNotation: units.shortNotation,
quantity: cartItems.quantity,
addedAt: cartItems.addedAt,
})
.from(cartItems)
.innerJoin(productInfo, eq(cartItems.productId, productInfo.id))
.innerJoin(units, eq(productInfo.unitId, units.id))
.where(eq(cartItems.userId, userId))
}
async getProductById(productId: number) {
return db.query.productInfo.findFirst({
where: eq(productInfo.id, productId),
})
}
async getCartItemByUserAndProduct(userId: number, productId: number): Promise<CartItem | undefined> {
return db.query.cartItems.findFirst({
where: and(eq(cartItems.userId, userId), eq(cartItems.productId, productId)),
})
}
async incrementCartItemQuantity(cartItemId: number, quantity: number): Promise<void> {
await db.update(cartItems)
.set({
quantity: sql`${cartItems.quantity} + ${quantity}`,
})
.where(eq(cartItems.id, cartItemId))
}
async createCartItem(userId: number, productId: number, quantity: number): Promise<void> {
await db.insert(cartItems).values({
userId,
productId,
quantity: quantity.toString(),
})
}
async updateCartItemQuantity(itemId: number, userId: number, quantity: number): Promise<CartItem | undefined> {
const [updatedItem] = await db.update(cartItems)
.set({ quantity: quantity.toString() })
.where(and(eq(cartItems.id, itemId), eq(cartItems.userId, userId)))
.returning()
return updatedItem
}
async deleteCartItem(itemId: number, userId: number): Promise<CartItem | undefined> {
const [deletedItem] = await db.delete(cartItems)
.where(and(eq(cartItems.id, itemId), eq(cartItems.userId, userId)))
.returning()
return deletedItem
}
async clearCart(userId: number): Promise<void> {
await db.delete(cartItems).where(eq(cartItems.userId, userId))
}
}
export const userCartDbService: IUserCartDbService = new UserCartDbService()

View file

@ -0,0 +1,28 @@
import { db } from '@/src/db/db_index'
import { complaints } from '@/src/db/schema'
import { eq, asc } from 'drizzle-orm'
import { IUserComplaintDbService } from '@/src/trpc/apis/user-apis/dataAccessors/interfaces/user-complaint-db-service.interface'
export class UserComplaintDbService implements IUserComplaintDbService {
async getComplaintsByUserId(userId: number) {
return db
.select({
id: complaints.id,
complaintBody: complaints.complaintBody,
response: complaints.response,
isResolved: complaints.isResolved,
createdAt: complaints.createdAt,
orderId: complaints.orderId,
images: complaints.images,
})
.from(complaints)
.where(eq(complaints.userId, userId))
.orderBy(asc(complaints.createdAt))
}
async createComplaint(data: { userId: number; orderId?: number | null; complaintBody: string; images: string[] }) {
await db.insert(complaints).values(data)
}
}
export const userComplaintDbService: IUserComplaintDbService = new UserComplaintDbService()

View file

@ -0,0 +1,88 @@
import { db } from '@/src/db/db_index'
import { coupons, couponUsage, couponApplicableUsers, couponApplicableProducts, reservedCoupons } from '@/src/db/schema'
import { eq, and, or, gt, isNull } from 'drizzle-orm'
import { IUserCouponDbService, Coupon, ReservedCoupon, CouponWithRelations } from '@/src/trpc/apis/user-apis/dataAccessors/interfaces/user-coupon-db-service.interface'
export class UserCouponDbService implements IUserCouponDbService {
async getActiveCouponsForUser(userId: number): Promise<CouponWithRelations[]> {
return db.query.coupons.findMany({
where: and(
eq(coupons.isInvalidated, false),
or(
isNull(coupons.validTill),
gt(coupons.validTill, new Date())
)
),
with: {
usages: {
where: eq(couponUsage.userId, userId),
},
applicableUsers: {
with: { user: true },
},
applicableProducts: {
with: { product: true },
},
},
}) as Promise<CouponWithRelations[]>
}
async getAllCouponsForUser(userId: number): Promise<CouponWithRelations[]> {
return db.query.coupons.findMany({
with: {
usages: {
where: eq(couponUsage.userId, userId),
},
applicableUsers: {
with: { user: true },
},
applicableProducts: {
with: { product: true },
},
},
}) as Promise<CouponWithRelations[]>
}
async getReservedCouponBySecretCode(secretCode: string): Promise<ReservedCoupon | undefined> {
return db.query.reservedCoupons.findFirst({
where: and(
eq(reservedCoupons.secretCode, secretCode.toUpperCase()),
eq(reservedCoupons.isRedeemed, false)
),
})
}
async redeemReservedCoupon(userId: number, reservedCoupon: ReservedCoupon): Promise<Coupon> {
return db.transaction(async (tx) => {
const [coupon] = await tx.insert(coupons).values({
couponCode: reservedCoupon.couponCode,
isUserBased: true,
discountPercent: reservedCoupon.discountPercent,
flatDiscount: reservedCoupon.flatDiscount,
minOrder: reservedCoupon.minOrder,
productIds: reservedCoupon.productIds,
maxValue: reservedCoupon.maxValue,
isApplyForAll: false,
validTill: reservedCoupon.validTill,
maxLimitForUser: reservedCoupon.maxLimitForUser,
exclusiveApply: reservedCoupon.exclusiveApply,
createdBy: reservedCoupon.createdBy,
}).returning()
await tx.insert(couponApplicableUsers).values({
couponId: coupon.id,
userId,
})
await tx.update(reservedCoupons).set({
isRedeemed: true,
redeemedBy: userId,
redeemedAt: new Date(),
}).where(eq(reservedCoupons.id, reservedCoupon.id))
return coupon
})
}
}
export const userCouponDbService: IUserCouponDbService = new UserCouponDbService()

View file

@ -0,0 +1,265 @@
import { db } from '@/src/db/db_index'
import {
orders,
orderItems,
orderStatus,
addresses,
productInfo,
paymentInfoTable,
coupons,
couponUsage,
cartItems,
refunds,
units,
userDetails,
} from '@/src/db/schema'
import { and, desc, eq, gte, inArray } from 'drizzle-orm'
import {
IUserOrderDbService,
Order,
} from '@/src/trpc/apis/user-apis/dataAccessors/interfaces/user-order-db-service.interface'
export class UserOrderDbService implements IUserOrderDbService {
async getUserDetailByUserId(userId: number) {
return db.query.userDetails.findFirst({
where: eq(userDetails.userId, userId),
})
}
async getAddressByUserId(userId: number, addressId: number) {
return db.query.addresses.findFirst({
where: and(eq(addresses.userId, userId), eq(addresses.id, addressId)),
})
}
async getProductById(productId: number) {
return db.query.productInfo.findFirst({
where: eq(productInfo.id, productId),
})
}
async getCouponWithUsage(couponId: number, userId: number) {
return db.query.coupons.findFirst({
where: eq(coupons.id, couponId),
with: {
usages: { where: eq(couponUsage.userId, userId) },
},
})
}
async createOrdersWithItems(params: {
ordersData: Array<{
order: Omit<typeof orders.$inferInsert, 'id'>
orderItems: Omit<typeof orderItems.$inferInsert, 'id'>[]
orderStatus: Omit<typeof orderStatus.$inferInsert, 'id'>
}>
paymentMethod: 'online' | 'cod'
}): Promise<Order[]> {
const { ordersData, paymentMethod } = params
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, index) => {
const od = ordersData[index]
od.orderItems.forEach((item) => {
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)
return insertedOrders
})
}
async deleteCartItemsByUserAndProductIds(userId: number, productIds: number[]) {
await db.delete(cartItems).where(
and(eq(cartItems.userId, userId), inArray(cartItems.productId, productIds))
)
}
async createCouponUsage(params: { userId: number; couponId: number; orderId: number }) {
const { userId, couponId, orderId } = params
await db.insert(couponUsage).values({
userId,
couponId,
orderId,
orderItemId: null,
usedAt: new Date(),
})
}
async getOrdersCount(userId: number) {
return db.$count(orders, eq(orders.userId, userId))
}
async getOrdersWithRelations(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: (ordersRef, { desc }) => [desc(ordersRef.createdAt)],
limit,
offset,
})
}
async getOrderWithDetailsById(orderId: number, userId: number) {
return db.query.orders.findFirst({
where: and(eq(orders.id, orderId), eq(orders.userId, userId)),
with: {
orderItems: {
with: {
product: true,
},
},
slot: true,
paymentInfo: true,
orderStatus: {
with: {
refundCoupon: true,
},
},
refunds: true,
},
})
}
async getCouponUsagesByOrderId(orderId: number) {
return db.query.couponUsage.findMany({
where: eq(couponUsage.orderId, orderId),
with: {
coupon: true,
},
})
}
async getOrderWithStatus(orderId: number) {
return db.query.orders.findFirst({
where: eq(orders.id, orderId),
with: {
orderStatus: true,
},
})
}
async cancelOrderTransaction(params: {
statusId: number
reason: string
orderId: number
refundStatus: string
}) {
const { statusId, reason, orderId, refundStatus } = params
await db.transaction(async (tx) => {
await tx
.update(orderStatus)
.set({
isCancelled: true,
cancelReason: reason,
cancellationUserNotes: reason,
cancellationReviewed: false,
})
.where(eq(orderStatus.id, statusId))
await tx.insert(refunds).values({
orderId,
refundStatus,
})
})
}
async updateOrderNotes(orderId: number, userNotes: string | null) {
await db
.update(orders)
.set({
userNotes,
})
.where(eq(orders.id, orderId))
}
async getRecentDeliveredOrderIds(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)
}
async getProductIdsByOrderIds(orderIds: number[]) {
return db
.select({ productId: orderItems.productId })
.from(orderItems)
.where(inArray(orderItems.orderId, orderIds))
}
async getProductsWithUnitsByIds(productIds: number[], limit: 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))
.limit(limit)
}
}
export const userOrderDbService: IUserOrderDbService = new UserOrderDbService()

View file

@ -0,0 +1,109 @@
import { db } from '@/src/db/db_index'
import { productInfo, units, storeInfo, productSlots, deliverySlotInfo, specialDeals, productReviews, users } from '@/src/db/schema'
import { eq, and, gt, sql, desc } from 'drizzle-orm'
import { IUserProductDbService, Review } from '@/src/trpc/apis/user-apis/dataAccessors/interfaces/user-product-db-service.interface'
export class UserProductDbService implements IUserProductDbService {
async getProductById(productId: number) {
const result = await 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)
return result[0]
}
async getStoreBasicById(storeId: number) {
return db.query.storeInfo.findFirst({
where: eq(storeInfo.id, storeId),
columns: { id: true, name: true, description: true },
})
}
async getDeliverySlotsForProduct(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),
eq(deliverySlotInfo.isCapacityFull, false),
gt(deliverySlotInfo.deliveryTime, sql`NOW()`),
gt(deliverySlotInfo.freezeTime, sql`NOW()`)
)
)
.orderBy(deliverySlotInfo.deliveryTime)
}
async getSpecialDealsForProduct(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)
}
async 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)
}
async getReviewCount(productId: number): Promise<number> {
const result = await db
.select({ count: sql`count(*)` })
.from(productReviews)
.where(eq(productReviews.productId, productId))
return Number(result[0].count)
}
async createReview(data: { userId: number; productId: number; reviewBody: string; ratings: number; imageUrls: string[] }): Promise<Review> {
const [newReview] = await db.insert(productReviews).values(data).returning()
return newReview
}
}
export const userProductDbService: IUserProductDbService = new UserProductDbService()

View file

@ -0,0 +1,74 @@
import { db } from '@/src/db/db_index'
import { users, userDetails, userCreds, notifCreds, unloggedUserTokens } from '@/src/db/schema'
import { eq, and } from 'drizzle-orm'
import { IUserProfileDbService, User, UserDetail, UserCred, NotifCred, UnloggedToken } from '@/src/trpc/apis/user-apis/dataAccessors/interfaces/user-profile-db-service.interface'
export class UserProfileDbService implements IUserProfileDbService {
async getUserById(userId: number): Promise<User | undefined> {
const [user] = await db.select().from(users).where(eq(users.id, userId)).limit(1)
return user
}
async getUserDetailByUserId(userId: number): Promise<UserDetail | undefined> {
const [detail] = await db.select().from(userDetails).where(eq(userDetails.userId, userId)).limit(1)
return detail
}
async getUserWithCreds(userId: number): Promise<{ user: User; creds: UserCred | null } | undefined> {
const result = await db
.select()
.from(users)
.leftJoin(userCreds, eq(users.id, userCreds.userId))
.where(eq(users.id, userId))
.limit(1)
if (result.length === 0) return undefined
const row = result[0] as any
return {
user: row.users,
creds: row.user_creds || null,
}
}
async getNotifCredByUserAndToken(userId: number, token: string): Promise<NotifCred | undefined> {
return db.query.notifCreds.findFirst({
where: and(eq(notifCreds.userId, userId), eq(notifCreds.token, token)),
})
}
async updateNotifCredLastVerified(id: number): Promise<void> {
await db.update(notifCreds).set({ lastVerified: new Date() }).where(eq(notifCreds.id, id))
}
async insertNotifCred(userId: number, token: string): Promise<void> {
await db.insert(notifCreds).values({
userId,
token,
lastVerified: new Date(),
})
}
async deleteUnloggedToken(token: string): Promise<void> {
await db.delete(unloggedUserTokens).where(eq(unloggedUserTokens.token, token))
}
async getUnloggedToken(token: string): Promise<UnloggedToken | undefined> {
return db.query.unloggedUserTokens.findFirst({
where: eq(unloggedUserTokens.token, token),
})
}
async updateUnloggedTokenLastVerified(id: number): Promise<void> {
await db.update(unloggedUserTokens).set({ lastVerified: new Date() }).where(eq(unloggedUserTokens.id, id))
}
async insertUnloggedToken(token: string): Promise<void> {
await db.insert(unloggedUserTokens).values({
token,
lastVerified: new Date(),
})
}
}
export const userProfileDbService: IUserProfileDbService = new UserProfileDbService()

Some files were not shown because too many files have changed in this diff Show more