enh
This commit is contained in:
parent
cd5ab79f44
commit
56b606ebcf
156 changed files with 19095 additions and 4311 deletions
6
apps/admin-ui/.expo/types/router.d.ts
vendored
6
apps/admin-ui/.expo/types/router.d.ts
vendored
File diff suppressed because one or more lines are too long
|
|
@ -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" }} />
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -19,7 +19,6 @@ import { startAutomatedJobs } from '@/src/lib/automatedJobs'
|
|||
seed()
|
||||
initFunc()
|
||||
startAutomatedJobs()
|
||||
signedUrlCache.loadFromDisk()
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
132
apps/backend/src/lib/signed-url-cache.ts
Executable file → Normal 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;
|
||||
263
apps/backend/src/lib/signed-url-cache.ts.txt
Executable file
263
apps/backend/src/lib/signed-url-cache.ts.txt
Executable 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;
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,405 +0,0 @@
|
|||
import { db } from '@/src/db/db_index'
|
||||
import {
|
||||
orders,
|
||||
orderItems,
|
||||
orderStatus,
|
||||
addresses,
|
||||
productInfo,
|
||||
paymentInfoTable,
|
||||
coupons,
|
||||
couponUsage,
|
||||
payments,
|
||||
cartItems,
|
||||
refunds,
|
||||
units,
|
||||
userDetails,
|
||||
} from '@/src/db/schema'
|
||||
import { eq, and, inArray, desc, gte } from 'drizzle-orm'
|
||||
|
||||
// ============ User/Auth Queries ============
|
||||
|
||||
/**
|
||||
* Get user details by user ID
|
||||
*/
|
||||
export async function getUserDetails(userId: number) {
|
||||
return db.query.userDetails.findFirst({
|
||||
where: eq(userDetails.userId, userId),
|
||||
})
|
||||
}
|
||||
|
||||
// ============ Address Queries ============
|
||||
|
||||
/**
|
||||
* Get user address by ID
|
||||
*/
|
||||
export async function getUserAddress(userId: number, addressId: number) {
|
||||
return db.query.addresses.findFirst({
|
||||
where: and(eq(addresses.userId, userId), eq(addresses.id, addressId)),
|
||||
})
|
||||
}
|
||||
|
||||
// ============ Product Queries ============
|
||||
|
||||
/**
|
||||
* Get product by ID
|
||||
*/
|
||||
export async function getProductById(productId: number) {
|
||||
return db.query.productInfo.findFirst({
|
||||
where: eq(productInfo.id, productId),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get multiple products by IDs with unit info
|
||||
*/
|
||||
export async function getProductsByIdsWithUnits(productIds: number[]) {
|
||||
return db
|
||||
.select({
|
||||
id: productInfo.id,
|
||||
name: productInfo.name,
|
||||
shortDescription: productInfo.shortDescription,
|
||||
price: productInfo.price,
|
||||
images: productInfo.images,
|
||||
isOutOfStock: productInfo.isOutOfStock,
|
||||
unitShortNotation: units.shortNotation,
|
||||
incrementStep: productInfo.incrementStep,
|
||||
})
|
||||
.from(productInfo)
|
||||
.innerJoin(units, eq(productInfo.unitId, units.id))
|
||||
.where(and(inArray(productInfo.id, productIds), eq(productInfo.isSuspended, false)))
|
||||
.orderBy(desc(productInfo.createdAt))
|
||||
}
|
||||
|
||||
// ============ Coupon Queries ============
|
||||
|
||||
/**
|
||||
* Get coupon with usages for user
|
||||
*/
|
||||
export async function getCouponWithUsages(couponId: number, userId: number) {
|
||||
return db.query.coupons.findFirst({
|
||||
where: eq(coupons.id, couponId),
|
||||
with: {
|
||||
usages: { where: eq(couponUsage.userId, userId) },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert coupon usage
|
||||
*/
|
||||
export async function insertCouponUsage(data: {
|
||||
userId: number
|
||||
couponId: number
|
||||
orderId: number
|
||||
orderItemId: number | null
|
||||
usedAt: Date
|
||||
}) {
|
||||
return db.insert(couponUsage).values(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get coupon usages for order
|
||||
*/
|
||||
export async function getCouponUsagesForOrder(orderId: number) {
|
||||
return db.query.couponUsage.findMany({
|
||||
where: eq(couponUsage.orderId, orderId),
|
||||
with: {
|
||||
coupon: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ============ Cart Queries ============
|
||||
|
||||
/**
|
||||
* Delete cart items for user by product IDs
|
||||
*/
|
||||
export async function deleteCartItems(userId: number, productIds: number[]) {
|
||||
return db.delete(cartItems).where(
|
||||
and(
|
||||
eq(cartItems.userId, userId),
|
||||
inArray(cartItems.productId, productIds)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// ============ Payment Info Queries ============
|
||||
|
||||
/**
|
||||
* Create payment info
|
||||
*/
|
||||
export async function createPaymentInfo(data: {
|
||||
status: string
|
||||
gateway: string
|
||||
merchantOrderId: string
|
||||
}) {
|
||||
return db.insert(paymentInfoTable).values(data).returning()
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ============ Order Queries ============
|
||||
|
||||
/**
|
||||
* Insert multiple orders
|
||||
*/
|
||||
export async function insertOrders(ordersData: any[]) {
|
||||
return db.insert(orders).values(ordersData).returning()
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert multiple order items
|
||||
*/
|
||||
export async function insertOrderItems(itemsData: any[]) {
|
||||
return db.insert(orderItems).values(itemsData)
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert multiple order statuses
|
||||
*/
|
||||
export async function insertOrderStatuses(statusesData: any[]) {
|
||||
return db.insert(orderStatus).values(statusesData)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user orders with all relations
|
||||
*/
|
||||
export async function getUserOrdersWithRelations(userId: number, limit: number, offset: number) {
|
||||
return db.query.orders.findMany({
|
||||
where: eq(orders.userId, userId),
|
||||
with: {
|
||||
orderItems: {
|
||||
with: {
|
||||
product: true,
|
||||
},
|
||||
},
|
||||
slot: true,
|
||||
paymentInfo: true,
|
||||
orderStatus: true,
|
||||
refunds: true,
|
||||
},
|
||||
orderBy: (orders, { desc }) => [desc(orders.createdAt)],
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Count user orders
|
||||
*/
|
||||
export async function countUserOrders(userId: number) {
|
||||
return db.$count(orders, eq(orders.userId, userId))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get order by ID with all relations
|
||||
*/
|
||||
export async function getOrderByIdWithRelations(orderId: number) {
|
||||
return db.query.orders.findFirst({
|
||||
where: eq(orders.id, orderId),
|
||||
with: {
|
||||
orderItems: {
|
||||
with: {
|
||||
product: true,
|
||||
},
|
||||
},
|
||||
slot: true,
|
||||
paymentInfo: true,
|
||||
orderStatus: {
|
||||
with: {
|
||||
refundCoupon: true,
|
||||
},
|
||||
},
|
||||
refunds: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get order by ID with order status
|
||||
*/
|
||||
export async function getOrderWithStatus(orderId: number) {
|
||||
return db.query.orders.findFirst({
|
||||
where: eq(orders.id, orderId),
|
||||
with: {
|
||||
orderStatus: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Update order status to cancelled
|
||||
*/
|
||||
export async function updateOrderStatusToCancelled(
|
||||
statusId: number,
|
||||
data: {
|
||||
isCancelled: boolean
|
||||
cancelReason: string
|
||||
cancellationUserNotes: string
|
||||
cancellationReviewed: boolean
|
||||
}
|
||||
) {
|
||||
return db
|
||||
.update(orderStatus)
|
||||
.set(data)
|
||||
.where(eq(orderStatus.id, statusId))
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert refund record
|
||||
*/
|
||||
export async function insertRefund(data: { orderId: number; refundStatus: string }) {
|
||||
return db.insert(refunds).values(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update order notes
|
||||
*/
|
||||
export async function updateOrderNotes(orderId: number, userNotes: string | null) {
|
||||
return db
|
||||
.update(orders)
|
||||
.set({ userNotes })
|
||||
.where(eq(orders.id, orderId))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent delivered orders for user
|
||||
*/
|
||||
export async function getRecentDeliveredOrders(
|
||||
userId: number,
|
||||
since: Date,
|
||||
limit: number
|
||||
) {
|
||||
return db
|
||||
.select({ id: orders.id })
|
||||
.from(orders)
|
||||
.innerJoin(orderStatus, eq(orders.id, orderStatus.orderId))
|
||||
.where(
|
||||
and(
|
||||
eq(orders.userId, userId),
|
||||
eq(orderStatus.isDelivered, true),
|
||||
gte(orders.createdAt, since)
|
||||
)
|
||||
)
|
||||
.orderBy(desc(orders.createdAt))
|
||||
.limit(limit)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get order items by order IDs
|
||||
*/
|
||||
export async function getOrderItemsByOrderIds(orderIds: number[]) {
|
||||
return db
|
||||
.select({ productId: orderItems.productId })
|
||||
.from(orderItems)
|
||||
.where(inArray(orderItems.orderId, orderIds))
|
||||
}
|
||||
|
||||
// ============ Transaction Helper ============
|
||||
|
||||
/**
|
||||
* Execute function within a database transaction
|
||||
*/
|
||||
export async function withTransaction<T>(fn: (tx: any) => Promise<T>): Promise<T> {
|
||||
return db.transaction(fn)
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel order with refund record in a transaction
|
||||
*/
|
||||
export async function cancelOrderWithRefund(
|
||||
statusId: number,
|
||||
orderId: number,
|
||||
isCod: boolean,
|
||||
reason: string
|
||||
): Promise<{ orderId: number }> {
|
||||
return db.transaction(async (tx) => {
|
||||
// Update order status
|
||||
await tx
|
||||
.update(orderStatus)
|
||||
.set({
|
||||
isCancelled: true,
|
||||
cancelReason: reason,
|
||||
cancellationUserNotes: reason,
|
||||
cancellationReviewed: false,
|
||||
})
|
||||
.where(eq(orderStatus.id, statusId))
|
||||
|
||||
// Insert refund record
|
||||
const refundStatus = isCod ? "na" : "pending"
|
||||
await tx.insert(refunds).values({
|
||||
orderId,
|
||||
refundStatus,
|
||||
})
|
||||
|
||||
return { orderId }
|
||||
})
|
||||
}
|
||||
|
||||
type Tx = Parameters<Parameters<typeof db.transaction>[0]>[0]
|
||||
|
||||
/**
|
||||
* Create orders with payment info in a transaction
|
||||
*/
|
||||
export async function createOrdersWithPayment(
|
||||
ordersData: any[],
|
||||
paymentMethod: "online" | "cod",
|
||||
totalWithDelivery: number,
|
||||
razorpayOrderCreator?: (paymentInfoId: number, amount: string) => Promise<any>,
|
||||
paymentRecordInserter?: (paymentInfoId: number, razorpayOrder: any, tx: Tx) => Promise<any>
|
||||
): Promise<typeof orders.$inferSelect[]> {
|
||||
return db.transaction(async (tx) => {
|
||||
let sharedPaymentInfoId: number | null = null
|
||||
if (paymentMethod === "online") {
|
||||
const [paymentInfo] = await tx
|
||||
.insert(paymentInfoTable)
|
||||
.values({
|
||||
status: "pending",
|
||||
gateway: "razorpay",
|
||||
merchantOrderId: `multi_order_${Date.now()}`,
|
||||
})
|
||||
.returning()
|
||||
sharedPaymentInfoId = paymentInfo.id
|
||||
}
|
||||
|
||||
const ordersToInsert: Omit<typeof orders.$inferInsert, "id">[] = ordersData.map(
|
||||
(od) => ({
|
||||
...od.order,
|
||||
paymentInfoId: sharedPaymentInfoId,
|
||||
})
|
||||
)
|
||||
|
||||
const insertedOrders = await tx.insert(orders).values(ordersToInsert).returning()
|
||||
|
||||
const allOrderItems: Omit<typeof orderItems.$inferInsert, "id">[] = []
|
||||
const allOrderStatuses: Omit<typeof orderStatus.$inferInsert, "id">[] = []
|
||||
|
||||
insertedOrders.forEach((order: typeof orders.$inferSelect, index: number) => {
|
||||
const od = ordersData[index]
|
||||
od.orderItems.forEach((item: any) => {
|
||||
allOrderItems.push({ ...item, orderId: order.id as number })
|
||||
})
|
||||
allOrderStatuses.push({
|
||||
...od.orderStatus,
|
||||
orderId: order.id as number,
|
||||
})
|
||||
})
|
||||
|
||||
await tx.insert(orderItems).values(allOrderItems)
|
||||
await tx.insert(orderStatus).values(allOrderStatuses)
|
||||
|
||||
if (paymentMethod === "online" && sharedPaymentInfoId && razorpayOrderCreator && paymentRecordInserter) {
|
||||
const razorpayOrder = await razorpayOrderCreator(
|
||||
sharedPaymentInfoId,
|
||||
totalWithDelivery.toString()
|
||||
)
|
||||
await paymentRecordInserter(
|
||||
sharedPaymentInfoId,
|
||||
razorpayOrder,
|
||||
tx
|
||||
)
|
||||
}
|
||||
|
||||
return insertedOrders
|
||||
})
|
||||
}
|
||||
|
|
@ -1,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;
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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' };
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
};
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 => ({
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
41
apps/backend/src/trpc/apis/admin-apis/dataAccessors/main.ts
Normal file
41
apps/backend/src/trpc/apis/admin-apis/dataAccessors/main.ts
Normal 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'
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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' };
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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' };
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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) => ({
|
||||
|
|
|
|||
|
|
@ -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: [],
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
};
|
||||
}),
|
||||
|
||||
});
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { homeBanners } from '@/src/db/schema'
|
||||
|
||||
export type UserBanner = typeof homeBanners.$inferSelect
|
||||
|
||||
export interface IUserBannerDbService {
|
||||
getActiveBanners(): Promise<UserBanner[]>
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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
|
||||
}>>
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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 }>>
|
||||
}
|
||||
|
|
@ -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
|
||||
}>>
|
||||
}
|
||||
32
apps/backend/src/trpc/apis/user-apis/dataAccessors/main.ts
Normal file
32
apps/backend/src/trpc/apis/user-apis/dataAccessors/main.ts
Normal 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'
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
Loading…
Add table
Reference in a new issue