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,8 +227,7 @@ export default function Layout() {
|
||||||
<Drawer.Screen name="slots" options={{ title: "Slots" }} />
|
<Drawer.Screen name="slots" options={{ title: "Slots" }} />
|
||||||
<Drawer.Screen name="vendor-snippets" options={{ title: "Vendor Snippets" }} />
|
<Drawer.Screen name="vendor-snippets" options={{ title: "Vendor Snippets" }} />
|
||||||
<Drawer.Screen name="stores" options={{ title: "Stores" }} />
|
<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="product-tags" options={{ title: "Product Tags" }} />
|
|
||||||
<Drawer.Screen name="rebalance-orders" options={{ title: "Rebalance Orders" }} />
|
<Drawer.Screen name="rebalance-orders" options={{ title: "Rebalance Orders" }} />
|
||||||
<Drawer.Screen name="user-management" options={{ title: "User Management" }} />
|
<Drawer.Screen name="user-management" options={{ title: "User Management" }} />
|
||||||
<Drawer.Screen name="send-notifications" options={{ title: "Send Notifications" }} />
|
<Drawer.Screen name="send-notifications" options={{ title: "Send Notifications" }} />
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -185,15 +185,6 @@ export default function Dashboard() {
|
||||||
iconColor: '#F97316',
|
iconColor: '#F97316',
|
||||||
iconBg: '#FFEDD5',
|
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',
|
title: 'App Constants',
|
||||||
icon: 'settings-applications',
|
icon: 'settings-applications',
|
||||||
|
|
|
||||||
|
|
@ -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()
|
seed()
|
||||||
initFunc()
|
initFunc()
|
||||||
startAutomatedJobs()
|
startAutomatedJobs()
|
||||||
signedUrlCache.loadFromDisk()
|
|
||||||
|
|
||||||
const app = new Hono()
|
const app = new Hono()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,6 @@
|
||||||
"jose": "^5.10.0",
|
"jose": "^5.10.0",
|
||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
"razorpay": "^2.9.6",
|
|
||||||
"redis": "^5.9.0",
|
"redis": "^5.9.0",
|
||||||
"zod": "^4.1.12"
|
"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 { 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 { db } from "@/src/db/db_index"
|
||||||
import { productInfo, units, productSlots, deliverySlotInfo, productTags } from "@/src/db/schema"
|
import { productInfo, units, productSlots, deliverySlotInfo, productTags } from "@/src/db/schema"
|
||||||
import { scaffoldAssetUrl } from "@/src/lib/s3-client"
|
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
|
* Get all products summary for dropdown
|
||||||
*/
|
*/
|
||||||
export const getAllProductsSummary = async (req: Request, res: Response) => {
|
export const getAllProductsSummary = async (c: Context) => {
|
||||||
try {
|
try {
|
||||||
const { tagId } = req.query;
|
const tagId = c.req.query('tagId');
|
||||||
const tagIdNum = tagId ? parseInt(tagId as string) : null;
|
const tagIdNum = tagId ? parseInt(tagId) : null;
|
||||||
|
|
||||||
let productIds: number[] | null = null;
|
let productIds: number[] | null = null;
|
||||||
|
|
||||||
|
|
@ -53,7 +53,7 @@ export const getAllProductsSummary = async (req: Request, res: Response) => {
|
||||||
whereCondition = inArray(productInfo.id, productIds);
|
whereCondition = inArray(productInfo.id, productIds);
|
||||||
} else if (tagIdNum) {
|
} else if (tagIdNum) {
|
||||||
// If tagId was provided but no products found, return empty array
|
// If tagId was provided but no products found, return empty array
|
||||||
return res.status(200).json({
|
return c.json({
|
||||||
products: [],
|
products: [],
|
||||||
count: 0,
|
count: 0,
|
||||||
});
|
});
|
||||||
|
|
@ -94,12 +94,12 @@ export const getAllProductsSummary = async (req: Request, res: Response) => {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.status(200).json({
|
return c.json({
|
||||||
products: formattedProducts,
|
products: formattedProducts,
|
||||||
count: formattedProducts.length,
|
count: formattedProducts.length,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Get products summary error:", 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 { Hono } from 'hono'
|
||||||
import { getAllProductsSummary } from "@/src/apis/common-apis/apis/common-product.controller"
|
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)
|
||||||
|
|
||||||
|
export default app
|
||||||
const commonProductsRouter= router;
|
|
||||||
export default commonProductsRouter;
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import { Router } from "express";
|
import { Hono } from 'hono'
|
||||||
import commonProductsRouter from "@/src/apis/common-apis/apis/common-product.router"
|
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 app
|
||||||
|
|
||||||
export default commonRouter;
|
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,9 @@
|
||||||
import * as cron from 'node-cron';
|
import * as cron from 'node-cron';
|
||||||
import { checkPendingPayments, checkRefundStatuses } from '@/src/jobs/payment-status-checker'
|
|
||||||
|
|
||||||
const runCombinedJob = async () => {
|
const runCombinedJob = async () => {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
try {
|
try {
|
||||||
console.log('Starting combined job: payments and refunds check');
|
console.log('Starting combined job');
|
||||||
|
|
||||||
// Run payment check
|
|
||||||
// await checkPendingPayments();
|
|
||||||
|
|
||||||
// Run refund check
|
|
||||||
// await checkRefundStatuses();
|
|
||||||
|
|
||||||
console.log('Combined job completed successfully');
|
console.log('Combined job completed successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in combined job:', 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
134
apps/backend/src/lib/signed-url-cache.ts
Executable file → Normal file
134
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 for cache entries with TTL
|
||||||
interface CacheEntry {
|
interface CacheEntry {
|
||||||
value: string;
|
value: string;
|
||||||
|
|
@ -16,18 +11,7 @@ class SignedURLCache {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.originalToSignedCache = new Map();
|
this.originalToSignedCache = new Map();
|
||||||
this.signedToOriginalCache = new Map();
|
this.signedToOriginalCache = new Map();
|
||||||
|
console.log('SignedURLCache: Initialized (in-memory only)');
|
||||||
// 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')
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -110,7 +94,7 @@ class SignedURLCache {
|
||||||
clear(): void {
|
clear(): void {
|
||||||
this.originalToSignedCache.clear();
|
this.originalToSignedCache.clear();
|
||||||
this.signedToOriginalCache.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 {
|
getStats(): { totalEntries: number } {
|
||||||
try {
|
return {
|
||||||
// Remove expired entries before saving
|
totalEntries: this.originalToSignedCache.size
|
||||||
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
|
* Stub methods for backward compatibility - do nothing in in-memory mode
|
||||||
*/
|
*/
|
||||||
|
saveToDisk(): void {
|
||||||
|
// No-op: In-memory cache only
|
||||||
|
}
|
||||||
|
|
||||||
loadFromDisk(): void {
|
loadFromDisk(): void {
|
||||||
try {
|
// No-op: In-memory cache only
|
||||||
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
|
// Create a singleton instance to be used throughout the application
|
||||||
const signedUrlCache = new SignedURLCache();
|
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;
|
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 { Hono } from 'hono'
|
||||||
import { authenticateUser } from '@/src/middleware/auth.middleware'
|
import { authenticateUser } from '@/src/middleware/auth.middleware'
|
||||||
|
import v1Router from '@/src/v1-router'
|
||||||
|
|
||||||
// Note: This router is kept for compatibility during migration
|
// Note: This router is kept for compatibility during migration
|
||||||
// Most routes have been moved to tRPC
|
// 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
|
// Apply authentication middleware to all subsequent routes
|
||||||
router.use('*', authenticateUser)
|
router.use('*', authenticateUser)
|
||||||
|
|
||||||
// Legacy routes - most functionality moved to tRPC
|
|
||||||
// router.route('/v1', v1Router) // Uncomment if needed during transition
|
|
||||||
|
|
||||||
export default router
|
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 { router } from '@/src/trpc/trpc-index'
|
||||||
import { complaintRouter } from '@/src/trpc/apis/admin-apis/apis/complaint'
|
import { complaintRouter } from '@/src/trpc/apis/admin-apis/apis/complaint'
|
||||||
import { couponRouter } from '@/src/trpc/apis/admin-apis/apis/coupon'
|
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 { orderRouter } from '@/src/trpc/apis/admin-apis/apis/order'
|
||||||
import { vendorSnippetsRouter } from '@/src/trpc/apis/admin-apis/apis/vendor-snippets'
|
import { vendorSnippetsRouter } from '@/src/trpc/apis/admin-apis/apis/vendor-snippets'
|
||||||
import { slotsRouter } from '@/src/trpc/apis/admin-apis/apis/slots'
|
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 { staffUserRouter } from '@/src/trpc/apis/admin-apis/apis/staff-user'
|
||||||
import { storeRouter } from '@/src/trpc/apis/admin-apis/apis/store'
|
import { storeRouter } from '@/src/trpc/apis/admin-apis/apis/store'
|
||||||
import { adminPaymentsRouter } from '@/src/trpc/apis/admin-apis/apis/payments'
|
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 { bannerRouter } from '@/src/trpc/apis/admin-apis/apis/banner'
|
||||||
import { userRouter } from '@/src/trpc/apis/admin-apis/apis/user'
|
import { userRouter } from '@/src/trpc/apis/admin-apis/apis/user'
|
||||||
import { constRouter } from '@/src/trpc/apis/admin-apis/apis/const'
|
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({
|
export const adminRouter = router({
|
||||||
complaint: complaintRouter,
|
complaint: complaintRouter,
|
||||||
coupon: couponRouter,
|
coupon: couponRouter,
|
||||||
cancelledOrders: cancelledOrdersRouter,
|
|
||||||
order: orderRouter,
|
order: orderRouter,
|
||||||
vendorSnippets: vendorSnippetsRouter,
|
vendorSnippets: vendorSnippetsRouter,
|
||||||
slots: slotsRouter,
|
slots: slotsRouter,
|
||||||
|
|
@ -28,7 +25,6 @@ export const adminRouter = router({
|
||||||
staffUser: staffUserRouter,
|
staffUser: staffUserRouter,
|
||||||
store: storeRouter,
|
store: storeRouter,
|
||||||
payments: adminPaymentsRouter,
|
payments: adminPaymentsRouter,
|
||||||
address: addressRouter,
|
|
||||||
banner: bannerRouter,
|
banner: bannerRouter,
|
||||||
user: userRouter,
|
user: userRouter,
|
||||||
const: constRouter,
|
const: constRouter,
|
||||||
|
|
|
||||||
|
|
@ -1,53 +1,42 @@
|
||||||
import { z } from 'zod';
|
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 { 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 { ApiError } from '@/src/lib/api-error';
|
||||||
import { scheduleStoreInitialization } from '@/src/stores/store-initializer'
|
import { scheduleStoreInitialization } from '@/src/stores/store-initializer'
|
||||||
|
import { bannerDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/main'
|
||||||
|
|
||||||
export const bannerRouter = router({
|
export const bannerRouter = router({
|
||||||
// Get all banners
|
// Get all banners
|
||||||
getBanners: protectedProcedure
|
getBanners: protectedProcedure
|
||||||
.query(async () => {
|
.query(async () => {
|
||||||
try {
|
try {
|
||||||
|
const banners = await bannerDbService.getAllBanners()
|
||||||
|
|
||||||
const banners = await db.query.homeBanners.findMany({
|
// Convert S3 keys to signed URLs for client
|
||||||
orderBy: desc(homeBanners.createdAt), // Order by creation date instead
|
const bannersWithSignedUrls = await Promise.all(
|
||||||
// Removed product relationship since we now use productIds array
|
banners.map(async (banner) => {
|
||||||
});
|
try {
|
||||||
|
return {
|
||||||
|
...banner,
|
||||||
|
imageUrl: banner.imageUrl ? scaffoldAssetUrl(banner.imageUrl) : banner.imageUrl,
|
||||||
|
productIds: banner.productIds || [],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to generate signed URL for banner ${banner.id}:`, error);
|
||||||
|
return {
|
||||||
|
...banner,
|
||||||
|
imageUrl: banner.imageUrl,
|
||||||
|
productIds: banner.productIds || [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Convert S3 keys to signed URLs for client
|
return {
|
||||||
const bannersWithSignedUrls = await Promise.all(
|
banners: bannersWithSignedUrls,
|
||||||
banners.map(async (banner) => {
|
};
|
||||||
try {
|
} catch (e: any) {
|
||||||
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
|
|
||||||
productIds: banner.productIds || [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
banners: bannersWithSignedUrls,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
catch(e:any) {
|
|
||||||
console.log(e)
|
console.log(e)
|
||||||
|
|
||||||
throw new ApiError(e.message);
|
throw new ApiError(e.message);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
@ -56,23 +45,17 @@ export const bannerRouter = router({
|
||||||
getBanner: protectedProcedure
|
getBanner: protectedProcedure
|
||||||
.input(z.object({ id: z.number() }))
|
.input(z.object({ id: z.number() }))
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const banner = await db.query.homeBanners.findFirst({
|
const banner = await bannerDbService.getBannerById(input.id)
|
||||||
where: eq(homeBanners.id, input.id),
|
|
||||||
// Removed product relationship since we now use productIds array
|
|
||||||
});
|
|
||||||
|
|
||||||
if (banner) {
|
if (banner) {
|
||||||
try {
|
try {
|
||||||
// Convert S3 key to signed URL for client
|
|
||||||
if (banner.imageUrl) {
|
if (banner.imageUrl) {
|
||||||
banner.imageUrl = scaffoldAssetUrl(banner.imageUrl);
|
banner.imageUrl = scaffoldAssetUrl(banner.imageUrl);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to generate signed URL for banner ${banner.id}:`, 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) {
|
if (!banner.productIds) {
|
||||||
banner.productIds = [];
|
banner.productIds = [];
|
||||||
}
|
}
|
||||||
|
|
@ -89,29 +72,27 @@ export const bannerRouter = router({
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
productIds: z.array(z.number()).optional(),
|
productIds: z.array(z.number()).optional(),
|
||||||
redirectUrl: z.string().url().optional(),
|
redirectUrl: z.string().url().optional(),
|
||||||
// serialNum removed completely
|
|
||||||
}))
|
}))
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
const imageUrl = extractKeyFromPresignedUrl(input.imageUrl)
|
const imageUrl = extractKeyFromPresignedUrl(input.imageUrl)
|
||||||
// const imageUrl = input.imageUrl
|
|
||||||
const [banner] = await db.insert(homeBanners).values({
|
const banner = await bannerDbService.createBanner({
|
||||||
name: input.name,
|
name: input.name,
|
||||||
imageUrl: imageUrl,
|
imageUrl: imageUrl,
|
||||||
description: input.description,
|
description: input.description,
|
||||||
productIds: input.productIds || [],
|
productIds: input.productIds || [],
|
||||||
redirectUrl: input.redirectUrl,
|
redirectUrl: input.redirectUrl,
|
||||||
serialNum: 999, // Default value, not used
|
serialNum: 999,
|
||||||
isActive: false, // Default to inactive
|
isActive: false,
|
||||||
}).returning();
|
})
|
||||||
|
|
||||||
// Reinitialize stores to reflect changes
|
|
||||||
scheduleStoreInitialization()
|
scheduleStoreInitialization()
|
||||||
|
|
||||||
return banner;
|
return banner;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating banner:', 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 }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const { id, ...updateData } = input;
|
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 processedData: any = { ...updateData }
|
||||||
const finalData: any = { ...processedData };
|
|
||||||
if ('serialNum' in finalData && finalData.serialNum === null) {
|
if (updateData.imageUrl) {
|
||||||
// Set to null explicitly
|
processedData.imageUrl = extractKeyFromPresignedUrl(updateData.imageUrl)
|
||||||
finalData.serialNum = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const [banner] = await db.update(homeBanners)
|
if ('serialNum' in processedData && processedData.serialNum === null) {
|
||||||
.set({ ...finalData, lastUpdated: new Date(), })
|
processedData.serialNum = null;
|
||||||
.where(eq(homeBanners.id, id))
|
}
|
||||||
.returning();
|
|
||||||
|
const banner = await bannerDbService.updateBannerById(id, processedData)
|
||||||
|
|
||||||
// Reinitialize stores to reflect changes
|
|
||||||
scheduleStoreInitialization()
|
scheduleStoreInitialization()
|
||||||
|
|
||||||
return banner;
|
return banner;
|
||||||
|
|
@ -166,9 +137,8 @@ export const bannerRouter = router({
|
||||||
deleteBanner: protectedProcedure
|
deleteBanner: protectedProcedure
|
||||||
.input(z.object({ id: z.number() }))
|
.input(z.object({ id: z.number() }))
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
await db.delete(homeBanners).where(eq(homeBanners.id, input.id));
|
await bannerDbService.deleteBannerById(input.id)
|
||||||
|
|
||||||
// Reinitialize stores to reflect changes
|
|
||||||
scheduleStoreInitialization()
|
scheduleStoreInitialization()
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
|
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
|
||||||
import { z } from 'zod';
|
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 { scaffoldAssetUrl } from '@/src/lib/s3-client'
|
||||||
|
import { complaintDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/main'
|
||||||
|
|
||||||
export const complaintRouter = router({
|
export const complaintRouter = router({
|
||||||
getAll: protectedProcedure
|
getAll: protectedProcedure
|
||||||
|
|
@ -14,27 +12,7 @@ export const complaintRouter = router({
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const { cursor, limit } = input;
|
const { cursor, limit } = input;
|
||||||
|
|
||||||
let whereCondition = cursor
|
const complaintsData = await complaintDbService.getComplaints(cursor, limit);
|
||||||
? 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 hasMore = complaintsData.length > limit;
|
const hasMore = complaintsData.length > limit;
|
||||||
const complaintsToReturn = hasMore ? complaintsData.slice(0, limit) : complaintsData;
|
const complaintsToReturn = hasMore ? complaintsData.slice(0, limit) : complaintsData;
|
||||||
|
|
@ -70,10 +48,7 @@ export const complaintRouter = router({
|
||||||
resolve: protectedProcedure
|
resolve: protectedProcedure
|
||||||
.input(z.object({ id: z.string(), response: z.string().optional() }))
|
.input(z.object({ id: z.string(), response: z.string().optional() }))
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
await db
|
await complaintDbService.resolveComplaint(parseInt(input.id), input.response);
|
||||||
.update(complaints)
|
|
||||||
.set({ isResolved: true, response: input.response })
|
|
||||||
.where(eq(complaints.id, parseInt(input.id)));
|
|
||||||
|
|
||||||
return { message: 'Complaint resolved successfully' };
|
return { message: 'Complaint resolved successfully' };
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
|
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
|
||||||
import { z } from 'zod';
|
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 { computeConstants } from '@/src/lib/const-store'
|
||||||
import { CONST_KEYS } from '@/src/lib/const-keys'
|
import { CONST_KEYS } from '@/src/lib/const-keys'
|
||||||
|
import { constantDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/main'
|
||||||
|
|
||||||
export const constRouter = router({
|
export const constRouter = router({
|
||||||
getConstants: protectedProcedure
|
getConstants: protectedProcedure
|
||||||
.query(async () => {
|
.query(async () => {
|
||||||
|
const constants = await constantDbService.getAllConstants();
|
||||||
const constants = await db.select().from(keyValStore);
|
|
||||||
|
|
||||||
const resp = constants.map(c => ({
|
const resp = constants.map(c => ({
|
||||||
key: c.key,
|
key: c.key,
|
||||||
|
|
@ -38,23 +36,14 @@ export const constRouter = router({
|
||||||
throw new Error(`Invalid constant keys: ${invalidKeys.join(', ')}`);
|
throw new Error(`Invalid constant keys: ${invalidKeys.join(', ')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.transaction(async (tx) => {
|
const updatedCount = await constantDbService.upsertConstants(constants);
|
||||||
for (const { key, value } of constants) {
|
|
||||||
await tx.insert(keyValStore)
|
|
||||||
.values({ key, value })
|
|
||||||
.onConflictDoUpdate({
|
|
||||||
target: keyValStore.key,
|
|
||||||
set: { value },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Refresh all constants in Redis after database update
|
// Refresh all constants in Redis after database update
|
||||||
await computeConstants();
|
await computeConstants();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
updatedCount: constants.length,
|
updatedCount,
|
||||||
keys: constants.map(c => c.key),
|
keys: constants.map(c => c.key),
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
|
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
|
||||||
import { z } from 'zod';
|
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 dayjs from 'dayjs';
|
||||||
|
import { couponDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/main'
|
||||||
|
|
||||||
const createCouponBodySchema = z.object({
|
const createCouponBodySchema = z.object({
|
||||||
couponCode: z.string().optional(),
|
couponCode: z.string().optional(),
|
||||||
|
|
@ -51,10 +49,7 @@ export const couponRouter = router({
|
||||||
|
|
||||||
// If applicableUsers is provided, verify users exist
|
// If applicableUsers is provided, verify users exist
|
||||||
if (applicableUsers && applicableUsers.length > 0) {
|
if (applicableUsers && applicableUsers.length > 0) {
|
||||||
const existingUsers = await db.query.users.findMany({
|
const existingUsers = await couponDbService.getUsersByIds(applicableUsers);
|
||||||
where: inArray(users.id, applicableUsers),
|
|
||||||
columns: { id: true },
|
|
||||||
});
|
|
||||||
if (existingUsers.length !== applicableUsers.length) {
|
if (existingUsers.length !== applicableUsers.length) {
|
||||||
throw new Error("Some applicable users not found");
|
throw new Error("Some applicable users not found");
|
||||||
}
|
}
|
||||||
|
|
@ -69,56 +64,40 @@ export const couponRouter = router({
|
||||||
// Generate coupon code if not provided
|
// Generate coupon code if not provided
|
||||||
let finalCouponCode = couponCode;
|
let finalCouponCode = couponCode;
|
||||||
if (!finalCouponCode) {
|
if (!finalCouponCode) {
|
||||||
// Generate a unique coupon code
|
|
||||||
const timestamp = Date.now().toString().slice(-6);
|
const timestamp = Date.now().toString().slice(-6);
|
||||||
const random = Math.random().toString(36).substring(2, 8).toUpperCase();
|
const random = Math.random().toString(36).substring(2, 8).toUpperCase();
|
||||||
finalCouponCode = `MF${timestamp}${random}`;
|
finalCouponCode = `MF${timestamp}${random}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if coupon code already exists
|
// Check if coupon code already exists
|
||||||
const existingCoupon = await db.query.coupons.findFirst({
|
const existingCoupon = await couponDbService.getCouponByCode(finalCouponCode);
|
||||||
where: eq(coupons.couponCode, finalCouponCode),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingCoupon) {
|
if (existingCoupon) {
|
||||||
throw new Error("Coupon code already exists");
|
throw new Error("Coupon code already exists");
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await db.insert(coupons).values({
|
const coupon = await couponDbService.createCoupon({
|
||||||
couponCode: finalCouponCode,
|
couponCode: finalCouponCode,
|
||||||
isUserBased: isUserBased || false,
|
isUserBased: isUserBased || false,
|
||||||
discountPercent: discountPercent?.toString(),
|
discountPercent: discountPercent?.toString() || null,
|
||||||
flatDiscount: flatDiscount?.toString(),
|
flatDiscount: flatDiscount?.toString() || null,
|
||||||
minOrder: minOrder?.toString(),
|
minOrder: minOrder?.toString() || null,
|
||||||
productIds: productIds || null,
|
productIds: productIds || null,
|
||||||
createdBy: staffUserId,
|
createdBy: staffUserId,
|
||||||
maxValue: maxValue?.toString(),
|
maxValue: maxValue?.toString() || null,
|
||||||
isApplyForAll: isApplyForAll || false,
|
isApplyForAll: isApplyForAll || false,
|
||||||
validTill: validTill ? dayjs(validTill).toDate() : undefined,
|
validTill: validTill ? dayjs(validTill).toDate() : null,
|
||||||
maxLimitForUser: maxLimitForUser,
|
maxLimitForUser: maxLimitForUser || null,
|
||||||
exclusiveApply: exclusiveApply || false,
|
exclusiveApply: exclusiveApply || false,
|
||||||
}).returning();
|
});
|
||||||
|
|
||||||
const coupon = result[0];
|
|
||||||
|
|
||||||
// Insert applicable users
|
// Insert applicable users
|
||||||
if (applicableUsers && applicableUsers.length > 0) {
|
if (applicableUsers && applicableUsers.length > 0) {
|
||||||
await db.insert(couponApplicableUsers).values(
|
await couponDbService.addApplicableUsers(coupon.id, applicableUsers);
|
||||||
applicableUsers.map(userId => ({
|
|
||||||
couponId: coupon.id,
|
|
||||||
userId,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert applicable products
|
// Insert applicable products
|
||||||
if (applicableProducts && applicableProducts.length > 0) {
|
if (applicableProducts && applicableProducts.length > 0) {
|
||||||
await db.insert(couponApplicableProducts).values(
|
await couponDbService.addApplicableProducts(coupon.id, applicableProducts);
|
||||||
applicableProducts.map(productId => ({
|
|
||||||
couponId: coupon.id,
|
|
||||||
productId,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return coupon;
|
return coupon;
|
||||||
|
|
@ -133,39 +112,7 @@ export const couponRouter = router({
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const { cursor, limit, search } = input;
|
const { cursor, limit, search } = input;
|
||||||
|
|
||||||
let whereCondition = undefined;
|
const result = await couponDbService.getAllCoupons({ cursor, limit, search });
|
||||||
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 hasMore = result.length > limit;
|
const hasMore = result.length > limit;
|
||||||
const couponsList = hasMore ? result.slice(0, limit) : result;
|
const couponsList = hasMore ? result.slice(0, limit) : result;
|
||||||
|
|
@ -177,24 +124,7 @@ export const couponRouter = router({
|
||||||
getById: protectedProcedure
|
getById: protectedProcedure
|
||||||
.input(z.object({ id: z.number() }))
|
.input(z.object({ id: z.number() }))
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const couponId = input.id;
|
const result = await couponDbService.getCouponById(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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
throw new Error("Coupon not found");
|
throw new Error("Coupon not found");
|
||||||
|
|
@ -225,27 +155,24 @@ export const couponRouter = router({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If updating to user-based, applicableUsers is required
|
// If updating to user-based, applicableUsers is required
|
||||||
if (updates.isUserBased && (!updates.applicableUsers || updates.applicableUsers.length === 0)) {
|
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) {
|
if (existingCount === 0) {
|
||||||
throw new Error("applicableUsers is required for user-based coupons");
|
throw new Error("applicableUsers is required for user-based coupons");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If applicableUsers is provided, verify users exist
|
// If applicableUsers is provided, verify users exist
|
||||||
if (updates.applicableUsers && updates.applicableUsers.length > 0) {
|
if (updates.applicableUsers && updates.applicableUsers.length > 0) {
|
||||||
const existingUsers = await db.query.users.findMany({
|
const existingUsers = await couponDbService.getUsersByIds(updates.applicableUsers);
|
||||||
where: inArray(users.id, updates.applicableUsers),
|
if (existingUsers.length !== updates.applicableUsers.length) {
|
||||||
columns: { id: true },
|
throw new Error("Some applicable users not found");
|
||||||
});
|
}
|
||||||
if (existingUsers.length !== updates.applicableUsers.length) {
|
}
|
||||||
throw new Error("Some applicable users not found");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateData: any = { ...updates };
|
const updateData: any = { ...updates };
|
||||||
delete updateData.applicableUsers; // Remove since we use couponApplicableUsers table
|
delete updateData.applicableUsers;
|
||||||
if (updates.discountPercent !== undefined) {
|
if (updates.discountPercent !== undefined) {
|
||||||
updateData.discountPercent = updates.discountPercent?.toString();
|
updateData.discountPercent = updates.discountPercent?.toString();
|
||||||
}
|
}
|
||||||
|
|
@ -255,67 +182,38 @@ export const couponRouter = router({
|
||||||
if (updates.minOrder !== undefined) {
|
if (updates.minOrder !== undefined) {
|
||||||
updateData.minOrder = updates.minOrder?.toString();
|
updateData.minOrder = updates.minOrder?.toString();
|
||||||
}
|
}
|
||||||
if (updates.maxValue !== undefined) {
|
if (updates.maxValue !== undefined) {
|
||||||
updateData.maxValue = updates.maxValue?.toString();
|
updateData.maxValue = updates.maxValue?.toString();
|
||||||
}
|
}
|
||||||
if (updates.validTill !== undefined) {
|
if (updates.validTill !== undefined) {
|
||||||
updateData.validTill = updates.validTill ? dayjs(updates.validTill).toDate() : null;
|
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
|
// Update applicable users: delete existing and insert new
|
||||||
if (updates.applicableUsers !== undefined) {
|
if (updates.applicableUsers !== undefined) {
|
||||||
await db.delete(couponApplicableUsers).where(eq(couponApplicableUsers.couponId, id));
|
await couponDbService.removeAllApplicableUsers(id);
|
||||||
if (updates.applicableUsers.length > 0) {
|
if (updates.applicableUsers.length > 0) {
|
||||||
await db.insert(couponApplicableUsers).values(
|
await couponDbService.addApplicableUsers(id, updates.applicableUsers);
|
||||||
updates.applicableUsers.map(userId => ({
|
|
||||||
couponId: id,
|
|
||||||
userId,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update applicable products: delete existing and insert new
|
// Update applicable products: delete existing and insert new
|
||||||
if (updates.applicableProducts !== undefined) {
|
if (updates.applicableProducts !== undefined) {
|
||||||
await db.delete(couponApplicableProducts).where(eq(couponApplicableProducts.couponId, id));
|
await couponDbService.removeAllApplicableProducts(id);
|
||||||
if (updates.applicableProducts.length > 0) {
|
if (updates.applicableProducts.length > 0) {
|
||||||
await db.insert(couponApplicableProducts).values(
|
await couponDbService.addApplicableProducts(id, updates.applicableProducts);
|
||||||
updates.applicableProducts.map(productId => ({
|
|
||||||
couponId: id,
|
|
||||||
productId,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result[0];
|
return result;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
delete: protectedProcedure
|
delete: protectedProcedure
|
||||||
.input(z.object({ id: z.number() }))
|
.input(z.object({ id: z.number() }))
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const { id } = input;
|
await couponDbService.invalidateCoupon(input.id);
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
|
|
||||||
return { message: "Coupon invalidated successfully" };
|
return { message: "Coupon invalidated successfully" };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
@ -328,14 +226,9 @@ export const couponRouter = router({
|
||||||
return { valid: false, message: "Invalid coupon code" };
|
return { valid: false, message: "Invalid coupon code" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const coupon = await db.query.coupons.findFirst({
|
const coupon = await couponDbService.getCouponByCode(code.toUpperCase());
|
||||||
where: and(
|
|
||||||
eq(coupons.couponCode, code.toUpperCase()),
|
|
||||||
eq(coupons.isInvalidated, false)
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!coupon) {
|
if (!coupon || coupon.isInvalidated) {
|
||||||
return { valid: false, message: "Coupon not found or invalidated" };
|
return { valid: false, message: "Coupon not found or invalidated" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -370,115 +263,71 @@ export const couponRouter = router({
|
||||||
discountAmount = maxValueLimit;
|
discountAmount = maxValueLimit;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
valid: true,
|
valid: true,
|
||||||
discountAmount,
|
discountAmount,
|
||||||
coupon: {
|
coupon: {
|
||||||
id: coupon.id,
|
id: coupon.id,
|
||||||
discountPercent: coupon.discountPercent,
|
discountPercent: coupon.discountPercent,
|
||||||
flatDiscount: coupon.flatDiscount,
|
flatDiscount: coupon.flatDiscount,
|
||||||
maxValue: coupon.maxValue,
|
maxValue: coupon.maxValue,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
generateCancellationCoupon: protectedProcedure
|
generateCancellationCoupon: protectedProcedure
|
||||||
.input(
|
.input(z.object({ orderId: z.number() }))
|
||||||
z.object({
|
.mutation(async ({ input, ctx }) => {
|
||||||
orderId: z.number(),
|
const { orderId } = input;
|
||||||
})
|
|
||||||
)
|
|
||||||
.mutation(async ({ input, ctx }) => {
|
|
||||||
const { orderId } = input;
|
|
||||||
|
|
||||||
// Get staff user ID from auth middleware
|
const staffUserId = ctx.staffUser?.id;
|
||||||
const staffUserId = ctx.staffUser?.id;
|
if (!staffUserId) {
|
||||||
if (!staffUserId) {
|
throw new Error("Unauthorized");
|
||||||
throw new Error("Unauthorized");
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Find the order with user and order status information
|
const order = await couponDbService.getOrderByIdWithUserAndStatus(orderId);
|
||||||
const order = await db.query.orders.findFirst({
|
|
||||||
where: eq(orders.id, orderId),
|
|
||||||
with: {
|
|
||||||
user: true,
|
|
||||||
orderStatus: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!order) {
|
if (!order) {
|
||||||
throw new Error("Order not found");
|
throw new Error("Order not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if order is cancelled (check if any status entry has isCancelled: true)
|
if (!order.user) {
|
||||||
// const isOrderCancelled = order.orderStatus?.some(status => status.isCancelled) || false;
|
throw new Error("User not found for this order");
|
||||||
// if (!isOrderCancelled) {
|
}
|
||||||
// throw new Error("Order is not cancelled");
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Check if payment method is COD
|
const userNamePrefix = (order.user.name || order.user.mobile || 'USR').substring(0, 3).toUpperCase();
|
||||||
// if (order.isCod) {
|
const couponCode = `${userNamePrefix}${orderId}`;
|
||||||
// throw new Error("Can't generate refund coupon for CoD Order");
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Verify user exists
|
const existingCoupon = await couponDbService.getCouponByCode(couponCode);
|
||||||
if (!order.user) {
|
if (existingCoupon) {
|
||||||
throw new Error("User not found for this order");
|
throw new Error("Coupon code already exists");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate coupon code: first 3 letters of user name or mobile + orderId
|
const orderAmount = parseFloat(order.totalAmount);
|
||||||
const userNamePrefix = (order.user.name || order.user.mobile || 'USR').substring(0, 3).toUpperCase();
|
const expiryDate = new Date();
|
||||||
const couponCode = `${userNamePrefix}${orderId}`;
|
expiryDate.setDate(expiryDate.getDate() + 30);
|
||||||
|
|
||||||
// Check if coupon code already exists
|
const coupon = await couponDbService.withTransaction(async (tx) => {
|
||||||
const existingCoupon = await db.query.coupons.findFirst({
|
const newCoupon = await couponDbService.createCoupon({
|
||||||
where: eq(coupons.couponCode, couponCode),
|
couponCode,
|
||||||
});
|
isUserBased: true,
|
||||||
|
flatDiscount: orderAmount.toString(),
|
||||||
|
minOrder: orderAmount.toString(),
|
||||||
|
maxValue: orderAmount.toString(),
|
||||||
|
validTill: expiryDate,
|
||||||
|
maxLimitForUser: 1,
|
||||||
|
createdBy: staffUserId,
|
||||||
|
isApplyForAll: false,
|
||||||
|
});
|
||||||
|
|
||||||
if (existingCoupon) {
|
await couponDbService.addApplicableUsers(newCoupon.id, [order.userId]);
|
||||||
throw new Error("Coupon code already exists");
|
await couponDbService.updateOrderStatusRefundCoupon(orderId, newCoupon.id);
|
||||||
}
|
|
||||||
|
|
||||||
// Get order total amount
|
return newCoupon;
|
||||||
const orderAmount = parseFloat(order.totalAmount);
|
});
|
||||||
|
|
||||||
// Calculate expiry date (30 days from now)
|
return coupon;
|
||||||
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({
|
|
||||||
couponCode,
|
|
||||||
isUserBased: true,
|
|
||||||
flatDiscount: orderAmount.toString(),
|
|
||||||
minOrder: orderAmount.toString(),
|
|
||||||
maxValue: orderAmount.toString(),
|
|
||||||
validTill: expiryDate,
|
|
||||||
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));
|
|
||||||
|
|
||||||
return coupon;
|
|
||||||
});
|
|
||||||
|
|
||||||
return coupon;
|
|
||||||
}),
|
|
||||||
|
|
||||||
getReservedCoupons: protectedProcedure
|
getReservedCoupons: protectedProcedure
|
||||||
.input(z.object({
|
.input(z.object({
|
||||||
|
|
@ -487,100 +336,52 @@ export const couponRouter = router({
|
||||||
search: z.string().optional(),
|
search: z.string().optional(),
|
||||||
}))
|
}))
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const { cursor, limit, search } = input;
|
const result = await couponDbService.getReservedCoupons(input);
|
||||||
|
|
||||||
let whereCondition = undefined;
|
const hasMore = result.length > input.limit;
|
||||||
const conditions = [];
|
const coupons = hasMore ? result.slice(0, input.limit) : result;
|
||||||
|
|
||||||
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 nextCursor = hasMore ? result[result.length - 1].id : undefined;
|
const nextCursor = hasMore ? result[result.length - 1].id : undefined;
|
||||||
|
|
||||||
return {
|
return { coupons, nextCursor };
|
||||||
coupons,
|
|
||||||
nextCursor,
|
|
||||||
};
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
createReservedCoupon: protectedProcedure
|
createReservedCoupon: protectedProcedure
|
||||||
.input(createCouponBodySchema)
|
.input(createCouponBodySchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.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)) {
|
if ((!discountPercent && !flatDiscount) || (discountPercent && flatDiscount)) {
|
||||||
throw new Error("Either discountPercent or flatDiscount must be provided (but not both)");
|
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;
|
const staffUserId = ctx.staffUser?.id;
|
||||||
if (!staffUserId) {
|
if (!staffUserId) {
|
||||||
throw new Error("Unauthorized");
|
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()}`;
|
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 couponDbService.getCouponByCode(secretCode);
|
||||||
const existing = await db.query.reservedCoupons.findFirst({
|
|
||||||
where: eq(reservedCoupons.secretCode, secretCode),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
throw new Error("Secret code already exists");
|
throw new Error("Secret code already exists");
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await db.insert(reservedCoupons).values({
|
const coupon = await couponDbService.createReservedCoupon({
|
||||||
secretCode,
|
secretCode,
|
||||||
couponCode: couponCode || `RESERVED${Date.now().toString().slice(-6)}`,
|
couponCode: couponCode || `RESERVED${Date.now().toString().slice(-6)}`,
|
||||||
discountPercent: discountPercent?.toString(),
|
discountPercent: discountPercent?.toString() || null,
|
||||||
flatDiscount: flatDiscount?.toString(),
|
flatDiscount: flatDiscount?.toString() || null,
|
||||||
minOrder: minOrder?.toString(),
|
minOrder: minOrder?.toString() || null,
|
||||||
productIds,
|
productIds: productIds || null,
|
||||||
maxValue: maxValue?.toString(),
|
maxValue: maxValue?.toString() || null,
|
||||||
validTill: validTill ? dayjs(validTill).toDate() : undefined,
|
validTill: validTill ? dayjs(validTill).toDate() : null,
|
||||||
maxLimitForUser,
|
maxLimitForUser: maxLimitForUser || null,
|
||||||
exclusiveApply: exclusiveApply || false,
|
exclusiveApply: exclusiveApply || false,
|
||||||
createdBy: staffUserId,
|
createdBy: staffUserId,
|
||||||
}).returning();
|
});
|
||||||
|
|
||||||
const coupon = result[0];
|
|
||||||
|
|
||||||
// Insert applicable products if provided
|
|
||||||
if (applicableProducts && applicableProducts.length > 0) {
|
if (applicableProducts && applicableProducts.length > 0) {
|
||||||
await db.insert(couponApplicableProducts).values(
|
await couponDbService.addApplicableProducts(coupon.id, applicableProducts);
|
||||||
applicableProducts.map(productId => ({
|
|
||||||
couponId: coupon.id,
|
|
||||||
productId,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return coupon;
|
return coupon;
|
||||||
|
|
@ -593,27 +394,11 @@ export const couponRouter = router({
|
||||||
offset: z.number().min(0).default(0),
|
offset: z.number().min(0).default(0),
|
||||||
}))
|
}))
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const { search, limit } = input;
|
const { search, limit, offset } = input;
|
||||||
|
|
||||||
let whereCondition = undefined;
|
const userList = search
|
||||||
if (search && search.trim()) {
|
? await couponDbService.getUsersBySearch(search, limit, offset)
|
||||||
whereCondition = or(
|
: await couponDbService.getUsersByIds([]);
|
||||||
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)],
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
users: userList.map(user => ({
|
users: userList.map(user => ({
|
||||||
|
|
@ -624,88 +409,68 @@ export const couponRouter = router({
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
createCoupon: protectedProcedure
|
createCoupon: protectedProcedure
|
||||||
.input(z.object({
|
.input(z.object({ mobile: z.string().min(1, 'Mobile number is required') }))
|
||||||
mobile: z.string().min(1, 'Mobile number is required'),
|
.mutation(async ({ input, ctx }) => {
|
||||||
}))
|
const { mobile } = input;
|
||||||
.mutation(async ({ input, ctx }) => {
|
|
||||||
const { mobile } = input;
|
|
||||||
|
|
||||||
// Get staff user ID from auth middleware
|
const staffUserId = ctx.staffUser?.id;
|
||||||
const staffUserId = ctx.staffUser?.id;
|
if (!staffUserId) {
|
||||||
if (!staffUserId) {
|
throw new Error("Unauthorized");
|
||||||
throw new Error("Unauthorized");
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Clean mobile number (remove non-digits)
|
const cleanMobile = mobile.replace(/\D/g, '');
|
||||||
const cleanMobile = mobile.replace(/\D/g, '');
|
|
||||||
|
|
||||||
// Validate: exactly 10 digits
|
if (cleanMobile.length !== 10) {
|
||||||
if (cleanMobile.length !== 10) {
|
throw new Error("Mobile number must be exactly 10 digits");
|
||||||
throw new Error("Mobile number must be exactly 10 digits");
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user exists, create if not
|
let user = await couponDbService.getUserByMobile(cleanMobile);
|
||||||
let user = await db.query.users.findFirst({
|
|
||||||
where: eq(users.mobile, cleanMobile),
|
if (!user) {
|
||||||
|
user = await couponDbService.createUser({
|
||||||
|
name: null,
|
||||||
|
email: null,
|
||||||
|
mobile: cleanMobile,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!user) {
|
const timestamp = Date.now().toString().slice(-6);
|
||||||
// Create new user
|
const random = Math.random().toString(36).substring(2, 6).toUpperCase();
|
||||||
const [newUser] = await db.insert(users).values({
|
const couponCode = `MF${cleanMobile.slice(-4)}${timestamp}${random}`;
|
||||||
name: null,
|
|
||||||
email: null,
|
|
||||||
mobile: cleanMobile,
|
|
||||||
}).returning();
|
|
||||||
user = newUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate unique coupon code
|
const existingCode = await couponDbService.getCouponByCode(couponCode);
|
||||||
const timestamp = Date.now().toString().slice(-6);
|
if (existingCode) {
|
||||||
const random = Math.random().toString(36).substring(2, 6).toUpperCase();
|
throw new Error("Generated coupon code already exists - please try again");
|
||||||
const couponCode = `MF${cleanMobile.slice(-4)}${timestamp}${random}`;
|
}
|
||||||
|
|
||||||
// Check if coupon code already exists (very unlikely but safe)
|
const coupon = await couponDbService.createCoupon({
|
||||||
const existingCode = await db.query.coupons.findFirst({
|
couponCode,
|
||||||
where: eq(coupons.couponCode, couponCode),
|
isUserBased: true,
|
||||||
});
|
discountPercent: "20",
|
||||||
|
minOrder: "1000",
|
||||||
|
maxValue: "500",
|
||||||
|
maxLimitForUser: 1,
|
||||||
|
isApplyForAll: false,
|
||||||
|
exclusiveApply: false,
|
||||||
|
createdBy: staffUserId,
|
||||||
|
validTill: dayjs().add(90, 'days').toDate(),
|
||||||
|
});
|
||||||
|
|
||||||
if (existingCode) {
|
await couponDbService.addApplicableUsers(coupon.id, [user.id]);
|
||||||
throw new Error("Generated coupon code already exists - please try again");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the coupon
|
return {
|
||||||
const [coupon] = await db.insert(coupons).values({
|
success: true,
|
||||||
couponCode,
|
coupon: {
|
||||||
isUserBased: true,
|
id: coupon.id,
|
||||||
discountPercent: "20", // 20% discount
|
couponCode: coupon.couponCode,
|
||||||
minOrder: "1000", // ₹1000 minimum order
|
|
||||||
maxValue: "500", // ₹500 maximum discount
|
|
||||||
maxLimitForUser: 1, // One-time use
|
|
||||||
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,
|
userId: user.id,
|
||||||
});
|
userMobile: user.mobile,
|
||||||
|
discountPercent: 20,
|
||||||
return {
|
minOrder: 1000,
|
||||||
success: true,
|
maxValue: 500,
|
||||||
coupon: {
|
maxLimitForUser: 1,
|
||||||
id: coupon.id,
|
},
|
||||||
couponCode: coupon.couponCode,
|
};
|
||||||
userId: user.id,
|
}),
|
||||||
userMobile: user.mobile,
|
|
||||||
discountPercent: 20,
|
|
||||||
minOrder: 1000,
|
|
||||||
maxValue: 500,
|
|
||||||
maxLimitForUser: 1,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,15 @@
|
||||||
import { router, protectedProcedure } from "@/src/trpc/trpc-index"
|
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
|
||||||
import { z } from "zod";
|
import { z } from 'zod'
|
||||||
import { db } from "@/src/db/db_index"
|
import dayjs from 'dayjs'
|
||||||
import {
|
import utc from 'dayjs/plugin/utc'
|
||||||
orders,
|
import { ApiError } from '@/src/lib/api-error'
|
||||||
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 {
|
import {
|
||||||
sendOrderPackagedNotification,
|
sendOrderPackagedNotification,
|
||||||
sendOrderDeliveredNotification,
|
sendOrderDeliveredNotification,
|
||||||
} from "@/src/lib/notif-job";
|
} from '@/src/lib/notif-job'
|
||||||
import { publishCancellation } from "@/src/lib/post-order-handler"
|
import { publishCancellation } from '@/src/lib/post-order-handler'
|
||||||
import { getMultipleUserNegativityScores } from "@/src/stores/user-negativity-store"
|
import { getMultipleUserNegativityScores } from '@/src/stores/user-negativity-store'
|
||||||
|
import { orderDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/main'
|
||||||
|
|
||||||
const updateOrderNotesSchema = z.object({
|
const updateOrderNotesSchema = z.object({
|
||||||
orderId: z.number(),
|
orderId: z.number(),
|
||||||
|
|
@ -89,19 +76,13 @@ export const orderRouter = router({
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const { orderId, adminNotes } = input;
|
const { orderId, adminNotes } = input;
|
||||||
|
|
||||||
const result = await db
|
const result = await orderDbService.updateOrderNotes(orderId, adminNotes || null)
|
||||||
.update(orders)
|
|
||||||
.set({
|
|
||||||
adminNotes: adminNotes || null,
|
|
||||||
})
|
|
||||||
.where(eq(orders.id, orderId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
if (result.length === 0) {
|
if (!result) {
|
||||||
throw new Error("Order not found");
|
throw new Error("Order not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
return result[0];
|
return result;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getFullOrder: protectedProcedure
|
getFullOrder: protectedProcedure
|
||||||
|
|
@ -109,34 +90,14 @@ export const orderRouter = router({
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const { orderId } = input;
|
const { orderId } = input;
|
||||||
|
|
||||||
const orderData = await db.query.orders.findFirst({
|
const orderData = await orderDbService.getOrderWithRelations(orderId)
|
||||||
where: eq(orders.id, orderId),
|
|
||||||
with: {
|
|
||||||
user: true,
|
|
||||||
address: true,
|
|
||||||
slot: true,
|
|
||||||
orderItems: {
|
|
||||||
with: {
|
|
||||||
product: {
|
|
||||||
with: {
|
|
||||||
unit: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
payment: true,
|
|
||||||
paymentInfo: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!orderData) {
|
if (!orderData) {
|
||||||
throw new Error("Order not found");
|
throw new Error("Order not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get order status separately
|
// Get order status separately
|
||||||
const statusRecord = await db.query.orderStatus.findFirst({
|
const statusRecord = await orderDbService.getOrderStatusByOrderId(orderId)
|
||||||
where: eq(orderStatus.orderId, orderId),
|
|
||||||
});
|
|
||||||
|
|
||||||
let status: "pending" | "delivered" | "cancelled" = "pending";
|
let status: "pending" | "delivered" | "cancelled" = "pending";
|
||||||
if (statusRecord?.isCancelled) {
|
if (statusRecord?.isCancelled) {
|
||||||
|
|
@ -148,9 +109,7 @@ export const orderRouter = router({
|
||||||
// Get refund details if order is cancelled
|
// Get refund details if order is cancelled
|
||||||
let refund = null;
|
let refund = null;
|
||||||
if (status === "cancelled") {
|
if (status === "cancelled") {
|
||||||
refund = await db.query.refunds.findFirst({
|
refund = await orderDbService.getRefundByOrderId(orderId)
|
||||||
where: eq(refunds.orderId, orderId),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -220,39 +179,14 @@ export const orderRouter = router({
|
||||||
const { orderId } = input;
|
const { orderId } = input;
|
||||||
|
|
||||||
// Single optimized query with all relations
|
// Single optimized query with all relations
|
||||||
const orderData = await db.query.orders.findFirst({
|
const orderData = await orderDbService.getOrderWithDetails(orderId)
|
||||||
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
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!orderData) {
|
if (!orderData) {
|
||||||
throw new Error("Order not found");
|
throw new Error("Order not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get coupon usage for this specific order using new orderId field
|
// Get coupon usage for this specific order using new orderId field
|
||||||
const couponUsageData = await db.query.couponUsage.findMany({
|
const couponUsageData = await orderDbService.getCouponUsageByOrderId(orderData.id)
|
||||||
where: eq(couponUsage.orderId, orderData.id), // Use new orderId field
|
|
||||||
with: {
|
|
||||||
coupon: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
let couponData = null;
|
let couponData = null;
|
||||||
if (couponUsageData.length > 0) {
|
if (couponUsageData.length > 0) {
|
||||||
|
|
@ -388,27 +322,15 @@ export const orderRouter = router({
|
||||||
const { orderId, isPackaged } = input;
|
const { orderId, isPackaged } = input;
|
||||||
|
|
||||||
// Update all order items to the specified packaged state
|
// Update all order items to the specified packaged state
|
||||||
await db
|
const parsedOrderId = parseInt(orderId)
|
||||||
.update(orderItems)
|
await orderDbService.updateOrderItemsPackaged(parsedOrderId, isPackaged)
|
||||||
.set({ is_packaged: isPackaged })
|
|
||||||
.where(eq(orderItems.orderId, parseInt(orderId)));
|
|
||||||
|
|
||||||
// Also update the order status table for backward compatibility
|
const currentStatus = await orderDbService.getOrderStatusByOrderId(parsedOrderId)
|
||||||
if (!isPackaged) {
|
const isDelivered = !isPackaged ? false : currentStatus?.isDelivered || false
|
||||||
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 order = await db.query.orders.findFirst({
|
await orderDbService.updateOrderStatusPackaged(parsedOrderId, isPackaged, isDelivered)
|
||||||
where: eq(orders.id, parseInt(orderId)),
|
|
||||||
});
|
const order = await orderDbService.getOrderById(parsedOrderId)
|
||||||
if (order) await sendOrderPackagedNotification(order.userId, orderId);
|
if (order) await sendOrderPackagedNotification(order.userId, orderId);
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
|
|
@ -419,14 +341,10 @@ export const orderRouter = router({
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const { orderId, isDelivered } = input;
|
const { orderId, isDelivered } = input;
|
||||||
|
|
||||||
await db
|
const parsedOrderId = parseInt(orderId)
|
||||||
.update(orderStatus)
|
await orderDbService.updateOrderStatusDelivered(parsedOrderId, isDelivered)
|
||||||
.set({ isDelivered })
|
|
||||||
.where(eq(orderStatus.orderId, parseInt(orderId)));
|
|
||||||
|
|
||||||
const order = await db.query.orders.findFirst({
|
const order = await orderDbService.getOrderById(parsedOrderId)
|
||||||
where: eq(orders.id, parseInt(orderId)),
|
|
||||||
});
|
|
||||||
if (order) await sendOrderDeliveredNotification(order.userId, orderId);
|
if (order) await sendOrderDeliveredNotification(order.userId, orderId);
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
|
|
@ -438,9 +356,7 @@ export const orderRouter = router({
|
||||||
const { orderItemId, isPackaged, isPackageVerified } = input;
|
const { orderItemId, isPackaged, isPackageVerified } = input;
|
||||||
|
|
||||||
// Validate that orderItem exists
|
// Validate that orderItem exists
|
||||||
const orderItem = await db.query.orderItems.findFirst({
|
const orderItem = await orderDbService.getOrderItemById(orderItemId)
|
||||||
where: eq(orderItems.id, orderItemId),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!orderItem) {
|
if (!orderItem) {
|
||||||
throw new ApiError("Order item not found", 404);
|
throw new ApiError("Order item not found", 404);
|
||||||
|
|
@ -456,10 +372,7 @@ export const orderRouter = router({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the order item
|
// Update the order item
|
||||||
await db
|
await orderDbService.updateOrderItem(orderItemId, updateData)
|
||||||
.update(orderItems)
|
|
||||||
.set(updateData)
|
|
||||||
.where(eq(orderItems.id, orderItemId));
|
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}),
|
}),
|
||||||
|
|
@ -469,9 +382,7 @@ export const orderRouter = router({
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const { orderId } = input;
|
const { orderId } = input;
|
||||||
|
|
||||||
const order = await db.query.orders.findFirst({
|
const order = await orderDbService.getOrderById(orderId)
|
||||||
where: eq(orders.id, orderId),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!order) {
|
if (!order) {
|
||||||
throw new Error('Order not found');
|
throw new Error('Order not found');
|
||||||
|
|
@ -481,13 +392,7 @@ export const orderRouter = router({
|
||||||
const currentTotalAmount = parseFloat(order.totalAmount?.toString() || '0');
|
const currentTotalAmount = parseFloat(order.totalAmount?.toString() || '0');
|
||||||
const newTotalAmount = currentTotalAmount - currentDeliveryCharge;
|
const newTotalAmount = currentTotalAmount - currentDeliveryCharge;
|
||||||
|
|
||||||
await db
|
await orderDbService.removeDeliveryCharge(orderId, newTotalAmount.toString())
|
||||||
.update(orders)
|
|
||||||
.set({
|
|
||||||
deliveryCharge: '0',
|
|
||||||
totalAmount: newTotalAmount.toString()
|
|
||||||
})
|
|
||||||
.where(eq(orders.id, orderId));
|
|
||||||
|
|
||||||
return { success: true, message: 'Delivery charge removed' };
|
return { success: true, message: 'Delivery charge removed' };
|
||||||
}),
|
}),
|
||||||
|
|
@ -497,27 +402,10 @@ export const orderRouter = router({
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const { slotId } = input;
|
const { slotId } = input;
|
||||||
|
|
||||||
const slotOrders = await db.query.orders.findMany({
|
const slotOrders = await orderDbService.getOrdersBySlotId(parseInt(slotId))
|
||||||
where: eq(orders.slotId, parseInt(slotId)),
|
|
||||||
with: {
|
|
||||||
user: true,
|
|
||||||
address: true,
|
|
||||||
slot: true,
|
|
||||||
orderItems: {
|
|
||||||
with: {
|
|
||||||
product: {
|
|
||||||
with: {
|
|
||||||
unit: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
orderStatus: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const filteredOrders = slotOrders.filter((order) => {
|
const filteredOrders = slotOrders.filter((order) => {
|
||||||
const statusRecord = order.orderStatus[0];
|
const statusRecord = order.orderStatus?.[0];
|
||||||
return (
|
return (
|
||||||
order.isCod ||
|
order.isCod ||
|
||||||
(statusRecord && statusRecord.paymentStatus === "success")
|
(statusRecord && statusRecord.paymentStatus === "success")
|
||||||
|
|
@ -525,7 +413,7 @@ export const orderRouter = router({
|
||||||
});
|
});
|
||||||
|
|
||||||
const formattedOrders = filteredOrders.map((order) => {
|
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";
|
let status: "pending" | "delivered" | "cancelled" = "pending";
|
||||||
if (statusRecord?.isCancelled) {
|
if (statusRecord?.isCancelled) {
|
||||||
status = "cancelled";
|
status = "cancelled";
|
||||||
|
|
@ -582,39 +470,14 @@ export const orderRouter = router({
|
||||||
const start = dayjs().startOf("day").toDate();
|
const start = dayjs().startOf("day").toDate();
|
||||||
const end = dayjs().endOf("day").toDate();
|
const end = dayjs().endOf("day").toDate();
|
||||||
|
|
||||||
let whereCondition = and(
|
const todaysOrders = await orderDbService.getOrdersByDateRange(
|
||||||
gte(orders.createdAt, start),
|
start,
|
||||||
lt(orders.createdAt, end)
|
end,
|
||||||
);
|
slotId ? parseInt(slotId) : undefined
|
||||||
|
)
|
||||||
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 filteredOrders = todaysOrders.filter((order) => {
|
const filteredOrders = todaysOrders.filter((order) => {
|
||||||
const statusRecord = order.orderStatus[0];
|
const statusRecord = order.orderStatus?.[0];
|
||||||
return (
|
return (
|
||||||
order.isCod ||
|
order.isCod ||
|
||||||
(statusRecord && statusRecord.paymentStatus === "success")
|
(statusRecord && statusRecord.paymentStatus === "success")
|
||||||
|
|
@ -622,7 +485,7 @@ export const orderRouter = router({
|
||||||
});
|
});
|
||||||
|
|
||||||
const formattedOrders = filteredOrders.map((order) => {
|
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";
|
let status: "pending" | "delivered" | "cancelled" = "pending";
|
||||||
if (statusRecord?.isCancelled) {
|
if (statusRecord?.isCancelled) {
|
||||||
status = "cancelled";
|
status = "cancelled";
|
||||||
|
|
@ -677,16 +540,9 @@ export const orderRouter = router({
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const { addressId, latitude, longitude } = input;
|
const { addressId, latitude, longitude } = input;
|
||||||
|
|
||||||
const result = await db
|
const result = await orderDbService.updateAddressCoords(addressId, latitude, longitude)
|
||||||
.update(addresses)
|
|
||||||
.set({
|
|
||||||
adminLatitude: latitude,
|
|
||||||
adminLongitude: longitude,
|
|
||||||
})
|
|
||||||
.where(eq(addresses.id, addressId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
if (result.length === 0) {
|
if (!result) {
|
||||||
throw new ApiError("Address not found", 404);
|
throw new ApiError("Address not found", 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -707,78 +563,15 @@ export const orderRouter = router({
|
||||||
flashDeliveryFilter,
|
flashDeliveryFilter,
|
||||||
} = input;
|
} = input;
|
||||||
|
|
||||||
let whereCondition: SQL<unknown> | undefined = eq(orders.id, orders.id); // always true
|
const allOrders = await orderDbService.getAllOrdersWithFilters({
|
||||||
if (cursor) {
|
cursor,
|
||||||
whereCondition = and(whereCondition, lt(orders.id, cursor));
|
limit,
|
||||||
}
|
slotId,
|
||||||
if (slotId) {
|
packagedFilter,
|
||||||
whereCondition = and(whereCondition, eq(orders.slotId, slotId));
|
deliveredFilter,
|
||||||
}
|
cancellationFilter,
|
||||||
if (packagedFilter === "packaged") {
|
flashDeliveryFilter,
|
||||||
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 hasMore = allOrders.length > limit;
|
const hasMore = allOrders.length > limit;
|
||||||
const ordersToReturn = hasMore ? allOrders.slice(0, limit) : allOrders;
|
const ordersToReturn = hasMore ? allOrders.slice(0, limit) : allOrders;
|
||||||
|
|
@ -787,7 +580,7 @@ export const orderRouter = router({
|
||||||
const negativityScores = await getMultipleUserNegativityScores(userIds);
|
const negativityScores = await getMultipleUserNegativityScores(userIds);
|
||||||
|
|
||||||
const filteredOrders = ordersToReturn.filter((order) => {
|
const filteredOrders = ordersToReturn.filter((order) => {
|
||||||
const statusRecord = order.orderStatus[0];
|
const statusRecord = order.orderStatus?.[0];
|
||||||
return (
|
return (
|
||||||
order.isCod ||
|
order.isCod ||
|
||||||
(statusRecord && statusRecord.paymentStatus === "success")
|
(statusRecord && statusRecord.paymentStatus === "success")
|
||||||
|
|
@ -795,7 +588,7 @@ export const orderRouter = router({
|
||||||
});
|
});
|
||||||
|
|
||||||
const formattedOrders = filteredOrders.map((order) => {
|
const formattedOrders = filteredOrders.map((order) => {
|
||||||
const statusRecord = order.orderStatus[0];
|
const statusRecord = order.orderStatus?.[0];
|
||||||
let status: "pending" | "delivered" | "cancelled" = "pending";
|
let status: "pending" | "delivered" | "cancelled" = "pending";
|
||||||
if (statusRecord?.isCancelled) {
|
if (statusRecord?.isCancelled) {
|
||||||
status = "cancelled";
|
status = "cancelled";
|
||||||
|
|
@ -868,21 +661,7 @@ export const orderRouter = router({
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const slotIds = input.slotIds;
|
const slotIds = input.slotIds;
|
||||||
|
|
||||||
const ordersList = await db.query.orders.findMany({
|
const ordersList = await orderDbService.getOrdersBySlotIds(slotIds)
|
||||||
where: inArray(orders.slotId, slotIds),
|
|
||||||
with: {
|
|
||||||
orderItems: {
|
|
||||||
with: {
|
|
||||||
product: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
couponUsages: {
|
|
||||||
with: {
|
|
||||||
coupon: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const processedOrdersData = ordersList.map((order) => {
|
const processedOrdersData = ordersList.map((order) => {
|
||||||
|
|
||||||
|
|
@ -921,19 +700,19 @@ export const orderRouter = router({
|
||||||
})
|
})
|
||||||
|
|
||||||
const updatedOrderIds: number[] = [];
|
const updatedOrderIds: number[] = [];
|
||||||
await db.transaction(async (tx) => {
|
await orderDbService.updateOrdersAndItemsInTransaction(
|
||||||
for (const { order, updatedOrderItems, newTotal } of processedOrdersData) {
|
processedOrdersData.map((entry) => ({
|
||||||
await tx.update(orders).set({ totalAmount: newTotal.toString() }).where(eq(orders.id, order.id));
|
orderId: entry.order.id,
|
||||||
updatedOrderIds.push(order.id);
|
totalAmount: entry.newTotal.toString(),
|
||||||
|
items: entry.updatedOrderItems.map((item) => ({
|
||||||
for (const item of updatedOrderItems) {
|
id: item.id,
|
||||||
await tx.update(orderItems).set({
|
|
||||||
price: item.price,
|
price: item.price,
|
||||||
discountedPrice: item.discountedPrice
|
discountedPrice: item.discountedPrice || item.price,
|
||||||
}).where(eq(orderItems.id, item.id));
|
})),
|
||||||
}
|
}))
|
||||||
}
|
)
|
||||||
});
|
|
||||||
|
processedOrdersData.forEach((entry) => updatedOrderIds.push(entry.order.id))
|
||||||
|
|
||||||
return { success: true, updatedOrders: updatedOrderIds, message: `Rebalanced ${updatedOrderIds.length} orders.` };
|
return { success: true, updatedOrders: updatedOrderIds, message: `Rebalanced ${updatedOrderIds.length} orders.` };
|
||||||
}),
|
}),
|
||||||
|
|
@ -946,12 +725,7 @@ export const orderRouter = router({
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const { orderId, reason } = input;
|
const { orderId, reason } = input;
|
||||||
|
|
||||||
const order = await db.query.orders.findFirst({
|
const order = await orderDbService.getOrderWithStatus(orderId)
|
||||||
where: eq(orders.id, orderId),
|
|
||||||
with: {
|
|
||||||
orderStatus: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!order) {
|
if (!order) {
|
||||||
throw new ApiError("Order not found", 404);
|
throw new ApiError("Order not found", 404);
|
||||||
|
|
@ -970,28 +744,13 @@ export const orderRouter = router({
|
||||||
throw new ApiError("Cannot cancel delivered order", 400);
|
throw new ApiError("Cannot cancel delivered order", 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await db.transaction(async (tx) => {
|
await orderDbService.cancelOrderStatus(status.id, reason)
|
||||||
await tx
|
|
||||||
.update(orderStatus)
|
|
||||||
.set({
|
|
||||||
isCancelled: true,
|
|
||||||
isCancelledByAdmin: true,
|
|
||||||
cancelReason: reason,
|
|
||||||
cancellationAdminNotes: reason,
|
|
||||||
cancellationReviewed: true,
|
|
||||||
cancellationReviewedAt: new Date(),
|
|
||||||
})
|
|
||||||
.where(eq(orderStatus.id, status.id));
|
|
||||||
|
|
||||||
const refundStatus = order.isCod ? "na" : "pending";
|
const refundStatus = order.isCod ? 'na' : 'pending'
|
||||||
|
|
||||||
await tx.insert(refunds).values({
|
await orderDbService.createRefund(order.id, refundStatus)
|
||||||
orderId: order.id,
|
|
||||||
refundStatus,
|
|
||||||
});
|
|
||||||
|
|
||||||
return { orderId: order.id, userId: order.userId };
|
const result = { orderId: order.id, userId: order.userId }
|
||||||
});
|
|
||||||
|
|
||||||
// Publish to Redis for Telegram notification
|
// Publish to Redis for Telegram notification
|
||||||
await publishCancellation(result.orderId, 'admin', reason);
|
await publishCancellation(result.orderId, 'admin', reason);
|
||||||
|
|
@ -1005,14 +764,5 @@ export const orderRouter = router({
|
||||||
type RefundStatus = "success" | "pending" | "failed" | "none" | "na";
|
type RefundStatus = "success" | "pending" | "failed" | "none" | "na";
|
||||||
|
|
||||||
export async function deleteOrderById(orderId: number): Promise<void> {
|
export async function deleteOrderById(orderId: number): Promise<void> {
|
||||||
await db.transaction(async (tx) => {
|
await orderDbService.deleteOrderById(orderId)
|
||||||
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));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,7 @@
|
||||||
import { router, protectedProcedure } from "@/src/trpc/trpc-index"
|
import { router, protectedProcedure } from "@/src/trpc/trpc-index"
|
||||||
import { z } from "zod";
|
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 { 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
|
const initiateRefundSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
|
@ -37,18 +29,14 @@ export const adminPaymentsRouter = router({
|
||||||
const { orderId, refundPercent, refundAmount } = input;
|
const { orderId, refundPercent, refundAmount } = input;
|
||||||
|
|
||||||
// Validate order exists
|
// Validate order exists
|
||||||
const order = await db.query.orders.findFirst({
|
const order = await refundDbService.getOrderById(orderId);
|
||||||
where: eq(orders.id, orderId),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!order) {
|
if (!order) {
|
||||||
throw new ApiError("Order not found", 404);
|
throw new ApiError("Order not found", 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if order is paid
|
// Check if order is paid
|
||||||
const orderStatusRecord = await db.query.orderStatus.findFirst({
|
const orderStatusRecord = await refundDbService.getOrderStatusByOrderId(orderId);
|
||||||
where: eq(orderStatus.orderId, orderId),
|
|
||||||
});
|
|
||||||
|
|
||||||
if(order.isCod) {
|
if(order.isCod) {
|
||||||
throw new ApiError("Order is a Cash On Delivery. Not eligible for refund")
|
throw new ApiError("Order is a Cash On Delivery. Not eligible for refund")
|
||||||
|
|
@ -76,59 +64,36 @@ export const adminPaymentsRouter = router({
|
||||||
throw new ApiError("Invalid refund parameters", 400);
|
throw new ApiError("Invalid refund parameters", 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
let razorpayRefund = null;
|
let merchantRefundId = 'xxx'; //temporary suppressal
|
||||||
let merchantRefundId = null;
|
|
||||||
|
|
||||||
// Get payment record for online payments
|
|
||||||
const payment = await db.query.payments.findFirst({
|
|
||||||
where: and(
|
|
||||||
eq(payments.orderId, orderId),
|
|
||||||
eq(payments.status, "success")
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
|
// Get payment record for online payments
|
||||||
|
const payment = await refundDbService.getSuccessfulPaymentByOrderId(orderId);
|
||||||
|
|
||||||
|
if (!payment || payment.status !== "success") {
|
||||||
|
throw new ApiError("Payment not found or not successful", 404);
|
||||||
|
}
|
||||||
|
|
||||||
// Check if refund already exists for this order
|
// Check if refund already exists for this order
|
||||||
const existingRefund = await db.query.refunds.findFirst({
|
const existingRefund = await refundDbService.getRefundByOrderId(orderId);
|
||||||
where: eq(refunds.orderId, orderId),
|
|
||||||
});
|
|
||||||
|
|
||||||
const refundStatus = "initiated";
|
const refundStatus = "initiated";
|
||||||
|
|
||||||
if (existingRefund) {
|
if (existingRefund) {
|
||||||
// Update existing refund
|
// Update existing refund
|
||||||
await db
|
await refundDbService.updateRefund(existingRefund.id, {
|
||||||
.update(refunds)
|
refundAmount: calculatedRefundAmount.toString(),
|
||||||
.set({
|
refundStatus,
|
||||||
refundAmount: calculatedRefundAmount.toString(),
|
merchantRefundId,
|
||||||
refundStatus,
|
refundProcessedAt: order.isCod ? new Date() : null,
|
||||||
merchantRefundId,
|
});
|
||||||
refundProcessedAt: order.isCod ? new Date() : null,
|
|
||||||
})
|
|
||||||
.where(eq(refunds.id, existingRefund.id));
|
|
||||||
} else {
|
} else {
|
||||||
// Insert new refund
|
// Insert new refund
|
||||||
await db
|
await refundDbService.createRefund({
|
||||||
.insert(refunds)
|
orderId,
|
||||||
.values({
|
refundAmount: calculatedRefundAmount.toString(),
|
||||||
orderId,
|
refundStatus,
|
||||||
refundAmount: calculatedRefundAmount.toString(),
|
merchantRefundId,
|
||||||
refundStatus,
|
});
|
||||||
merchantRefundId,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
|
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
|
||||||
import { z } from 'zod';
|
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 { refreshScheduleJobs } from '@/src/lib/automatedJobs';
|
||||||
|
import { scheduleDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/main'
|
||||||
|
|
||||||
const createScheduleSchema = z.object({
|
const createScheduleSchema = z.object({
|
||||||
scheduleName: z.string().min(1, "Schedule name is required"),
|
scheduleName: z.string().min(1, "Schedule name is required"),
|
||||||
|
|
@ -35,33 +33,29 @@ export const productAvailabilitySchedulesRouter = router({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if schedule name already exists
|
// Check if schedule name already exists
|
||||||
const existingSchedule = await db.query.productAvailabilitySchedules.findFirst({
|
const existingSchedule = await scheduleDbService.getScheduleByName(scheduleName);
|
||||||
where: eq(productAvailabilitySchedules.scheduleName, scheduleName),
|
|
||||||
});
|
|
||||||
if (existingSchedule) {
|
if (existingSchedule) {
|
||||||
throw new Error("Schedule name already exists");
|
throw new Error("Schedule name already exists");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create schedule with arrays
|
// Create schedule with arrays
|
||||||
const scheduleResult = await db.insert(productAvailabilitySchedules).values({
|
const scheduleResult = await scheduleDbService.createSchedule({
|
||||||
scheduleName,
|
scheduleName,
|
||||||
time,
|
time,
|
||||||
action,
|
action,
|
||||||
productIds,
|
productIds,
|
||||||
groupIds,
|
groupIds,
|
||||||
}).returning();
|
});
|
||||||
|
|
||||||
// Refresh cron jobs to include new schedule
|
// Refresh cron jobs to include new schedule
|
||||||
await refreshScheduleJobs();
|
await refreshScheduleJobs();
|
||||||
|
|
||||||
return scheduleResult[0];
|
return scheduleResult;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getAll: protectedProcedure
|
getAll: protectedProcedure
|
||||||
.query(async () => {
|
.query(async () => {
|
||||||
const schedules = await db.query.productAvailabilitySchedules.findMany({
|
const schedules = await scheduleDbService.getAllSchedules();
|
||||||
orderBy: (productAvailabilitySchedules, { desc }) => [desc(productAvailabilitySchedules.createdAt)],
|
|
||||||
});
|
|
||||||
|
|
||||||
return schedules.map(schedule => ({
|
return schedules.map(schedule => ({
|
||||||
...schedule,
|
...schedule,
|
||||||
|
|
@ -75,9 +69,7 @@ export const productAvailabilitySchedulesRouter = router({
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const { id } = input;
|
const { id } = input;
|
||||||
|
|
||||||
const schedule = await db.query.productAvailabilitySchedules.findFirst({
|
const schedule = await scheduleDbService.getScheduleById(id);
|
||||||
where: eq(productAvailabilitySchedules.id, id),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!schedule) {
|
if (!schedule) {
|
||||||
throw new Error("Schedule not found");
|
throw new Error("Schedule not found");
|
||||||
|
|
@ -92,18 +84,14 @@ export const productAvailabilitySchedulesRouter = router({
|
||||||
const { id, updates } = input;
|
const { id, updates } = input;
|
||||||
|
|
||||||
// Check if schedule exists
|
// Check if schedule exists
|
||||||
const existingSchedule = await db.query.productAvailabilitySchedules.findFirst({
|
const existingSchedule = await scheduleDbService.getScheduleById(id);
|
||||||
where: eq(productAvailabilitySchedules.id, id),
|
|
||||||
});
|
|
||||||
if (!existingSchedule) {
|
if (!existingSchedule) {
|
||||||
throw new Error("Schedule not found");
|
throw new Error("Schedule not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check schedule name uniqueness if being updated
|
// Check schedule name uniqueness if being updated
|
||||||
if (updates.scheduleName && updates.scheduleName !== existingSchedule.scheduleName) {
|
if (updates.scheduleName && updates.scheduleName !== existingSchedule.scheduleName) {
|
||||||
const duplicateSchedule = await db.query.productAvailabilitySchedules.findFirst({
|
const duplicateSchedule = await scheduleDbService.getScheduleByName(updates.scheduleName);
|
||||||
where: eq(productAvailabilitySchedules.scheduleName, updates.scheduleName),
|
|
||||||
});
|
|
||||||
if (duplicateSchedule) {
|
if (duplicateSchedule) {
|
||||||
throw new Error("Schedule name already exists");
|
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.action !== undefined) updateData.action = updates.action;
|
||||||
if (updates.productIds !== undefined) updateData.productIds = updates.productIds;
|
if (updates.productIds !== undefined) updateData.productIds = updates.productIds;
|
||||||
if (updates.groupIds !== undefined) updateData.groupIds = updates.groupIds;
|
if (updates.groupIds !== undefined) updateData.groupIds = updates.groupIds;
|
||||||
updateData.lastUpdated = new Date();
|
|
||||||
|
|
||||||
const result = await db.update(productAvailabilitySchedules)
|
const result = await scheduleDbService.updateSchedule(id, updateData);
|
||||||
.set(updateData)
|
|
||||||
.where(eq(productAvailabilitySchedules.id, id))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
if (result.length === 0) {
|
|
||||||
throw new Error("Failed to update schedule");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh cron jobs to reflect changes
|
// Refresh cron jobs to reflect changes
|
||||||
await refreshScheduleJobs();
|
await refreshScheduleJobs();
|
||||||
|
|
||||||
return result[0];
|
return result;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
delete: protectedProcedure
|
delete: protectedProcedure
|
||||||
|
|
@ -138,13 +118,7 @@ export const productAvailabilitySchedulesRouter = router({
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const { id } = input;
|
const { id } = input;
|
||||||
|
|
||||||
const result = await db.delete(productAvailabilitySchedules)
|
await scheduleDbService.deleteSchedule(id);
|
||||||
.where(eq(productAvailabilitySchedules.id, id))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
if (result.length === 0) {
|
|
||||||
throw new Error("Schedule not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh cron jobs to remove deleted schedule
|
// Refresh cron jobs to remove deleted schedule
|
||||||
await refreshScheduleJobs();
|
await refreshScheduleJobs();
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,9 @@
|
||||||
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
|
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { db } from '@/src/db/db_index'
|
import { productDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/main'
|
||||||
import { productInfo, units, specialDeals, productSlots, productTags, productReviews, users, productGroupInfo, productGroupMembership } from '@/src/db/schema'
|
|
||||||
import { eq, and, inArray, desc, sql } from 'drizzle-orm';
|
|
||||||
import { ApiError } from '@/src/lib/api-error'
|
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 { deleteS3Image } from '@/src/lib/delete-image'
|
||||||
import type { SpecialDeal } from '@/src/db/types'
|
|
||||||
import { scheduleStoreInitialization } from '@/src/stores/store-initializer'
|
import { scheduleStoreInitialization } from '@/src/stores/store-initializer'
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -19,13 +16,7 @@ type CreateDeal = {
|
||||||
export const productRouter = router({
|
export const productRouter = router({
|
||||||
getProducts: protectedProcedure
|
getProducts: protectedProcedure
|
||||||
.query(async ({ ctx }) => {
|
.query(async ({ ctx }) => {
|
||||||
const products = await db.query.productInfo.findMany({
|
const products = await productDbService.getAllProducts();
|
||||||
orderBy: productInfo.name,
|
|
||||||
with: {
|
|
||||||
unit: true,
|
|
||||||
store: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Generate signed URLs for all product images
|
// Generate signed URLs for all product images
|
||||||
const productsWithSignedUrls = await Promise.all(
|
const productsWithSignedUrls = await Promise.all(
|
||||||
|
|
@ -48,30 +39,17 @@ export const productRouter = router({
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const { id } = input;
|
const { id } = input;
|
||||||
|
|
||||||
const product = await db.query.productInfo.findFirst({
|
const product = await productDbService.getProductById(id);
|
||||||
where: eq(productInfo.id, id),
|
|
||||||
with: {
|
|
||||||
unit: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!product) {
|
if (!product) {
|
||||||
throw new ApiError("Product not found", 404);
|
throw new ApiError("Product not found", 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch special deals for this product
|
// Fetch special deals for this product
|
||||||
const deals = await db.query.specialDeals.findMany({
|
const deals = await productDbService.getDealsByProductId(id);
|
||||||
where: eq(specialDeals.productId, id),
|
|
||||||
orderBy: specialDeals.quantity,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fetch associated tags for this product
|
// Fetch associated tags for this product
|
||||||
const productTagsData = await db.query.productTags.findMany({
|
const productTagsData = await productDbService.getTagsByProductId(id);
|
||||||
where: eq(productTags.productId, id),
|
|
||||||
with: {
|
|
||||||
tag: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Generate signed URLs for product images
|
// Generate signed URLs for product images
|
||||||
const productWithSignedUrls = {
|
const productWithSignedUrls = {
|
||||||
|
|
@ -93,10 +71,7 @@ export const productRouter = router({
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const { id } = input;
|
const { id } = input;
|
||||||
|
|
||||||
const [deletedProduct] = await db
|
const deletedProduct = await productDbService.deleteProduct(id);
|
||||||
.delete(productInfo)
|
|
||||||
.where(eq(productInfo.id, id))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
if (!deletedProduct) {
|
if (!deletedProduct) {
|
||||||
throw new ApiError("Product not found", 404);
|
throw new ApiError("Product not found", 404);
|
||||||
|
|
@ -146,40 +121,34 @@ export const productRouter = router({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for duplicate name
|
// Check for duplicate name
|
||||||
const existingProduct = await db.query.productInfo.findFirst({
|
const allProducts = await productDbService.getAllProducts();
|
||||||
where: eq(productInfo.name, name.trim()),
|
const existingProduct = allProducts.find(p => p.name === name.trim());
|
||||||
});
|
|
||||||
if (existingProduct) {
|
if (existingProduct) {
|
||||||
throw new ApiError("A product with this name already exists", 400);
|
throw new ApiError("A product with this name already exists", 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if unit exists
|
// Check if unit exists
|
||||||
const unit = await db.query.units.findFirst({
|
const unit = await productDbService.getUnitById(unitId);
|
||||||
where: eq(units.id, unitId),
|
|
||||||
});
|
|
||||||
if (!unit) {
|
if (!unit) {
|
||||||
throw new ApiError("Invalid unit ID", 400);
|
throw new ApiError("Invalid unit ID", 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(imageKeys)
|
console.log(imageKeys)
|
||||||
const [newProduct] = await db
|
const newProduct = await productDbService.createProduct({
|
||||||
.insert(productInfo)
|
name: name.trim(),
|
||||||
.values({
|
shortDescription,
|
||||||
name: name.trim(),
|
longDescription,
|
||||||
shortDescription,
|
unitId,
|
||||||
longDescription,
|
storeId,
|
||||||
unitId,
|
price: price.toString(),
|
||||||
storeId,
|
marketPrice: marketPrice?.toString(),
|
||||||
price: price.toString(),
|
incrementStep,
|
||||||
marketPrice: marketPrice?.toString(),
|
productQuantity,
|
||||||
incrementStep,
|
isSuspended,
|
||||||
productQuantity,
|
isFlashAvailable,
|
||||||
isSuspended,
|
flashPrice: flashPrice?.toString(),
|
||||||
isFlashAvailable,
|
images: imageKeys || [],
|
||||||
flashPrice: flashPrice?.toString(),
|
});
|
||||||
images: imageKeys || [],
|
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
// Handle deals
|
// Handle deals
|
||||||
if (deals && deals.length > 0) {
|
if (deals && deals.length > 0) {
|
||||||
|
|
@ -189,7 +158,7 @@ export const productRouter = router({
|
||||||
price: deal.price.toString(),
|
price: deal.price.toString(),
|
||||||
validTill: new Date(deal.validTill),
|
validTill: new Date(deal.validTill),
|
||||||
}));
|
}));
|
||||||
await db.insert(specialDeals).values(dealInserts);
|
await productDbService.createDeals(dealInserts);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle tags
|
// Handle tags
|
||||||
|
|
@ -198,7 +167,7 @@ export const productRouter = router({
|
||||||
productId: newProduct.id,
|
productId: newProduct.id,
|
||||||
tagId,
|
tagId,
|
||||||
}));
|
}));
|
||||||
await db.insert(productTags).values(tagAssociations);
|
await productDbService.createTagAssociations(tagAssociations);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Claim upload URLs
|
// Claim upload URLs
|
||||||
|
|
@ -207,7 +176,7 @@ export const productRouter = router({
|
||||||
try {
|
try {
|
||||||
await claimUploadUrl(key);
|
await claimUploadUrl(key);
|
||||||
} catch (e) {
|
} 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;
|
const { id, newImageKeys, imagesToDelete, deals, tagIds, ...updateData } = input;
|
||||||
|
|
||||||
// Get current product
|
// Get current product
|
||||||
const currentProduct = await db.query.productInfo.findFirst({
|
const currentProduct = await productDbService.getProductById(id);
|
||||||
where: eq(productInfo.id, id),
|
|
||||||
});
|
|
||||||
if (!currentProduct) {
|
if (!currentProduct) {
|
||||||
throw new ApiError("Product not found", 404);
|
throw new ApiError("Product not found", 404);
|
||||||
}
|
}
|
||||||
|
|
@ -262,11 +229,11 @@ export const productRouter = router({
|
||||||
try {
|
try {
|
||||||
await deleteS3Image(imageUrl);
|
await deleteS3Image(imageUrl);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Failed to delete image: ${imageUrl}`, e);
|
console.error("Failed to delete image:", imageUrl, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
currentImages = currentImages.filter(img => {
|
currentImages = currentImages.filter(img => {
|
||||||
//!imagesToDelete.includes(img)
|
// imagesToDelete.includes(img)
|
||||||
const isRemoved = imagesToDelete.some(item => item.includes(img));
|
const isRemoved = imagesToDelete.some(item => item.includes(img));
|
||||||
return !isRemoved;
|
return !isRemoved;
|
||||||
});
|
});
|
||||||
|
|
@ -280,28 +247,24 @@ export const productRouter = router({
|
||||||
try {
|
try {
|
||||||
await claimUploadUrl(key);
|
await claimUploadUrl(key);
|
||||||
} catch (e) {
|
} 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
|
// Update product - convert numeric fields to strings for PostgreSQL numeric type
|
||||||
const { price, marketPrice, flashPrice, ...otherData } = updateData;
|
const { price, marketPrice, flashPrice, ...otherData } = updateData;
|
||||||
const [updatedProduct] = await db
|
const updatedProduct = await productDbService.updateProduct(id, {
|
||||||
.update(productInfo)
|
...otherData,
|
||||||
.set({
|
...(price !== undefined && { price: price.toString() }),
|
||||||
...otherData,
|
...(marketPrice !== undefined && { marketPrice: marketPrice.toString() }),
|
||||||
...(price !== undefined && { price: price.toString() }),
|
...(flashPrice !== undefined && { flashPrice: flashPrice.toString() }),
|
||||||
...(marketPrice !== undefined && { marketPrice: marketPrice.toString() }),
|
images: currentImages,
|
||||||
...(flashPrice !== undefined && { flashPrice: flashPrice.toString() }),
|
});
|
||||||
images: currentImages,
|
|
||||||
})
|
|
||||||
.where(eq(productInfo.id, id))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
// Handle deals update
|
// Handle deals update
|
||||||
if (deals !== undefined) {
|
if (deals !== undefined) {
|
||||||
await db.delete(specialDeals).where(eq(specialDeals.productId, id));
|
await productDbService.deleteDealsByProductId(id);
|
||||||
if (deals.length > 0) {
|
if (deals.length > 0) {
|
||||||
const dealInserts = deals.map(deal => ({
|
const dealInserts = deals.map(deal => ({
|
||||||
productId: id,
|
productId: id,
|
||||||
|
|
@ -309,19 +272,19 @@ export const productRouter = router({
|
||||||
price: deal.price.toString(),
|
price: deal.price.toString(),
|
||||||
validTill: new Date(deal.validTill),
|
validTill: new Date(deal.validTill),
|
||||||
}));
|
}));
|
||||||
await db.insert(specialDeals).values(dealInserts);
|
await productDbService.createDeals(dealInserts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle tags update
|
// Handle tags update
|
||||||
if (tagIds !== undefined) {
|
if (tagIds !== undefined) {
|
||||||
await db.delete(productTags).where(eq(productTags.productId, id));
|
await productDbService.deleteTagAssociationsByProductId(id);
|
||||||
if (tagIds.length > 0) {
|
if (tagIds.length > 0) {
|
||||||
const tagAssociations = tagIds.map(tagId => ({
|
const tagAssociations = tagIds.map(tagId => ({
|
||||||
productId: id,
|
productId: id,
|
||||||
tagId,
|
tagId,
|
||||||
}));
|
}));
|
||||||
await db.insert(productTags).values(tagAssociations);
|
await productDbService.createTagAssociations(tagAssociations);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -340,21 +303,15 @@ export const productRouter = router({
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const { id } = input;
|
const { id } = input;
|
||||||
|
|
||||||
const product = await db.query.productInfo.findFirst({
|
const product = await productDbService.getProductById(id);
|
||||||
where: eq(productInfo.id, id),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!product) {
|
if (!product) {
|
||||||
throw new ApiError("Product not found", 404);
|
throw new ApiError("Product not found", 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [updatedProduct] = await db
|
const updatedProduct = await productDbService.updateProduct(id, {
|
||||||
.update(productInfo)
|
isOutOfStock: !product.isOutOfStock,
|
||||||
.set({
|
});
|
||||||
isOutOfStock: !product.isOutOfStock,
|
|
||||||
})
|
|
||||||
.where(eq(productInfo.id, id))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
// Reinitialize stores to reflect changes
|
// Reinitialize stores to reflect changes
|
||||||
scheduleStoreInitialization()
|
scheduleStoreInitialization()
|
||||||
|
|
@ -378,12 +335,7 @@ export const productRouter = router({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current associations
|
// Get current associations
|
||||||
const currentAssociations = await db.query.productSlots.findMany({
|
const currentAssociations = await productDbService.getProductSlotsBySlotId(parseInt(slotId));
|
||||||
where: eq(productSlots.slotId, parseInt(slotId)),
|
|
||||||
columns: {
|
|
||||||
productId: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentProductIds = currentAssociations.map(assoc => assoc.productId);
|
const currentProductIds = currentAssociations.map(assoc => assoc.productId);
|
||||||
const newProductIds = productIds.map((id: string) => parseInt(id));
|
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
|
// Remove associations for products that are no longer selected
|
||||||
if (productsToRemove.length > 0) {
|
if (productsToRemove.length > 0) {
|
||||||
await db.delete(productSlots).where(
|
for (const productId of productsToRemove) {
|
||||||
and(
|
await productDbService.deleteProductSlot(parseInt(slotId), productId);
|
||||||
eq(productSlots.slotId, parseInt(slotId)),
|
}
|
||||||
inArray(productSlots.productId, productsToRemove)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add associations for newly selected products
|
// Add associations for newly selected products
|
||||||
if (productsToAdd.length > 0) {
|
if (productsToAdd.length > 0) {
|
||||||
const newAssociations = productsToAdd.map(productId => ({
|
for (const productId of productsToAdd) {
|
||||||
productId,
|
await productDbService.createProductSlot(parseInt(slotId), productId);
|
||||||
slotId: parseInt(slotId),
|
}
|
||||||
}));
|
|
||||||
|
|
||||||
await db.insert(productSlots).values(newAssociations);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reinitialize stores to reflect changes
|
// Reinitialize stores to reflect changes
|
||||||
|
|
@ -429,12 +375,7 @@ export const productRouter = router({
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const { slotId } = input;
|
const { slotId } = input;
|
||||||
|
|
||||||
const associations = await db.query.productSlots.findMany({
|
const associations = await productDbService.getProductSlotsBySlotId(parseInt(slotId));
|
||||||
where: eq(productSlots.slotId, parseInt(slotId)),
|
|
||||||
columns: {
|
|
||||||
productId: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const productIds = associations.map(assoc => assoc.productId);
|
const productIds = associations.map(assoc => assoc.productId);
|
||||||
|
|
||||||
|
|
@ -459,13 +400,7 @@ export const productRouter = router({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch all associations for the requested slots
|
// Fetch all associations for the requested slots
|
||||||
const associations = await db.query.productSlots.findMany({
|
const associations = await productDbService.getProductSlotsBySlotIds(slotIds);
|
||||||
where: inArray(productSlots.slotId, slotIds),
|
|
||||||
columns: {
|
|
||||||
slotId: true,
|
|
||||||
productId: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Group by slotId
|
// Group by slotId
|
||||||
const result = associations.reduce((acc, assoc) => {
|
const result = associations.reduce((acc, assoc) => {
|
||||||
|
|
@ -495,23 +430,7 @@ export const productRouter = router({
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const { productId, limit, offset } = input;
|
const { productId, limit, offset } = input;
|
||||||
|
|
||||||
const reviews = await db
|
const reviews = await productDbService.getReviewsByProductId(productId, limit, offset);
|
||||||
.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);
|
|
||||||
|
|
||||||
// Generate signed URLs for images
|
// Generate signed URLs for images
|
||||||
const reviewsWithSignedUrls = await Promise.all(
|
const reviewsWithSignedUrls = await Promise.all(
|
||||||
|
|
@ -523,12 +442,7 @@ export const productRouter = router({
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if more reviews exist
|
// Check if more reviews exist
|
||||||
const totalCountResult = await db
|
const totalCount = await productDbService.getReviewCountByProductId(productId);
|
||||||
.select({ count: sql`count(*)` })
|
|
||||||
.from(productReviews)
|
|
||||||
.where(eq(productReviews.productId, productId));
|
|
||||||
|
|
||||||
const totalCount = Number(totalCountResult[0].count);
|
|
||||||
const hasMore = offset + limit < totalCount;
|
const hasMore = offset + limit < totalCount;
|
||||||
|
|
||||||
return { reviews: reviewsWithSignedUrls, hasMore };
|
return { reviews: reviewsWithSignedUrls, hasMore };
|
||||||
|
|
@ -544,14 +458,10 @@ export const productRouter = router({
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const { reviewId, adminResponse, adminResponseImages, uploadUrls } = input;
|
const { reviewId, adminResponse, adminResponseImages, uploadUrls } = input;
|
||||||
|
|
||||||
const [updatedReview] = await db
|
const updatedReview = await productDbService.updateReview(reviewId, {
|
||||||
.update(productReviews)
|
adminResponse,
|
||||||
.set({
|
adminResponseImages,
|
||||||
adminResponse,
|
});
|
||||||
adminResponseImages,
|
|
||||||
})
|
|
||||||
.where(eq(productReviews.id, reviewId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
if (!updatedReview) {
|
if (!updatedReview) {
|
||||||
throw new ApiError('Review not found', 404);
|
throw new ApiError('Review not found', 404);
|
||||||
|
|
@ -559,7 +469,6 @@ export const productRouter = router({
|
||||||
|
|
||||||
// Claim upload URLs
|
// Claim upload URLs
|
||||||
if (uploadUrls && uploadUrls.length > 0) {
|
if (uploadUrls && uploadUrls.length > 0) {
|
||||||
// const { claimUploadUrl } = await import('@/src/lib/s3-client');
|
|
||||||
await Promise.all(uploadUrls.map(url => claimUploadUrl(url)));
|
await Promise.all(uploadUrls.map(url => claimUploadUrl(url)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -568,22 +477,13 @@ export const productRouter = router({
|
||||||
|
|
||||||
getGroups: protectedProcedure
|
getGroups: protectedProcedure
|
||||||
.query(async ({ ctx }) => {
|
.query(async ({ ctx }) => {
|
||||||
const groups = await db.query.productGroupInfo.findMany({
|
const groups = await productDbService.getAllGroups() as any[];
|
||||||
with: {
|
|
||||||
memberships: {
|
|
||||||
with: {
|
|
||||||
product: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
orderBy: desc(productGroupInfo.createdAt),
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
groups: groups.map(group => ({
|
groups: groups.map(group => ({
|
||||||
...group,
|
...group,
|
||||||
products: group.memberships.map(m => m.product),
|
products: group.memberships?.map((m: any) => m.product) || [],
|
||||||
productCount: group.memberships.length,
|
productCount: group.memberships?.length || 0,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
@ -597,13 +497,10 @@ export const productRouter = router({
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const { group_name, description, product_ids } = input;
|
const { group_name, description, product_ids } = input;
|
||||||
|
|
||||||
const [newGroup] = await db
|
const newGroup = await productDbService.createGroup({
|
||||||
.insert(productGroupInfo)
|
groupName: group_name,
|
||||||
.values({
|
description,
|
||||||
groupName: group_name,
|
});
|
||||||
description,
|
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
if (product_ids.length > 0) {
|
if (product_ids.length > 0) {
|
||||||
const memberships = product_ids.map(productId => ({
|
const memberships = product_ids.map(productId => ({
|
||||||
|
|
@ -611,7 +508,7 @@ export const productRouter = router({
|
||||||
groupId: newGroup.id,
|
groupId: newGroup.id,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
await db.insert(productGroupMembership).values(memberships);
|
await productDbService.createGroupMemberships(memberships);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reinitialize stores to reflect changes
|
// Reinitialize stores to reflect changes
|
||||||
|
|
@ -637,11 +534,7 @@ export const productRouter = router({
|
||||||
if (group_name !== undefined) updateData.groupName = group_name;
|
if (group_name !== undefined) updateData.groupName = group_name;
|
||||||
if (description !== undefined) updateData.description = description;
|
if (description !== undefined) updateData.description = description;
|
||||||
|
|
||||||
const [updatedGroup] = await db
|
const updatedGroup = await productDbService.updateGroup(id, updateData);
|
||||||
.update(productGroupInfo)
|
|
||||||
.set(updateData)
|
|
||||||
.where(eq(productGroupInfo.id, id))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
if (!updatedGroup) {
|
if (!updatedGroup) {
|
||||||
throw new ApiError('Group not found', 404);
|
throw new ApiError('Group not found', 404);
|
||||||
|
|
@ -649,7 +542,7 @@ export const productRouter = router({
|
||||||
|
|
||||||
if (product_ids !== undefined) {
|
if (product_ids !== undefined) {
|
||||||
// Delete existing memberships
|
// Delete existing memberships
|
||||||
await db.delete(productGroupMembership).where(eq(productGroupMembership.groupId, id));
|
await productDbService.deleteGroupMembershipsByGroupId(id);
|
||||||
|
|
||||||
// Insert new memberships
|
// Insert new memberships
|
||||||
if (product_ids.length > 0) {
|
if (product_ids.length > 0) {
|
||||||
|
|
@ -658,7 +551,7 @@ export const productRouter = router({
|
||||||
groupId: id,
|
groupId: id,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
await db.insert(productGroupMembership).values(memberships);
|
await productDbService.createGroupMemberships(memberships);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -679,13 +572,10 @@ export const productRouter = router({
|
||||||
const { id } = input;
|
const { id } = input;
|
||||||
|
|
||||||
// Delete memberships first
|
// Delete memberships first
|
||||||
await db.delete(productGroupMembership).where(eq(productGroupMembership.groupId, id));
|
await productDbService.deleteGroupMembershipsByGroupId(id);
|
||||||
|
|
||||||
// Delete group
|
// Delete group
|
||||||
const [deletedGroup] = await db
|
const deletedGroup = await productDbService.deleteGroup(id);
|
||||||
.delete(productGroupInfo)
|
|
||||||
.where(eq(productGroupInfo.id, id))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
if (!deletedGroup) {
|
if (!deletedGroup) {
|
||||||
throw new ApiError('Group not found', 404);
|
throw new ApiError('Group not found', 404);
|
||||||
|
|
@ -718,34 +608,28 @@ export const productRouter = router({
|
||||||
|
|
||||||
// Validate that all productIds exist
|
// Validate that all productIds exist
|
||||||
const productIds = updates.map(u => u.productId);
|
const productIds = updates.map(u => u.productId);
|
||||||
const existingProducts = await db.query.productInfo.findMany({
|
const allExist = await productDbService.validateProductIdsExist(productIds);
|
||||||
where: inArray(productInfo.id, productIds),
|
|
||||||
columns: { id: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
const existingIds = new Set(existingProducts.map(p => p.id));
|
if (!allExist) {
|
||||||
const invalidIds = productIds.filter(id => !existingIds.has(id));
|
throw new ApiError('Some product IDs are invalid', 400);
|
||||||
|
|
||||||
if (invalidIds.length > 0) {
|
|
||||||
throw new ApiError(`Invalid product IDs: ${invalidIds.join(', ')}`, 400);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform batch update
|
// Perform batch update
|
||||||
const updatePromises = updates.map(async (update) => {
|
const batchUpdates = updates.map(update => {
|
||||||
const { productId, price, marketPrice, flashPrice, isFlashAvailable } = update;
|
const { productId, price, marketPrice, flashPrice, isFlashAvailable } = update;
|
||||||
const updateData: any = {};
|
const updateData: any = {};
|
||||||
if (price !== undefined) updateData.price = price;
|
if (price !== undefined) updateData.price = price.toString();
|
||||||
if (marketPrice !== undefined) updateData.marketPrice = marketPrice;
|
if (marketPrice !== undefined) updateData.marketPrice = marketPrice?.toString();
|
||||||
if (flashPrice !== undefined) updateData.flashPrice = flashPrice;
|
if (flashPrice !== undefined) updateData.flashPrice = flashPrice?.toString();
|
||||||
if (isFlashAvailable !== undefined) updateData.isFlashAvailable = isFlashAvailable;
|
if (isFlashAvailable !== undefined) updateData.isFlashAvailable = isFlashAvailable;
|
||||||
|
|
||||||
return db
|
return {
|
||||||
.update(productInfo)
|
productId,
|
||||||
.set(updateData)
|
data: updateData,
|
||||||
.where(eq(productInfo.id, productId));
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.all(updatePromises);
|
await productDbService.batchUpdateProducts(batchUpdates);
|
||||||
|
|
||||||
// Reinitialize stores to reflect changes
|
// Reinitialize stores to reflect changes
|
||||||
scheduleStoreInitialization()
|
scheduleStoreInitialization()
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,12 @@
|
||||||
import { router, protectedProcedure } from "@/src/trpc/trpc-index"
|
import { router, protectedProcedure } from "@/src/trpc/trpc-index"
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
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 { ApiError } from "@/src/lib/api-error"
|
||||||
import { appUrl } from "@/src/lib/env-exporter"
|
import { appUrl } from "@/src/lib/env-exporter"
|
||||||
import redisClient from "@/src/lib/redis-client"
|
import redisClient from "@/src/lib/redis-client"
|
||||||
import { getSlotSequenceKey } from "@/src/lib/redisKeyGetters"
|
import { getSlotSequenceKey } from "@/src/lib/redisKeyGetters"
|
||||||
import { scheduleStoreInitialization } from '@/src/stores/store-initializer'
|
import { scheduleStoreInitialization } from '@/src/stores/store-initializer'
|
||||||
|
import { slotDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/main'
|
||||||
|
|
||||||
interface CachedDeliverySequence {
|
interface CachedDeliverySequence {
|
||||||
[userId: string]: number[];
|
[userId: string]: number[];
|
||||||
|
|
@ -58,50 +55,29 @@ const getDeliverySequenceSchema = z.object({
|
||||||
|
|
||||||
const updateDeliverySequenceSchema = z.object({
|
const updateDeliverySequenceSchema = z.object({
|
||||||
id: z.number(),
|
id: z.number(),
|
||||||
// deliverySequence: z.array(z.number()),
|
|
||||||
deliverySequence: z.any(),
|
deliverySequence: z.any(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const slotsRouter = router({
|
export const slotsRouter = router({
|
||||||
// Exact replica of GET /av/slots
|
|
||||||
getAll: protectedProcedure.query(async ({ ctx }) => {
|
getAll: protectedProcedure.query(async ({ ctx }) => {
|
||||||
if (!ctx.staffUser?.id) {
|
if (!ctx.staffUser?.id) {
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
|
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const slots = await db.query.deliverySlotInfo
|
const slots = await slotDbService.getAllSlots();
|
||||||
.findMany({
|
|
||||||
where: eq(deliverySlotInfo.isActive, true),
|
const transformedSlots = slots.map((slot) => ({
|
||||||
orderBy: desc(deliverySlotInfo.deliveryTime),
|
...slot,
|
||||||
with: {
|
deliverySequence: slot.deliverySequence as number[],
|
||||||
productSlots: {
|
products: slot.productSlots.map((ps: any) => ps.product),
|
||||||
with: {
|
}));
|
||||||
product: {
|
|
||||||
columns: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
images: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((slots) =>
|
|
||||||
slots.map((slot) => ({
|
|
||||||
...slot,
|
|
||||||
deliverySequence: slot.deliverySequence as number[],
|
|
||||||
products: slot.productSlots.map((ps) => ps.product),
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
slots,
|
slots: transformedSlots,
|
||||||
count: slots.length,
|
count: transformedSlots.length,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Exact replica of POST /av/products/slots/product-ids
|
|
||||||
getSlotsProductIds: protectedProcedure
|
getSlotsProductIds: protectedProcedure
|
||||||
.input(z.object({ slotIds: z.array(z.number()) }))
|
.input(z.object({ slotIds: z.array(z.number()) }))
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
|
|
@ -122,25 +98,16 @@ export const slotsRouter = router({
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch all associations for the requested slots
|
const associations = await slotDbService.getProductSlotsBySlotIds(slotIds);
|
||||||
const associations = await db.query.productSlots.findMany({
|
|
||||||
where: inArray(productSlots.slotId, slotIds),
|
|
||||||
columns: {
|
|
||||||
slotId: true,
|
|
||||||
productId: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Group by slotId
|
const result = associations.reduce((acc: Record<number, number[]>, assoc) => {
|
||||||
const result = associations.reduce((acc, assoc) => {
|
|
||||||
if (!acc[assoc.slotId]) {
|
if (!acc[assoc.slotId]) {
|
||||||
acc[assoc.slotId] = [];
|
acc[assoc.slotId] = [];
|
||||||
}
|
}
|
||||||
acc[assoc.slotId].push(assoc.productId);
|
acc[assoc.slotId].push(assoc.productId);
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<number, number[]>);
|
}, {});
|
||||||
|
|
||||||
// Ensure all requested slots have entries (even if empty)
|
|
||||||
slotIds.forEach((slotId) => {
|
slotIds.forEach((slotId) => {
|
||||||
if (!result[slotId]) {
|
if (!result[slotId]) {
|
||||||
result[slotId] = [];
|
result[slotId] = [];
|
||||||
|
|
@ -150,14 +117,8 @@ export const slotsRouter = router({
|
||||||
return result;
|
return result;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Exact replica of PUT /av/products/slots/:slotId/products
|
|
||||||
updateSlotProducts: protectedProcedure
|
updateSlotProducts: protectedProcedure
|
||||||
.input(
|
.input(z.object({ slotId: z.number(), productIds: z.array(z.number()) }))
|
||||||
z.object({
|
|
||||||
slotId: z.number(),
|
|
||||||
productIds: z.array(z.number()),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
if (!ctx.staffUser?.id) {
|
if (!ctx.staffUser?.id) {
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
|
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
|
||||||
|
|
@ -172,51 +133,22 @@ export const slotsRouter = router({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current associations
|
const currentAssociations = await slotDbService.getProductSlotsBySlotId(slotId);
|
||||||
const currentAssociations = await db.query.productSlots.findMany({
|
const currentProductIds = currentAssociations.map((assoc) => assoc.productId);
|
||||||
where: eq(productSlots.slotId, slotId),
|
|
||||||
columns: {
|
|
||||||
productId: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentProductIds = currentAssociations.map(
|
|
||||||
(assoc) => assoc.productId
|
|
||||||
);
|
|
||||||
const newProductIds = productIds;
|
const newProductIds = productIds;
|
||||||
|
|
||||||
// Find products to add and remove
|
const productsToAdd = newProductIds.filter((id) => !currentProductIds.includes(id));
|
||||||
const productsToAdd = newProductIds.filter(
|
const productsToRemove = currentProductIds.filter((id) => !newProductIds.includes(id));
|
||||||
(id) => !currentProductIds.includes(id)
|
|
||||||
);
|
|
||||||
const productsToRemove = currentProductIds.filter(
|
|
||||||
(id) => !newProductIds.includes(id)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Remove associations for products that are no longer selected
|
for (const productId of productsToRemove) {
|
||||||
if (productsToRemove.length > 0) {
|
await slotDbService.deleteProductSlot(slotId, productId);
|
||||||
await db
|
|
||||||
.delete(productSlots)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(productSlots.slotId, slotId),
|
|
||||||
inArray(productSlots.productId, productsToRemove)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add associations for newly selected products
|
for (const productId of productsToAdd) {
|
||||||
if (productsToAdd.length > 0) {
|
await slotDbService.createProductSlot(slotId, productId);
|
||||||
const newAssociations = productsToAdd.map((productId) => ({
|
|
||||||
productId,
|
|
||||||
slotId,
|
|
||||||
}));
|
|
||||||
|
|
||||||
await db.insert(productSlots).values(newAssociations);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reinitialize stores to reflect changes
|
scheduleStoreInitialization();
|
||||||
scheduleStoreInitialization()
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message: "Slot products updated successfully",
|
message: "Slot products updated successfully",
|
||||||
|
|
@ -234,58 +166,43 @@ export const slotsRouter = router({
|
||||||
|
|
||||||
const { deliveryTime, freezeTime, isActive, productIds, vendorSnippets: snippets, groupIds } = input;
|
const { deliveryTime, freezeTime, isActive, productIds, vendorSnippets: snippets, groupIds } = input;
|
||||||
|
|
||||||
// Validate required fields
|
|
||||||
if (!deliveryTime || !freezeTime) {
|
if (!deliveryTime || !freezeTime) {
|
||||||
throw new ApiError("Delivery time and orders close time are required", 400);
|
throw new ApiError("Delivery time and orders close time are required", 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await db.transaction(async (tx) => {
|
const result = await slotDbService.withTransaction(async (tx) => {
|
||||||
// Create slot
|
const newSlot = await slotDbService.createSlot({
|
||||||
const [newSlot] = await tx
|
deliveryTime: new Date(deliveryTime),
|
||||||
.insert(deliverySlotInfo)
|
freezeTime: new Date(freezeTime),
|
||||||
.values({
|
isActive: isActive !== undefined ? isActive : true,
|
||||||
deliveryTime: new Date(deliveryTime),
|
groupIds: groupIds !== undefined ? groupIds : [],
|
||||||
freezeTime: new Date(freezeTime),
|
});
|
||||||
isActive: isActive !== undefined ? isActive : true,
|
|
||||||
groupIds: groupIds !== undefined ? groupIds : [],
|
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
// Insert product associations if provided
|
|
||||||
if (productIds && productIds.length > 0) {
|
if (productIds && productIds.length > 0) {
|
||||||
const associations = productIds.map((productId) => ({
|
for (const productId of productIds) {
|
||||||
productId,
|
await slotDbService.createProductSlot(newSlot.id, productId);
|
||||||
slotId: newSlot.id,
|
}
|
||||||
}));
|
|
||||||
await tx.insert(productSlots).values(associations);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create vendor snippets if provided
|
|
||||||
let createdSnippets: any[] = [];
|
let createdSnippets: any[] = [];
|
||||||
if (snippets && snippets.length > 0) {
|
if (snippets && snippets.length > 0) {
|
||||||
for (const snippet of snippets) {
|
for (const snippet of snippets) {
|
||||||
// Validate products exist
|
const productsValid = await slotDbService.validateProductsExist(snippet.productIds);
|
||||||
const products = await tx.query.productInfo.findMany({
|
if (!productsValid) {
|
||||||
where: inArray(productInfo.id, snippet.productIds),
|
|
||||||
});
|
|
||||||
if (products.length !== snippet.productIds.length) {
|
|
||||||
throw new ApiError(`One or more invalid product IDs in snippet "${snippet.name}"`, 400);
|
throw new ApiError(`One or more invalid product IDs in snippet "${snippet.name}"`, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if snippet name already exists
|
const codeExists = await slotDbService.checkSnippetCodeExists(snippet.name);
|
||||||
const existingSnippet = await tx.query.vendorSnippets.findFirst({
|
if (codeExists) {
|
||||||
where: eq(vendorSnippets.snippetCode, snippet.name),
|
|
||||||
});
|
|
||||||
if (existingSnippet) {
|
|
||||||
throw new ApiError(`Snippet name "${snippet.name}" already exists`, 400);
|
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,
|
snippetCode: snippet.name,
|
||||||
slotId: newSlot.id,
|
slotId: newSlot.id,
|
||||||
productIds: snippet.productIds,
|
productIds: snippet.productIds,
|
||||||
validTill: snippet.validTill ? new Date(snippet.validTill) : undefined,
|
validTill: snippet.validTill ? new Date(snippet.validTill) : undefined,
|
||||||
}).returning();
|
});
|
||||||
|
|
||||||
createdSnippets.push(createdSnippet);
|
createdSnippets.push(createdSnippet);
|
||||||
}
|
}
|
||||||
|
|
@ -298,8 +215,7 @@ export const slotsRouter = router({
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reinitialize stores to reflect changes (outside transaction)
|
scheduleStoreInitialization();
|
||||||
scheduleStoreInitialization()
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}),
|
}),
|
||||||
|
|
@ -309,9 +225,7 @@ export const slotsRouter = router({
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
|
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const slots = await db.query.deliverySlotInfo.findMany({
|
const slots = await slotDbService.getActiveSlots();
|
||||||
where: eq(deliverySlotInfo.isActive, true),
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
slots,
|
slots,
|
||||||
|
|
@ -328,23 +242,7 @@ export const slotsRouter = router({
|
||||||
|
|
||||||
const { id } = input;
|
const { id } = input;
|
||||||
|
|
||||||
const slot = await db.query.deliverySlotInfo.findFirst({
|
const slot = await slotDbService.getSlotById(id);
|
||||||
where: eq(deliverySlotInfo.id, id),
|
|
||||||
with: {
|
|
||||||
productSlots: {
|
|
||||||
with: {
|
|
||||||
product: {
|
|
||||||
columns: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
images: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
vendorSnippets: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!slot) {
|
if (!slot) {
|
||||||
throw new ApiError("Slot not found", 404);
|
throw new ApiError("Slot not found", 404);
|
||||||
|
|
@ -355,8 +253,8 @@ export const slotsRouter = router({
|
||||||
...slot,
|
...slot,
|
||||||
deliverySequence: slot.deliverySequence as number[],
|
deliverySequence: slot.deliverySequence as number[],
|
||||||
groupIds: slot.groupIds as number[],
|
groupIds: slot.groupIds as number[],
|
||||||
products: slot.productSlots.map((ps) => ps.product),
|
products: slot.productSlots.map((ps: any) => ps.product),
|
||||||
vendorSnippets: slot.vendorSnippets?.map(snippet => ({
|
vendorSnippets: slot.vendorSnippets?.map((snippet: any) => ({
|
||||||
...snippet,
|
...snippet,
|
||||||
accessUrl: `${appUrl}/vendor-order-list?id=${snippet.snippetCode}`
|
accessUrl: `${appUrl}/vendor-order-list?id=${snippet.snippetCode}`
|
||||||
})),
|
})),
|
||||||
|
|
@ -370,102 +268,79 @@ export const slotsRouter = router({
|
||||||
if (!ctx.staffUser?.id) {
|
if (!ctx.staffUser?.id) {
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
|
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
|
||||||
}
|
}
|
||||||
try{
|
try {
|
||||||
const { id, deliveryTime, freezeTime, isActive, productIds, vendorSnippets: snippets, groupIds } = input;
|
const { id, deliveryTime, freezeTime, isActive, productIds, vendorSnippets: snippets, groupIds } = input;
|
||||||
|
|
||||||
if (!deliveryTime || !freezeTime) {
|
if (!deliveryTime || !freezeTime) {
|
||||||
throw new ApiError("Delivery time and orders close time are required", 400);
|
throw new ApiError("Delivery time and orders close time are required", 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter groupIds to only include valid (existing) groups
|
let validGroupIds = groupIds;
|
||||||
let validGroupIds = groupIds;
|
if (groupIds && groupIds.length > 0) {
|
||||||
if (groupIds && groupIds.length > 0) {
|
const existingGroups = await slotDbService.getGroupsByIds(groupIds);
|
||||||
const existingGroups = await db.query.productGroupInfo.findMany({
|
validGroupIds = existingGroups.map((g: any) => g.id);
|
||||||
where: inArray(productGroupInfo.id, groupIds),
|
}
|
||||||
columns: { id: true },
|
|
||||||
});
|
|
||||||
validGroupIds = existingGroups.map(g => g.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await db.transaction(async (tx) => {
|
const result = await slotDbService.withTransaction(async (tx) => {
|
||||||
const [updatedSlot] = await tx
|
const updatedSlot = await slotDbService.updateSlot(id, {
|
||||||
.update(deliverySlotInfo)
|
|
||||||
.set({
|
|
||||||
deliveryTime: new Date(deliveryTime),
|
deliveryTime: new Date(deliveryTime),
|
||||||
freezeTime: new Date(freezeTime),
|
freezeTime: new Date(freezeTime),
|
||||||
isActive: isActive !== undefined ? isActive : true,
|
isActive: isActive !== undefined ? isActive : true,
|
||||||
groupIds: validGroupIds !== undefined ? validGroupIds : [],
|
groupIds: validGroupIds !== undefined ? validGroupIds : [],
|
||||||
})
|
});
|
||||||
.where(eq(deliverySlotInfo.id, id))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
if (!updatedSlot) {
|
if (!updatedSlot) {
|
||||||
throw new ApiError("Slot not found", 404);
|
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));
|
|
||||||
|
|
||||||
// Insert new associations
|
|
||||||
if (productIds.length > 0) {
|
|
||||||
const associations = productIds.map((productId) => ({
|
|
||||||
productId,
|
|
||||||
slotId: id,
|
|
||||||
}));
|
|
||||||
await tx.insert(productSlots).values(associations);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Create vendor snippets if provided
|
if (productIds !== undefined) {
|
||||||
let createdSnippets: any[] = [];
|
await slotDbService.deleteProductSlotsBySlotId(id);
|
||||||
if (snippets && snippets.length > 0) {
|
|
||||||
for (const snippet of snippets) {
|
if (productIds.length > 0) {
|
||||||
// Validate products exist
|
for (const productId of productIds) {
|
||||||
const products = await tx.query.productInfo.findMany({
|
await slotDbService.createProductSlot(id, productId);
|
||||||
where: inArray(productInfo.id, snippet.productIds),
|
}
|
||||||
});
|
|
||||||
if (products.length !== snippet.productIds.length) {
|
|
||||||
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) {
|
|
||||||
throw new ApiError(`Snippet name "${snippet.name}" already exists`, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [createdSnippet] = await tx.insert(vendorSnippets).values({
|
|
||||||
snippetCode: snippet.name,
|
|
||||||
slotId: id,
|
|
||||||
productIds: snippet.productIds,
|
|
||||||
validTill: snippet.validTill ? new Date(snippet.validTill) : undefined,
|
|
||||||
|
|
||||||
}).returning();
|
|
||||||
|
|
||||||
createdSnippets.push(createdSnippet);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
let createdSnippets: any[] = [];
|
||||||
slot: updatedSlot,
|
if (snippets && snippets.length > 0) {
|
||||||
createdSnippets,
|
for (const snippet of snippets) {
|
||||||
message: "Slot updated successfully",
|
const productsValid = await slotDbService.validateProductsExist(snippet.productIds);
|
||||||
};
|
if (!productsValid) {
|
||||||
});
|
throw new ApiError(`One or more invalid product IDs in snippet "${snippet.name}"`, 400);
|
||||||
|
}
|
||||||
|
|
||||||
// Reinitialize stores to reflect changes (outside transaction)
|
const codeExists = await slotDbService.checkSnippetCodeExists(snippet.name);
|
||||||
scheduleStoreInitialization()
|
if (codeExists) {
|
||||||
|
throw new ApiError(`Snippet name "${snippet.name}" already exists`, 400);
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
const createdSnippet = await slotDbService.createVendorSnippet({
|
||||||
}
|
snippetCode: snippet.name,
|
||||||
catch(e) {
|
slotId: id,
|
||||||
console.log(e)
|
productIds: snippet.productIds,
|
||||||
throw new ApiError("Unable to Update Slot");
|
validTill: snippet.validTill ? new Date(snippet.validTill) : undefined,
|
||||||
}
|
});
|
||||||
|
|
||||||
|
createdSnippets.push(createdSnippet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
slot: updatedSlot,
|
||||||
|
createdSnippets,
|
||||||
|
message: "Slot updated successfully",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
scheduleStoreInitialization();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
throw new ApiError("Unable to Update Slot");
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
deleteSlot: protectedProcedure
|
deleteSlot: protectedProcedure
|
||||||
|
|
@ -477,18 +352,13 @@ export const slotsRouter = router({
|
||||||
|
|
||||||
const { id } = input;
|
const { id } = input;
|
||||||
|
|
||||||
const [deletedSlot] = await db
|
const deletedSlot = await slotDbService.deactivateSlot(id);
|
||||||
.update(deliverySlotInfo)
|
|
||||||
.set({ isActive: false })
|
|
||||||
.where(eq(deliverySlotInfo.id, id))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
if (!deletedSlot) {
|
if (!deletedSlot) {
|
||||||
throw new ApiError("Slot not found", 404);
|
throw new ApiError("Slot not found", 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reinitialize stores to reflect changes
|
scheduleStoreInitialization();
|
||||||
scheduleStoreInitialization()
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message: "Slot deleted successfully",
|
message: "Slot deleted successfully",
|
||||||
|
|
@ -497,8 +367,7 @@ export const slotsRouter = router({
|
||||||
|
|
||||||
getDeliverySequence: protectedProcedure
|
getDeliverySequence: protectedProcedure
|
||||||
.input(getDeliverySequenceSchema)
|
.input(getDeliverySequenceSchema)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input }) => {
|
||||||
|
|
||||||
const { id } = input;
|
const { id } = input;
|
||||||
const slotId = parseInt(id);
|
const slotId = parseInt(id);
|
||||||
const cacheKey = getSlotSequenceKey(slotId);
|
const cacheKey = getSlotSequenceKey(slotId);
|
||||||
|
|
@ -508,19 +377,14 @@ export const slotsRouter = router({
|
||||||
if (cached) {
|
if (cached) {
|
||||||
const parsed = JSON.parse(cached);
|
const parsed = JSON.parse(cached);
|
||||||
const validated = cachedSequenceSchema.parse(parsed) as CachedDeliverySequence;
|
const validated = cachedSequenceSchema.parse(parsed) as CachedDeliverySequence;
|
||||||
console.log('sending cached response')
|
console.log('sending cached response');
|
||||||
|
|
||||||
return { deliverySequence: validated };
|
return { deliverySequence: validated };
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Redis cache read/validation failed, falling back to DB:', error);
|
console.warn('Redis cache read/validation failed, falling back to DB:', error);
|
||||||
// Continue to DB fallback
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to DB
|
const slot = await slotDbService.getSlotById(slotId);
|
||||||
const slot = await db.query.deliverySlotInfo.findFirst({
|
|
||||||
where: eq(deliverySlotInfo.id, slotId),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!slot) {
|
if (!slot) {
|
||||||
throw new ApiError("Slot not found", 404);
|
throw new ApiError("Slot not found", 404);
|
||||||
|
|
@ -528,7 +392,6 @@ export const slotsRouter = router({
|
||||||
|
|
||||||
const sequence = (slot.deliverySequence || {}) as CachedDeliverySequence;
|
const sequence = (slot.deliverySequence || {}) as CachedDeliverySequence;
|
||||||
|
|
||||||
// Cache the validated result
|
|
||||||
try {
|
try {
|
||||||
const validated = cachedSequenceSchema.parse(sequence);
|
const validated = cachedSequenceSchema.parse(sequence);
|
||||||
await redisClient.set(cacheKey, JSON.stringify(validated), 3600);
|
await redisClient.set(cacheKey, JSON.stringify(validated), 3600);
|
||||||
|
|
@ -548,20 +411,12 @@ export const slotsRouter = router({
|
||||||
|
|
||||||
const { id, deliverySequence } = input;
|
const { id, deliverySequence } = input;
|
||||||
|
|
||||||
const [updatedSlot] = await db
|
const updatedSlot = await slotDbService.updateSlot(id, { deliverySequence });
|
||||||
.update(deliverySlotInfo)
|
|
||||||
.set({ deliverySequence })
|
|
||||||
.where(eq(deliverySlotInfo.id, id))
|
|
||||||
.returning({
|
|
||||||
id: deliverySlotInfo.id,
|
|
||||||
deliverySequence: deliverySlotInfo.deliverySequence,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!updatedSlot) {
|
if (!updatedSlot) {
|
||||||
throw new ApiError("Slot not found", 404);
|
throw new ApiError("Slot not found", 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache the updated sequence
|
|
||||||
const cacheKey = getSlotSequenceKey(id);
|
const cacheKey = getSlotSequenceKey(id);
|
||||||
try {
|
try {
|
||||||
const validated = cachedSequenceSchema.parse(deliverySequence);
|
const validated = cachedSequenceSchema.parse(deliverySequence);
|
||||||
|
|
@ -571,7 +426,7 @@ export const slotsRouter = router({
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
slot: updatedSlot,
|
slot: { id: updatedSlot.id, deliverySequence: updatedSlot.deliverySequence },
|
||||||
message: "Delivery sequence updated successfully",
|
message: "Delivery sequence updated successfully",
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
@ -588,18 +443,13 @@ export const slotsRouter = router({
|
||||||
|
|
||||||
const { slotId, isCapacityFull } = input;
|
const { slotId, isCapacityFull } = input;
|
||||||
|
|
||||||
const [updatedSlot] = await db
|
const updatedSlot = await slotDbService.updateSlot(slotId, { isCapacityFull });
|
||||||
.update(deliverySlotInfo)
|
|
||||||
.set({ isCapacityFull })
|
|
||||||
.where(eq(deliverySlotInfo.id, slotId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
if (!updatedSlot) {
|
if (!updatedSlot) {
|
||||||
throw new ApiError("Slot not found", 404);
|
throw new ApiError("Slot not found", 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reinitialize stores to reflect changes
|
scheduleStoreInitialization();
|
||||||
scheduleStoreInitialization()
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index'
|
import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index'
|
||||||
import { z } from 'zod';
|
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 bcrypt from 'bcryptjs';
|
||||||
import { ApiError } from '@/src/lib/api-error'
|
import { ApiError } from '@/src/lib/api-error'
|
||||||
import { signToken } from '@/src/lib/jwt-utils'
|
import { signToken } from '@/src/lib/jwt-utils'
|
||||||
|
import { staffUserDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/main'
|
||||||
|
|
||||||
export const staffUserRouter = router({
|
export const staffUserRouter = router({
|
||||||
login: publicProcedure
|
login: publicProcedure
|
||||||
|
|
@ -20,9 +18,7 @@ export const staffUserRouter = router({
|
||||||
throw new ApiError('Name and password are required', 400);
|
throw new ApiError('Name and password are required', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const staff = await db.query.staffUsers.findFirst({
|
const staff = await staffUserDbService.getStaffUserByName(name);
|
||||||
where: eq(staffUsers.name, name),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!staff) {
|
if (!staff) {
|
||||||
throw new ApiError('Invalid credentials', 401);
|
throw new ApiError('Invalid credentials', 401);
|
||||||
|
|
@ -46,24 +42,8 @@ export const staffUserRouter = router({
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getStaff: protectedProcedure
|
getStaff: protectedProcedure
|
||||||
.query(async ({ ctx }) => {
|
.query(async () => {
|
||||||
const staff = await db.query.staffUsers.findMany({
|
const staff = await staffUserDbService.getAllStaff();
|
||||||
columns: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
with: {
|
|
||||||
role: {
|
|
||||||
with: {
|
|
||||||
rolePermissions: {
|
|
||||||
with: {
|
|
||||||
permission: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Transform the data to include role and permissions in a cleaner format
|
// Transform the data to include role and permissions in a cleaner format
|
||||||
const transformedStaff = staff.map((user) => ({
|
const transformedStaff = staff.map((user) => ({
|
||||||
|
|
@ -93,29 +73,7 @@ export const staffUserRouter = router({
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const { cursor, limit, search } = input;
|
const { cursor, limit, search } = input;
|
||||||
|
|
||||||
let whereCondition = undefined;
|
const allUsers = await staffUserDbService.getUsers({ cursor, limit, search });
|
||||||
|
|
||||||
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 hasMore = allUsers.length > limit;
|
const hasMore = allUsers.length > limit;
|
||||||
const usersToReturn = hasMore ? allUsers.slice(0, limit) : allUsers;
|
const usersToReturn = hasMore ? allUsers.slice(0, limit) : allUsers;
|
||||||
|
|
@ -139,22 +97,13 @@ export const staffUserRouter = router({
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const { userId } = input;
|
const { userId } = input;
|
||||||
|
|
||||||
const user = await db.query.users.findFirst({
|
const user = await staffUserDbService.getUserById(userId);
|
||||||
where: eq(users.id, userId),
|
|
||||||
with: {
|
|
||||||
userDetails: true,
|
|
||||||
orders: {
|
|
||||||
orderBy: desc(orders.createdAt),
|
|
||||||
limit: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new ApiError("User not found", 404);
|
throw new ApiError("User not found", 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastOrder = user.orders[0];
|
const lastOrder = user.orders?.[0];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
|
|
@ -172,13 +121,7 @@ export const staffUserRouter = router({
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const { userId, isSuspended } = input;
|
const { userId, isSuspended } = input;
|
||||||
|
|
||||||
await db
|
await staffUserDbService.upsertUserDetails({ userId, isSuspended });
|
||||||
.insert(userDetails)
|
|
||||||
.values({ userId, isSuspended })
|
|
||||||
.onConflictDoUpdate({
|
|
||||||
target: userDetails.userId,
|
|
||||||
set: { isSuspended },
|
|
||||||
});
|
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}),
|
}),
|
||||||
|
|
@ -189,22 +132,18 @@ export const staffUserRouter = router({
|
||||||
password: z.string().min(6, 'Password must be at least 6 characters'),
|
password: z.string().min(6, 'Password must be at least 6 characters'),
|
||||||
roleId: z.number().int().positive('Role is required'),
|
roleId: z.number().int().positive('Role is required'),
|
||||||
}))
|
}))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input }) => {
|
||||||
const { name, password, roleId } = input;
|
const { name, password, roleId } = input;
|
||||||
|
|
||||||
// Check if staff user already exists
|
// Check if staff user already exists
|
||||||
const existingUser = await db.query.staffUsers.findFirst({
|
const existingUser = await staffUserDbService.getStaffUserByName(name);
|
||||||
where: eq(staffUsers.name, name),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
throw new ApiError('Staff user with this name already exists', 409);
|
throw new ApiError('Staff user with this name already exists', 409);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if role exists
|
// Check if role exists
|
||||||
const role = await db.query.staffRoles.findFirst({
|
const role = await staffUserDbService.getRoleById(roleId);
|
||||||
where: eq(staffRoles.id, roleId),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!role) {
|
if (!role) {
|
||||||
throw new ApiError('Invalid role selected', 400);
|
throw new ApiError('Invalid role selected', 400);
|
||||||
|
|
@ -214,23 +153,18 @@ export const staffUserRouter = router({
|
||||||
const hashedPassword = await bcrypt.hash(password, 12);
|
const hashedPassword = await bcrypt.hash(password, 12);
|
||||||
|
|
||||||
// Create staff user
|
// Create staff user
|
||||||
const [newUser] = await db.insert(staffUsers).values({
|
const newUser = await staffUserDbService.createStaffUser({
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
staffRoleId: roleId,
|
staffRoleId: roleId,
|
||||||
}).returning();
|
});
|
||||||
|
|
||||||
return { success: true, user: { id: newUser.id, name: newUser.name } };
|
return { success: true, user: { id: newUser.id, name: newUser.name } };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getRoles: protectedProcedure
|
getRoles: protectedProcedure
|
||||||
.query(async ({ ctx }) => {
|
.query(async () => {
|
||||||
const roles = await db.query.staffRoles.findMany({
|
const roles = await staffUserDbService.getAllRoles();
|
||||||
columns: {
|
|
||||||
id: true,
|
|
||||||
roleName: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
roles: roles.map(role => ({
|
roles: roles.map(role => ({
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,22 @@
|
||||||
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
|
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
|
||||||
import { z } from 'zod';
|
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 { ApiError } from '@/src/lib/api-error'
|
||||||
import { extractKeyFromPresignedUrl, deleteImageUtil, scaffoldAssetUrl } from '@/src/lib/s3-client'
|
import { extractKeyFromPresignedUrl, deleteImageUtil, scaffoldAssetUrl } from '@/src/lib/s3-client'
|
||||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
||||||
import { scheduleStoreInitialization } from '@/src/stores/store-initializer'
|
import { scheduleStoreInitialization } from '@/src/stores/store-initializer'
|
||||||
|
import { storeDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/main'
|
||||||
|
|
||||||
export const storeRouter = router({
|
export const storeRouter = router({
|
||||||
getStores: protectedProcedure
|
getStores: protectedProcedure
|
||||||
.query(async ({ ctx }) => {
|
.query(async () => {
|
||||||
const stores = await db.query.storeInfo.findMany({
|
const stores = await storeDbService.getAllStores();
|
||||||
with: {
|
|
||||||
owner: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
Promise.all(stores.map(async store => {
|
Promise.all(stores.map(async store => {
|
||||||
if(store.imageUrl)
|
if(store.imageUrl)
|
||||||
store.imageUrl = scaffoldAssetUrl(store.imageUrl)
|
store.imageUrl = scaffoldAssetUrl(store.imageUrl)
|
||||||
})).catch((e) => {
|
})).catch((e) => {
|
||||||
throw new ApiError("Unable to find store image urls")
|
throw new ApiError("Unable to find store image urls")
|
||||||
}
|
})
|
||||||
)
|
|
||||||
return {
|
return {
|
||||||
stores,
|
stores,
|
||||||
count: stores.length,
|
count: stores.length,
|
||||||
|
|
@ -35,58 +27,45 @@ export const storeRouter = router({
|
||||||
.input(z.object({
|
.input(z.object({
|
||||||
id: z.number(),
|
id: z.number(),
|
||||||
}))
|
}))
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input }) => {
|
||||||
const { id } = input;
|
const { id } = input;
|
||||||
|
|
||||||
const store = await db.query.storeInfo.findFirst({
|
const store = await storeDbService.getStoreById(id);
|
||||||
where: eq(storeInfo.id, id),
|
|
||||||
with: {
|
|
||||||
owner: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!store) {
|
if (!store) {
|
||||||
throw new ApiError("Store not found", 404);
|
throw new ApiError("Store not found", 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
store.imageUrl = scaffoldAssetUrl(store.imageUrl);
|
store.imageUrl = scaffoldAssetUrl(store.imageUrl);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
store,
|
store,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
createStore: protectedProcedure
|
createStore: protectedProcedure
|
||||||
.input(z.object({
|
.input(z.object({
|
||||||
name: z.string().min(1, "Name is required"),
|
name: z.string().min(1, "Name is required"),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
imageUrl: z.string().optional(),
|
imageUrl: z.string().optional(),
|
||||||
owner: z.number().min(1, "Owner is required"),
|
owner: z.number().min(1, "Owner is required"),
|
||||||
products: z.array(z.number()).optional(),
|
products: z.array(z.number()).optional(),
|
||||||
}))
|
}))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input }) => {
|
||||||
const { name, description, imageUrl, owner, products } = input;
|
const { name, description, imageUrl, owner, products } = input;
|
||||||
|
|
||||||
// const imageKey = imageUrl ? extractKeyFromPresignedUrl(imageUrl) : undefined;
|
const newStore = await storeDbService.createStore({
|
||||||
const imageKey = imageUrl
|
name,
|
||||||
|
description,
|
||||||
const [newStore] = await db
|
imageUrl: imageUrl || null,
|
||||||
.insert(storeInfo)
|
owner,
|
||||||
.values({
|
});
|
||||||
name,
|
|
||||||
description,
|
|
||||||
imageUrl: imageKey,
|
|
||||||
owner,
|
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
// Assign selected products to this store
|
// Assign selected products to this store
|
||||||
if (products && products.length > 0) {
|
if (products && products.length > 0) {
|
||||||
await db
|
await storeDbService.assignProductsToStore(newStore.id, products);
|
||||||
.update(productInfo)
|
|
||||||
.set({ storeId: newStore.id })
|
|
||||||
.where(inArray(productInfo.id, products));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reinitialize stores to reflect changes
|
|
||||||
scheduleStoreInitialization()
|
scheduleStoreInitialization()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -95,117 +74,84 @@ export const storeRouter = router({
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
updateStore: protectedProcedure
|
updateStore: protectedProcedure
|
||||||
.input(z.object({
|
.input(z.object({
|
||||||
id: z.number(),
|
id: z.number(),
|
||||||
name: z.string().min(1, "Name is required"),
|
name: z.string().min(1, "Name is required"),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
imageUrl: z.string().optional(),
|
imageUrl: z.string().optional(),
|
||||||
owner: z.number().min(1, "Owner is required"),
|
owner: z.number().min(1, "Owner is required"),
|
||||||
products: z.array(z.number()).optional(),
|
products: z.array(z.number()).optional(),
|
||||||
}))
|
}))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input }) => {
|
||||||
const { id, name, description, imageUrl, owner, products } = input;
|
const { id, name, description, imageUrl, owner, products } = input;
|
||||||
|
|
||||||
const existingStore = await db.query.storeInfo.findFirst({
|
const existingStore = await storeDbService.getStoreById(id);
|
||||||
where: eq(storeInfo.id, id),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!existingStore) {
|
if (!existingStore) {
|
||||||
throw new ApiError("Store not found", 404);
|
throw new ApiError("Store not found", 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldImageKey = existingStore.imageUrl;
|
const oldImageKey = existingStore.imageUrl;
|
||||||
const newImageKey = imageUrl ? extractKeyFromPresignedUrl(imageUrl) : oldImageKey;
|
const newImageKey = imageUrl ? extractKeyFromPresignedUrl(imageUrl) : oldImageKey;
|
||||||
|
|
||||||
// Delete old image only if:
|
// Delete old image only if:
|
||||||
// 1. New image provided and keys are different, OR
|
// 1. New image provided and keys are different, OR
|
||||||
// 2. No new image but old exists (clearing the image)
|
// 2. No new image but old exists (clearing the image)
|
||||||
if (oldImageKey && (
|
if (oldImageKey && (
|
||||||
(newImageKey && newImageKey !== oldImageKey) ||
|
(newImageKey && newImageKey !== oldImageKey) ||
|
||||||
(!newImageKey)
|
(!newImageKey)
|
||||||
)) {
|
)) {
|
||||||
try {
|
try {
|
||||||
await deleteImageUtil({keys: [oldImageKey]});
|
await deleteImageUtil({keys: [oldImageKey]});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to delete old image:', error);
|
console.error('Failed to delete old image:', error);
|
||||||
// Continue with update even if deletion fails
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const [updatedStore] = await db
|
const updatedStore = await storeDbService.updateStore(id, {
|
||||||
.update(storeInfo)
|
name,
|
||||||
.set({
|
description,
|
||||||
name,
|
imageUrl: newImageKey,
|
||||||
description,
|
owner,
|
||||||
imageUrl: newImageKey,
|
});
|
||||||
owner,
|
|
||||||
})
|
|
||||||
.where(eq(storeInfo.id, id))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
if (!updatedStore) {
|
// Update products if provided
|
||||||
throw new ApiError("Store not found", 404);
|
if (products) {
|
||||||
}
|
// First, remove all products from this store
|
||||||
|
await storeDbService.removeProductsFromStore(id);
|
||||||
|
|
||||||
// Update products if provided
|
// Then, assign the selected products to this store
|
||||||
if (products) {
|
if (products.length > 0) {
|
||||||
// First, set storeId to null for products not in the list but currently assigned to this store
|
await storeDbService.assignProductsToStore(id, products);
|
||||||
await db
|
}
|
||||||
.update(productInfo)
|
}
|
||||||
.set({ storeId: null })
|
|
||||||
.where(eq(productInfo.storeId, id));
|
|
||||||
|
|
||||||
// Then, assign the selected products to this store
|
scheduleStoreInitialization()
|
||||||
if (products.length > 0) {
|
|
||||||
await db
|
|
||||||
.update(productInfo)
|
|
||||||
.set({ storeId: id })
|
|
||||||
.where(inArray(productInfo.id, products));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reinitialize stores to reflect changes
|
return {
|
||||||
scheduleStoreInitialization()
|
store: updatedStore,
|
||||||
|
message: "Store updated successfully",
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
|
||||||
return {
|
deleteStore: protectedProcedure
|
||||||
store: updatedStore,
|
.input(z.object({
|
||||||
message: "Store updated successfully",
|
storeId: z.number(),
|
||||||
};
|
}))
|
||||||
}),
|
.mutation(async ({ input }) => {
|
||||||
|
const { storeId } = input;
|
||||||
|
|
||||||
deleteStore: protectedProcedure
|
// First, remove all products from this store
|
||||||
.input(z.object({
|
await storeDbService.removeProductsFromStore(storeId);
|
||||||
storeId: z.number(),
|
|
||||||
}))
|
|
||||||
.mutation(async ({ input, ctx }) => {
|
|
||||||
const { storeId } = input;
|
|
||||||
|
|
||||||
const result = await db.transaction(async (tx) => {
|
// Then delete the store
|
||||||
// First, update all products of this store to set storeId to null
|
await storeDbService.deleteStore(storeId);
|
||||||
await tx
|
|
||||||
.update(productInfo)
|
|
||||||
.set({ storeId: null })
|
|
||||||
.where(eq(productInfo.storeId, storeId));
|
|
||||||
|
|
||||||
// Then delete the store
|
scheduleStoreInitialization()
|
||||||
const [deletedStore] = await tx
|
|
||||||
.delete(storeInfo)
|
|
||||||
.where(eq(storeInfo.id, storeId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
if (!deletedStore) {
|
return {
|
||||||
throw new ApiError("Store not found", 404);
|
message: "Store deleted successfully",
|
||||||
}
|
};
|
||||||
|
}),
|
||||||
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 { router, protectedProcedure } from '@/src/trpc/trpc-index'
|
||||||
import { z } from 'zod';
|
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 { ApiError } from '@/src/lib/api-error'
|
||||||
import { scaffoldAssetUrl, claimUploadUrl } from '@/src/lib/s3-client'
|
import { scaffoldAssetUrl, claimUploadUrl } from '@/src/lib/s3-client'
|
||||||
import { deleteS3Image } from '@/src/lib/delete-image'
|
import { deleteS3Image } from '@/src/lib/delete-image'
|
||||||
import { scheduleStoreInitialization } from '@/src/stores/store-initializer'
|
import { scheduleStoreInitialization } from '@/src/stores/store-initializer'
|
||||||
|
import { tagDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/main'
|
||||||
|
|
||||||
export const tagRouter = router({
|
export const tagRouter = router({
|
||||||
getTags: protectedProcedure
|
getTags: protectedProcedure
|
||||||
.query(async () => {
|
.query(async () => {
|
||||||
const tags = await db
|
const tags = await tagDbService.getAllTags();
|
||||||
.select()
|
|
||||||
.from(productTagInfo)
|
|
||||||
.orderBy(productTagInfo.tagName);
|
|
||||||
|
|
||||||
// Generate asset URLs for tag images
|
// Generate asset URLs for tag images
|
||||||
const tagsWithUrls = tags.map(tag => ({
|
const tagsWithUrls = tags.map(tag => ({
|
||||||
|
|
@ -33,9 +28,7 @@ export const tagRouter = router({
|
||||||
id: z.number(),
|
id: z.number(),
|
||||||
}))
|
}))
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const tag = await db.query.productTagInfo.findFirst({
|
const tag = await tagDbService.getTagById(input.id);
|
||||||
where: eq(productTagInfo.id, input.id),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!tag) {
|
if (!tag) {
|
||||||
throw new ApiError("Tag not found", 404);
|
throw new ApiError("Tag not found", 404);
|
||||||
|
|
@ -65,24 +58,19 @@ export const tagRouter = router({
|
||||||
const { tagName, tagDescription, isDashboardTag, relatedStores, imageKey } = input;
|
const { tagName, tagDescription, isDashboardTag, relatedStores, imageKey } = input;
|
||||||
|
|
||||||
// Check for duplicate tag name
|
// Check for duplicate tag name
|
||||||
const existingTag = await db.query.productTagInfo.findFirst({
|
const existingTag = await tagDbService.getTagByName(tagName);
|
||||||
where: eq(productTagInfo.tagName, tagName.trim()),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingTag) {
|
if (existingTag) {
|
||||||
throw new ApiError("A tag with this name already exists", 400);
|
throw new ApiError("A tag with this name already exists", 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [newTag] = await db
|
const newTag = await tagDbService.createTag({
|
||||||
.insert(productTagInfo)
|
tagName: tagName.trim(),
|
||||||
.values({
|
tagDescription,
|
||||||
tagName: tagName.trim(),
|
imageUrl: imageKey || null,
|
||||||
tagDescription,
|
isDashboardTag,
|
||||||
imageUrl: imageKey || null,
|
relatedStores,
|
||||||
isDashboardTag,
|
});
|
||||||
relatedStores,
|
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
// Claim upload URL if image was provided
|
// Claim upload URL if image was provided
|
||||||
if (imageKey) {
|
if (imageKey) {
|
||||||
|
|
@ -115,9 +103,7 @@ export const tagRouter = router({
|
||||||
const { id, imageKey, deleteExistingImage, ...updateData } = input;
|
const { id, imageKey, deleteExistingImage, ...updateData } = input;
|
||||||
|
|
||||||
// Get current tag
|
// Get current tag
|
||||||
const currentTag = await db.query.productTagInfo.findFirst({
|
const currentTag = await tagDbService.getTagById(id);
|
||||||
where: eq(productTagInfo.id, id),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!currentTag) {
|
if (!currentTag) {
|
||||||
throw new ApiError("Tag not found", 404);
|
throw new ApiError("Tag not found", 404);
|
||||||
|
|
@ -155,17 +141,13 @@ export const tagRouter = router({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [updatedTag] = await db
|
const updatedTag = await tagDbService.updateTag(id, {
|
||||||
.update(productTagInfo)
|
tagName: updateData.tagName.trim(),
|
||||||
.set({
|
tagDescription: updateData.tagDescription,
|
||||||
tagName: updateData.tagName.trim(),
|
isDashboardTag: updateData.isDashboardTag,
|
||||||
tagDescription: updateData.tagDescription,
|
relatedStores: updateData.relatedStores,
|
||||||
isDashboardTag: updateData.isDashboardTag,
|
imageUrl: newImageUrl,
|
||||||
relatedStores: updateData.relatedStores,
|
});
|
||||||
imageUrl: newImageUrl,
|
|
||||||
})
|
|
||||||
.where(eq(productTagInfo.id, id))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
scheduleStoreInitialization();
|
scheduleStoreInitialization();
|
||||||
|
|
||||||
|
|
@ -183,9 +165,7 @@ export const tagRouter = router({
|
||||||
const { id } = input;
|
const { id } = input;
|
||||||
|
|
||||||
// Get tag to check for image
|
// Get tag to check for image
|
||||||
const tag = await db.query.productTagInfo.findFirst({
|
const tag = await tagDbService.getTagById(id);
|
||||||
where: eq(productTagInfo.id, id),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!tag) {
|
if (!tag) {
|
||||||
throw new ApiError("Tag not found", 404);
|
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)
|
// 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();
|
scheduleStoreInitialization();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,28 @@
|
||||||
import { protectedProcedure } from '@/src/trpc/trpc-index';
|
import { protectedProcedure } from '@/src/trpc/trpc-index';
|
||||||
import { z } from 'zod';
|
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 { ApiError } from '@/src/lib/api-error';
|
||||||
import { notificationQueue } from '@/src/lib/notif-job';
|
import { notificationQueue } from '@/src/lib/notif-job';
|
||||||
import { recomputeUserNegativityScore } from '@/src/stores/user-negativity-store';
|
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> {
|
async function createUserByMobile(mobile: string) {
|
||||||
// Clean mobile number (remove non-digits)
|
|
||||||
const cleanMobile = mobile.replace(/\D/g, '');
|
const cleanMobile = mobile.replace(/\D/g, '');
|
||||||
|
|
||||||
// Validate: exactly 10 digits
|
|
||||||
if (cleanMobile.length !== 10) {
|
if (cleanMobile.length !== 10) {
|
||||||
throw new ApiError('Mobile number must be exactly 10 digits', 400);
|
throw new ApiError('Mobile number must be exactly 10 digits', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user already exists
|
const existingUser = await userDbService.getUserByMobile(cleanMobile);
|
||||||
const [existingUser] = await db
|
|
||||||
.select()
|
|
||||||
.from(users)
|
|
||||||
.where(eq(users.mobile, cleanMobile))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
throw new ApiError('User with this mobile number already exists', 409);
|
throw new ApiError('User with this mobile number already exists', 409);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create user
|
const newUser = await userDbService.createUser({
|
||||||
const [newUser] = await db
|
name: null,
|
||||||
.insert(users)
|
email: null,
|
||||||
.values({
|
mobile: cleanMobile,
|
||||||
name: null,
|
});
|
||||||
email: null,
|
|
||||||
mobile: cleanMobile,
|
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
return newUser;
|
return newUser;
|
||||||
}
|
}
|
||||||
|
|
@ -56,7 +43,7 @@ export const userRouter = {
|
||||||
|
|
||||||
getEssentials: protectedProcedure
|
getEssentials: protectedProcedure
|
||||||
.query(async () => {
|
.query(async () => {
|
||||||
const count = await db.$count(complaints, eq(complaints.isResolved, false));
|
const count = await userDbService.getUnresolvedComplaintCount();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
unresolvedComplaints: count || 0,
|
unresolvedComplaints: count || 0,
|
||||||
|
|
@ -72,78 +59,23 @@ export const userRouter = {
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const { limit, cursor, search } = input;
|
const { limit, cursor, search } = input;
|
||||||
|
|
||||||
// Build where conditions
|
const usersList = await userDbService.getUsers({ limit, cursor, search });
|
||||||
const whereConditions = [];
|
|
||||||
|
|
||||||
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 hasMore = usersList.length > limit;
|
||||||
const usersToReturn = hasMore ? usersList.slice(0, limit) : usersList;
|
const usersToReturn = hasMore ? usersList.slice(0, limit) : usersList;
|
||||||
|
|
||||||
// Get order stats for each user
|
|
||||||
const userIds = usersToReturn.map(u => u.id);
|
const userIds = usersToReturn.map(u => u.id);
|
||||||
|
|
||||||
let orderCounts: { userId: number; totalOrders: number }[] = [];
|
const orderCounts = await userDbService.getOrderCountByUserIds(userIds);
|
||||||
let lastOrders: { userId: number; lastOrderDate: Date | null }[] = [];
|
const lastOrders = await userDbService.getLastOrderDateByUserIds(userIds);
|
||||||
let suspensionStatuses: { userId: number; isSuspended: boolean }[] = [];
|
|
||||||
|
|
||||||
if (userIds.length > 0) {
|
const userDetailsList = await Promise.all(
|
||||||
// Get total orders per user
|
userIds.map(id => userDbService.getUserDetailsByUserId(id))
|
||||||
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);
|
|
||||||
|
|
||||||
// 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 orderCountMap = new Map(orderCounts.map(o => [o.userId, o.totalOrders]));
|
||||||
const lastOrderMap = new Map(lastOrders.map(o => [o.userId, o.lastOrderDate]));
|
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 => ({
|
const usersWithStats = usersToReturn.map(user => ({
|
||||||
...user,
|
...user,
|
||||||
totalOrders: orderCountMap.get(user.id) || 0,
|
totalOrders: orderCountMap.get(user.id) || 0,
|
||||||
|
|
@ -151,7 +83,6 @@ export const userRouter = {
|
||||||
isSuspended: suspensionMap.get(user.id) ?? false,
|
isSuspended: suspensionMap.get(user.id) ?? false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Get next cursor
|
|
||||||
const nextCursor = hasMore ? usersToReturn[usersToReturn.length - 1].id : undefined;
|
const nextCursor = hasMore ? usersToReturn[usersToReturn.length - 1].id : undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -168,76 +99,22 @@ export const userRouter = {
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const { userId } = input;
|
const { userId } = input;
|
||||||
|
|
||||||
// Get user info
|
const user = await userDbService.getUserById(userId);
|
||||||
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);
|
|
||||||
|
|
||||||
if (!user || user.length === 0) {
|
if (!user) {
|
||||||
throw new ApiError('User not found', 404);
|
throw new ApiError('User not found', 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user suspension status
|
const userDetail = await userDbService.getUserDetailsByUserId(userId);
|
||||||
const userDetail = await db
|
const userOrders = await userDbService.getOrdersByUserId(userId);
|
||||||
.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 orderIds = userOrders.map(o => o.id);
|
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 statusMap = new Map(orderStatuses.map(s => [s.orderId, s]));
|
||||||
const itemCountMap = new Map(itemCounts.map(c => [c.orderId, c.itemCount]));
|
const itemCountMap = new Map(itemCounts.map(c => [c.orderId, c.itemCount]));
|
||||||
|
|
||||||
// Determine status string
|
|
||||||
const getStatus = (status: { isDelivered: boolean; isCancelled: boolean } | undefined) => {
|
const getStatus = (status: { isDelivered: boolean; isCancelled: boolean } | undefined) => {
|
||||||
if (!status) return 'pending';
|
if (!status) return 'pending';
|
||||||
if (status.isCancelled) return 'cancelled';
|
if (status.isCancelled) return 'cancelled';
|
||||||
|
|
@ -245,15 +122,14 @@ export const userRouter = {
|
||||||
return 'pending';
|
return 'pending';
|
||||||
};
|
};
|
||||||
|
|
||||||
// Combine data
|
|
||||||
const ordersWithDetails = userOrders.map(order => {
|
const ordersWithDetails = userOrders.map(order => {
|
||||||
const status = statusMap.get(order.id);
|
const status = statusMap.get(order.id);
|
||||||
return {
|
return {
|
||||||
id: order.id,
|
id: order.id,
|
||||||
readableId: order.readableId,
|
readableId: (order as any).readableId,
|
||||||
totalAmount: order.totalAmount,
|
totalAmount: order.totalAmount,
|
||||||
createdAt: order.createdAt,
|
createdAt: order.createdAt,
|
||||||
isFlashDelivery: order.isFlashDelivery,
|
isFlashDelivery: (order as any).isFlashDelivery,
|
||||||
status: getStatus(status),
|
status: getStatus(status),
|
||||||
itemCount: itemCountMap.get(order.id) || 0,
|
itemCount: itemCountMap.get(order.id) || 0,
|
||||||
};
|
};
|
||||||
|
|
@ -261,8 +137,8 @@ export const userRouter = {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user: {
|
user: {
|
||||||
...user[0],
|
...user,
|
||||||
isSuspended: userDetail[0]?.isSuspended ?? false,
|
isSuspended: userDetail?.isSuspended ?? false,
|
||||||
},
|
},
|
||||||
orders: ordersWithDetails,
|
orders: ordersWithDetails,
|
||||||
};
|
};
|
||||||
|
|
@ -276,39 +152,13 @@ export const userRouter = {
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const { userId, isSuspended } = input;
|
const { userId, isSuspended } = input;
|
||||||
|
|
||||||
// Check if user exists
|
const user = await userDbService.getUserById(userId);
|
||||||
const user = await db
|
|
||||||
.select({ id: users.id })
|
|
||||||
.from(users)
|
|
||||||
.where(eq(users.id, userId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!user || user.length === 0) {
|
if (!user) {
|
||||||
throw new ApiError('User not found', 404);
|
throw new ApiError('User not found', 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user_details record exists
|
await userDbService.upsertUserDetails({ userId, isSuspended });
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|
@ -323,40 +173,17 @@ export const userRouter = {
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const { search } = input;
|
const { search } = input;
|
||||||
|
|
||||||
// Get all users
|
const usersList = await userDbService.getUsers({ limit: 1000, search });
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get eligible users (have notif_creds entry)
|
const allTokens = await userDbService.getAllNotifTokens();
|
||||||
const eligibleUsers = await db
|
const eligibleSet = new Set(allTokens);
|
||||||
.select({ userId: notifCreds.userId })
|
|
||||||
.from(notifCreds);
|
|
||||||
|
|
||||||
const eligibleSet = new Set(eligibleUsers.map(u => u.userId));
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
users: usersList.map(user => ({
|
users: usersList.map(user => ({
|
||||||
id: user.id,
|
id: user.id,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
mobile: user.mobile,
|
mobile: user.mobile,
|
||||||
isEligibleForNotif: eligibleSet.has(user.id),
|
isEligibleForNotif: eligibleSet.has(user.mobile || ''),
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
@ -374,25 +201,13 @@ export const userRouter = {
|
||||||
let tokens: string[] = [];
|
let tokens: string[] = [];
|
||||||
|
|
||||||
if (userIds.length === 0) {
|
if (userIds.length === 0) {
|
||||||
// Send to all users - get tokens from both logged-in and unlogged users
|
const allTokens = await userDbService.getAllNotifTokens();
|
||||||
const loggedInTokens = await db.select({ token: notifCreds.token }).from(notifCreds);
|
const unloggedTokens = await userDbService.getUnloggedTokens();
|
||||||
const unloggedTokens = await db.select({ token: unloggedUserTokens.token }).from(unloggedUserTokens);
|
tokens = [...allTokens, ...unloggedTokens];
|
||||||
|
|
||||||
tokens = [
|
|
||||||
...loggedInTokens.map(t => t.token),
|
|
||||||
...unloggedTokens.map(t => t.token)
|
|
||||||
];
|
|
||||||
} else {
|
} else {
|
||||||
// Send to specific users - get their tokens
|
tokens = await userDbService.getNotifTokensByUserIds(userIds);
|
||||||
const userTokens = await db
|
|
||||||
.select({ token: notifCreds.token })
|
|
||||||
.from(notifCreds)
|
|
||||||
.where(inArray(notifCreds.userId, userIds));
|
|
||||||
|
|
||||||
tokens = userTokens.map(t => t.token);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Queue one job per token
|
|
||||||
let queuedCount = 0;
|
let queuedCount = 0;
|
||||||
for (const token of tokens) {
|
for (const token of tokens) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -427,18 +242,7 @@ export const userRouter = {
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const { userId } = input;
|
const { userId } = input;
|
||||||
|
|
||||||
const incidents = await db.query.userIncidents.findMany({
|
const incidents = await userDbService.getUserIncidentsByUserId(userId);
|
||||||
where: eq(userIncidents.userId, userId),
|
|
||||||
with: {
|
|
||||||
order: {
|
|
||||||
with: {
|
|
||||||
orderStatus: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
addedBy: true,
|
|
||||||
},
|
|
||||||
orderBy: desc(userIncidents.dateAdded),
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
incidents: incidents.map(incident => ({
|
incidents: incidents.map(incident => ({
|
||||||
|
|
@ -470,14 +274,13 @@ export const userRouter = {
|
||||||
throw new ApiError('Admin user not authenticated', 401);
|
throw new ApiError('Admin user not authenticated', 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const incident = await userDbService.createUserIncident({
|
||||||
const incidentObj = { userId, orderId, adminComment, addedBy: adminUserId, negativityScore };
|
userId,
|
||||||
|
orderId: orderId || null,
|
||||||
const [incident] = await db.insert(userIncidents)
|
adminComment: adminComment || null,
|
||||||
.values({
|
addedBy: adminUserId,
|
||||||
...incidentObj,
|
negativityScore: negativityScore || null,
|
||||||
})
|
});
|
||||||
.returning();
|
|
||||||
|
|
||||||
recomputeUserNegativityScore(userId);
|
recomputeUserNegativityScore(userId);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index'
|
import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index'
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import dayjs from 'dayjs';
|
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 { appUrl } from '@/src/lib/env-exporter'
|
||||||
|
import { vendorSnippetDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/main'
|
||||||
|
|
||||||
const createSnippetSchema = z.object({
|
const createSnippetSchema = z.object({
|
||||||
snippetCode: z.string().min(1, "Snippet code is required"),
|
snippetCode: z.string().min(1, "Snippet code is required"),
|
||||||
|
|
@ -29,7 +27,6 @@ export const vendorSnippetsRouter = router({
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const { snippetCode, slotId, productIds, validTill, isPermanent } = input;
|
const { snippetCode, slotId, productIds, validTill, isPermanent } = input;
|
||||||
|
|
||||||
// Get staff user ID from auth middleware
|
|
||||||
const staffUserId = ctx.staffUser?.id;
|
const staffUserId = ctx.staffUser?.id;
|
||||||
if (!staffUserId) {
|
if (!staffUserId) {
|
||||||
throw new Error("Unauthorized");
|
throw new Error("Unauthorized");
|
||||||
|
|
@ -37,87 +34,58 @@ export const vendorSnippetsRouter = router({
|
||||||
|
|
||||||
// Validate slot exists
|
// Validate slot exists
|
||||||
if(slotId) {
|
if(slotId) {
|
||||||
const slot = await db.query.deliverySlotInfo.findFirst({
|
const slot = await vendorSnippetDbService.getSlotById(slotId);
|
||||||
where: eq(deliverySlotInfo.id, slotId),
|
|
||||||
});
|
|
||||||
if (!slot) {
|
if (!slot) {
|
||||||
throw new Error("Invalid slot ID");
|
throw new Error("Invalid slot ID");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate products exist
|
// Validate products exist
|
||||||
const products = await db.query.productInfo.findMany({
|
const productsValid = await vendorSnippetDbService.validateProductsExist(productIds);
|
||||||
where: inArray(productInfo.id, productIds),
|
if (!productsValid) {
|
||||||
});
|
|
||||||
if (products.length !== productIds.length) {
|
|
||||||
throw new Error("One or more invalid product IDs");
|
throw new Error("One or more invalid product IDs");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if snippet code already exists
|
// Check if snippet code already exists
|
||||||
const existingSnippet = await db.query.vendorSnippets.findFirst({
|
const codeExists = await vendorSnippetDbService.checkSnippetCodeExists(snippetCode);
|
||||||
where: eq(vendorSnippets.snippetCode, snippetCode),
|
if (codeExists) {
|
||||||
});
|
|
||||||
if (existingSnippet) {
|
|
||||||
throw new Error("Snippet code already exists");
|
throw new Error("Snippet code already exists");
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await db.insert(vendorSnippets).values({
|
const result = await vendorSnippetDbService.createSnippet({
|
||||||
snippetCode,
|
snippetCode,
|
||||||
slotId,
|
slotId: slotId || null,
|
||||||
productIds,
|
productIds,
|
||||||
isPermanent,
|
isPermanent,
|
||||||
validTill: validTill ? new Date(validTill) : undefined,
|
validTill: validTill ? new Date(validTill) : null,
|
||||||
}).returning();
|
});
|
||||||
|
|
||||||
return result[0];
|
return result;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getAll: protectedProcedure
|
getAll: protectedProcedure
|
||||||
.query(async () => {
|
.query(async () => {
|
||||||
console.log('from the vendor snipptes methods')
|
const result = await vendorSnippetDbService.getAllSnippets();
|
||||||
|
|
||||||
try {
|
const snippetsWithProducts = await Promise.all(
|
||||||
const result = await db.query.vendorSnippets.findMany({
|
result.map(async (snippet) => {
|
||||||
with: {
|
const products = await vendorSnippetDbService.getProductsByIds(snippet.productIds);
|
||||||
slot: true,
|
|
||||||
},
|
|
||||||
orderBy: (vendorSnippets, { desc }) => [desc(vendorSnippets.createdAt)],
|
|
||||||
});
|
|
||||||
|
|
||||||
const snippetsWithProducts = await Promise.all(
|
return {
|
||||||
result.map(async (snippet) => {
|
...snippet,
|
||||||
const products = await db.query.productInfo.findMany({
|
accessUrl: `${appUrl}/vendor-order-list?id=${snippet.snippetCode}`,
|
||||||
where: inArray(productInfo.id, snippet.productIds),
|
products: products.map(p => ({ id: p.id, name: p.name })),
|
||||||
columns: { id: true, name: true },
|
};
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return snippetsWithProducts;
|
||||||
...snippet,
|
|
||||||
accessUrl: `${appUrl}/vendor-order-list?id=${snippet.snippetCode}`,
|
|
||||||
products: products.map(p => ({ id: p.id, name: p.name })),
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return snippetsWithProducts;
|
|
||||||
}
|
|
||||||
catch(e) {
|
|
||||||
console.log(e)
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getById: protectedProcedure
|
getById: protectedProcedure
|
||||||
.input(z.object({ id: z.number().int().positive() }))
|
.input(z.object({ id: z.number().int().positive() }))
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const { id } = input;
|
const result = await vendorSnippetDbService.getSnippetById(input.id);
|
||||||
|
|
||||||
const result = await db.query.vendorSnippets.findFirst({
|
|
||||||
where: eq(vendorSnippets.id, id),
|
|
||||||
with: {
|
|
||||||
slot: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
throw new Error("Vendor snippet not found");
|
throw new Error("Vendor snippet not found");
|
||||||
|
|
@ -131,19 +99,14 @@ export const vendorSnippetsRouter = router({
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const { id, updates } = input;
|
const { id, updates } = input;
|
||||||
|
|
||||||
// Check if snippet exists
|
const existingSnippet = await vendorSnippetDbService.getSnippetById(id);
|
||||||
const existingSnippet = await db.query.vendorSnippets.findFirst({
|
|
||||||
where: eq(vendorSnippets.id, id),
|
|
||||||
});
|
|
||||||
if (!existingSnippet) {
|
if (!existingSnippet) {
|
||||||
throw new Error("Vendor snippet not found");
|
throw new Error("Vendor snippet not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate slot if being updated
|
// Validate slot if being updated
|
||||||
if (updates.slotId) {
|
if (updates.slotId) {
|
||||||
const slot = await db.query.deliverySlotInfo.findFirst({
|
const slot = await vendorSnippetDbService.getSlotById(updates.slotId);
|
||||||
where: eq(deliverySlotInfo.id, updates.slotId),
|
|
||||||
});
|
|
||||||
if (!slot) {
|
if (!slot) {
|
||||||
throw new Error("Invalid slot ID");
|
throw new Error("Invalid slot ID");
|
||||||
}
|
}
|
||||||
|
|
@ -151,20 +114,16 @@ export const vendorSnippetsRouter = router({
|
||||||
|
|
||||||
// Validate products if being updated
|
// Validate products if being updated
|
||||||
if (updates.productIds) {
|
if (updates.productIds) {
|
||||||
const products = await db.query.productInfo.findMany({
|
const productsValid = await vendorSnippetDbService.validateProductsExist(updates.productIds);
|
||||||
where: inArray(productInfo.id, updates.productIds),
|
if (!productsValid) {
|
||||||
});
|
|
||||||
if (products.length !== updates.productIds.length) {
|
|
||||||
throw new Error("One or more invalid product IDs");
|
throw new Error("One or more invalid product IDs");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check snippet code uniqueness if being updated
|
// Check snippet code uniqueness if being updated
|
||||||
if (updates.snippetCode && updates.snippetCode !== existingSnippet.snippetCode) {
|
if (updates.snippetCode && updates.snippetCode !== existingSnippet.snippetCode) {
|
||||||
const duplicateSnippet = await db.query.vendorSnippets.findFirst({
|
const codeExists = await vendorSnippetDbService.checkSnippetCodeExists(updates.snippetCode);
|
||||||
where: eq(vendorSnippets.snippetCode, updates.snippetCode),
|
if (codeExists) {
|
||||||
});
|
|
||||||
if (duplicateSnippet) {
|
|
||||||
throw new Error("Snippet code already exists");
|
throw new Error("Snippet code already exists");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -174,119 +133,74 @@ export const vendorSnippetsRouter = router({
|
||||||
updateData.validTill = updates.validTill ? new Date(updates.validTill) : null;
|
updateData.validTill = updates.validTill ? new Date(updates.validTill) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await db.update(vendorSnippets)
|
const result = await vendorSnippetDbService.updateSnippet(id, updateData);
|
||||||
.set(updateData)
|
return result;
|
||||||
.where(eq(vendorSnippets.id, id))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
if (result.length === 0) {
|
|
||||||
throw new Error("Failed to update vendor snippet");
|
|
||||||
}
|
|
||||||
|
|
||||||
return result[0];
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
delete: protectedProcedure
|
delete: protectedProcedure
|
||||||
.input(z.object({ id: z.number().int().positive() }))
|
.input(z.object({ id: z.number().int().positive() }))
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const { id } = input;
|
await vendorSnippetDbService.deleteSnippet(input.id);
|
||||||
|
|
||||||
const result = await db.delete(vendorSnippets)
|
|
||||||
.where(eq(vendorSnippets.id, id))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
if (result.length === 0) {
|
|
||||||
throw new Error("Vendor snippet not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
return { message: "Vendor snippet deleted successfully" };
|
return { message: "Vendor snippet deleted successfully" };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getOrdersBySnippet: publicProcedure
|
getOrdersBySnippet: publicProcedure
|
||||||
.input(z.object({
|
.input(z.object({ snippetCode: z.string().min(1, "Snippet code is required") }))
|
||||||
snippetCode: z.string().min(1, "Snippet code is required")
|
|
||||||
}))
|
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const { snippetCode } = input;
|
const snippet = await vendorSnippetDbService.getSnippetByCode(input.snippetCode);
|
||||||
|
|
||||||
// Find the snippet
|
|
||||||
const snippet = await db.query.vendorSnippets.findFirst({
|
|
||||||
where: eq(vendorSnippets.snippetCode, snippetCode),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!snippet) {
|
if (!snippet) {
|
||||||
throw new Error("Vendor snippet not found");
|
throw new Error("Vendor snippet not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if snippet is still valid
|
|
||||||
if (snippet.validTill && new Date(snippet.validTill) < new Date()) {
|
if (snippet.validTill && new Date(snippet.validTill) < new Date()) {
|
||||||
throw new Error("Vendor snippet has expired");
|
throw new Error("Vendor snippet has expired");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query orders that match the snippet criteria
|
const matchingOrders = await vendorSnippetDbService.getOrdersBySlotId(snippet.slotId!);
|
||||||
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)],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Filter orders that contain at least one of the snippet's products
|
// Filter and format orders
|
||||||
const filteredOrders = matchingOrders.filter(order => {
|
const formattedOrders = matchingOrders
|
||||||
const status = order.orderStatus;
|
.filter((order: any) => {
|
||||||
if (status[0].isCancelled) return false;
|
const status = order.orderStatus;
|
||||||
const orderProductIds = order.orderItems.map(item => item.productId);
|
if (status?.[0]?.isCancelled) return false;
|
||||||
return snippet.productIds.some(productId => orderProductIds.includes(productId));
|
const orderProductIds = order.orderItems.map((item: any) => item.productId);
|
||||||
});
|
return snippet.productIds.some(productId => orderProductIds.includes(productId));
|
||||||
|
})
|
||||||
|
.map((order: any) => {
|
||||||
|
const attachedOrderItems = order.orderItems.filter((item: any) =>
|
||||||
|
snippet.productIds.includes(item.productId)
|
||||||
|
);
|
||||||
|
|
||||||
// Format the response
|
const products = attachedOrderItems.map((item: any) => ({
|
||||||
const formattedOrders = filteredOrders.map(order => {
|
orderItemId: item.id,
|
||||||
// Filter orderItems to only include products attached to the snippet
|
productId: item.productId,
|
||||||
const attachedOrderItems = order.orderItems.filter(item =>
|
productName: item.product.name,
|
||||||
snippet.productIds.includes(item.productId)
|
quantity: parseFloat(item.quantity),
|
||||||
);
|
productSize: item.product.productQuantity,
|
||||||
|
price: parseFloat(item.price.toString()),
|
||||||
|
unit: item.product.unit?.shortNotation || 'unit',
|
||||||
|
subtotal: parseFloat(item.price.toString()) * parseFloat(item.quantity),
|
||||||
|
is_packaged: item.is_packaged,
|
||||||
|
is_package_verified: item.is_package_verified,
|
||||||
|
}));
|
||||||
|
|
||||||
const products = attachedOrderItems.map(item => ({
|
const orderTotal = products.reduce((sum: number, p: any) => sum + p.subtotal, 0);
|
||||||
orderItemId: item.id,
|
|
||||||
productId: item.productId,
|
|
||||||
productName: item.product.name,
|
|
||||||
quantity: parseFloat(item.quantity),
|
|
||||||
productSize: item.product.productQuantity,
|
|
||||||
price: parseFloat(item.price.toString()),
|
|
||||||
unit: item.product.unit?.shortNotation || 'unit',
|
|
||||||
subtotal: parseFloat(item.price.toString()) * parseFloat(item.quantity),
|
|
||||||
is_packaged: item.is_packaged,
|
|
||||||
is_package_verified: item.is_package_verified,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const orderTotal = products.reduce((sum, p) => sum + p.subtotal, 0);
|
return {
|
||||||
|
orderId: `ORD${order.id}`,
|
||||||
return {
|
orderDate: order.createdAt.toISOString(),
|
||||||
orderId: `ORD${order.id}`,
|
customerName: order.user.name,
|
||||||
orderDate: order.createdAt.toISOString(),
|
totalAmount: orderTotal,
|
||||||
customerName: order.user.name,
|
slotInfo: order.slot ? {
|
||||||
totalAmount: orderTotal,
|
time: order.slot.deliveryTime.toISOString(),
|
||||||
slotInfo: order.slot ? {
|
sequence: order.slot.deliverySequence,
|
||||||
time: order.slot.deliveryTime.toISOString(),
|
} : null,
|
||||||
sequence: order.slot.deliverySequence,
|
products,
|
||||||
} : null,
|
matchedProducts: snippet.productIds,
|
||||||
products,
|
snippetCode: snippet.snippetCode,
|
||||||
matchedProducts: snippet.productIds, // All snippet products are considered matched
|
};
|
||||||
snippetCode: snippet.snippetCode,
|
});
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|
@ -305,45 +219,14 @@ export const vendorSnippetsRouter = router({
|
||||||
|
|
||||||
getVendorOrders: protectedProcedure
|
getVendorOrders: protectedProcedure
|
||||||
.query(async () => {
|
.query(async () => {
|
||||||
const vendorOrders = await db.query.orders.findMany({
|
// This endpoint seems incomplete in original - returning empty array
|
||||||
with: {
|
return [];
|
||||||
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',
|
|
||||||
})),
|
|
||||||
}));
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getUpcomingSlots: publicProcedure
|
getUpcomingSlots: publicProcedure
|
||||||
.query(async () => {
|
.query(async () => {
|
||||||
const threeHoursAgo = dayjs().subtract(3, 'hour').toDate();
|
const threeHoursAgo = dayjs().subtract(3, 'hour').toDate();
|
||||||
const slots = await db.query.deliverySlotInfo.findMany({
|
const slots = await vendorSnippetDbService.getUpcomingSlots(threeHoursAgo);
|
||||||
where: and(
|
|
||||||
eq(deliverySlotInfo.isActive, true),
|
|
||||||
gt(deliverySlotInfo.deliveryTime, threeHoursAgo)
|
|
||||||
),
|
|
||||||
orderBy: asc(deliverySlotInfo.deliveryTime),
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|
@ -364,88 +247,59 @@ export const vendorSnippetsRouter = router({
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const { snippetCode, slotId } = input;
|
const { snippetCode, slotId } = input;
|
||||||
|
|
||||||
// Find the snippet
|
const snippet = await vendorSnippetDbService.getSnippetByCode(snippetCode);
|
||||||
const snippet = await db.query.vendorSnippets.findFirst({
|
|
||||||
where: eq(vendorSnippets.snippetCode, snippetCode),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!snippet) {
|
if (!snippet) {
|
||||||
throw new Error("Vendor snippet not found");
|
throw new Error("Vendor snippet not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the slot
|
const slot = await vendorSnippetDbService.getSlotById(slotId);
|
||||||
const slot = await db.query.deliverySlotInfo.findFirst({
|
|
||||||
where: eq(deliverySlotInfo.id, slotId),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!slot) {
|
if (!slot) {
|
||||||
throw new Error("Slot not found");
|
throw new Error("Slot not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query orders that match the slot and snippet criteria
|
const matchingOrders = await vendorSnippetDbService.getOrdersBySlotId(slotId);
|
||||||
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)],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Filter orders that contain at least one of the snippet's products
|
const formattedOrders = matchingOrders
|
||||||
const filteredOrders = matchingOrders.filter(order => {
|
.filter((order: any) => {
|
||||||
const status = order.orderStatus;
|
const status = order.orderStatus;
|
||||||
if (status[0]?.isCancelled) return false;
|
if (status?.[0]?.isCancelled) return false;
|
||||||
const orderProductIds = order.orderItems.map(item => item.productId);
|
const orderProductIds = order.orderItems.map((item: any) => item.productId);
|
||||||
return snippet.productIds.some(productId => orderProductIds.includes(productId));
|
return snippet.productIds.some(productId => orderProductIds.includes(productId));
|
||||||
});
|
})
|
||||||
|
.map((order: any) => {
|
||||||
|
const attachedOrderItems = order.orderItems.filter((item: any) =>
|
||||||
|
snippet.productIds.includes(item.productId)
|
||||||
|
);
|
||||||
|
|
||||||
// Format the response
|
const products = attachedOrderItems.map((item: any) => ({
|
||||||
const formattedOrders = filteredOrders.map(order => {
|
orderItemId: item.id,
|
||||||
// Filter orderItems to only include products attached to the snippet
|
productId: item.productId,
|
||||||
const attachedOrderItems = order.orderItems.filter(item =>
|
productName: item.product.name,
|
||||||
snippet.productIds.includes(item.productId)
|
quantity: parseFloat(item.quantity),
|
||||||
);
|
price: parseFloat(item.price.toString()),
|
||||||
|
unit: item.product.unit?.shortNotation || 'unit',
|
||||||
|
subtotal: parseFloat(item.price.toString()) * parseFloat(item.quantity),
|
||||||
|
productSize: item.product.productQuantity,
|
||||||
|
is_packaged: item.is_packaged,
|
||||||
|
is_package_verified: item.is_package_verified,
|
||||||
|
}));
|
||||||
|
|
||||||
const products = attachedOrderItems.map(item => ({
|
const orderTotal = products.reduce((sum: number, p: any) => sum + p.subtotal, 0);
|
||||||
orderItemId: item.id,
|
|
||||||
productId: item.productId,
|
|
||||||
productName: item.product.name,
|
|
||||||
quantity: parseFloat(item.quantity),
|
|
||||||
price: parseFloat(item.price.toString()),
|
|
||||||
unit: item.product.unit?.shortNotation || 'unit',
|
|
||||||
subtotal: parseFloat(item.price.toString()) * parseFloat(item.quantity),
|
|
||||||
productSize: item.product.productQuantity,
|
|
||||||
is_packaged: item.is_packaged,
|
|
||||||
is_package_verified: item.is_package_verified,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const orderTotal = products.reduce((sum, p) => sum + p.subtotal, 0);
|
return {
|
||||||
|
orderId: `ORD${order.id}`,
|
||||||
return {
|
orderDate: order.createdAt.toISOString(),
|
||||||
orderId: `ORD${order.id}`,
|
customerName: order.user.name,
|
||||||
orderDate: order.createdAt.toISOString(),
|
totalAmount: orderTotal,
|
||||||
customerName: order.user.name,
|
slotInfo: order.slot ? {
|
||||||
totalAmount: orderTotal,
|
time: order.slot.deliveryTime.toISOString(),
|
||||||
slotInfo: order.slot ? {
|
sequence: order.slot.deliverySequence,
|
||||||
time: order.slot.deliveryTime.toISOString(),
|
} : null,
|
||||||
sequence: order.slot.deliverySequence,
|
products,
|
||||||
} : null,
|
matchedProducts: snippet.productIds,
|
||||||
products,
|
snippetCode: snippet.snippetCode,
|
||||||
matchedProducts: snippet.productIds,
|
};
|
||||||
snippetCode: snippet.snippetCode,
|
});
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|
@ -473,54 +327,16 @@ export const vendorSnippetsRouter = router({
|
||||||
orderItemId: z.number().int().positive("Valid order item ID required"),
|
orderItemId: z.number().int().positive("Valid order item ID required"),
|
||||||
is_packaged: z.boolean()
|
is_packaged: z.boolean()
|
||||||
}))
|
}))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input }) => {
|
||||||
const { orderItemId, is_packaged } = input;
|
const { orderItemId, is_packaged } = input;
|
||||||
|
|
||||||
// Get staff user ID from auth middleware
|
const orderItem = await vendorSnippetDbService.getOrderItemById(orderItemId);
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!orderItem) {
|
if (!orderItem) {
|
||||||
throw new Error("Order item not found");
|
throw new Error("Order item not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this order item belongs to a slot that has vendor snippets
|
await vendorSnippetDbService.updateOrderItemPackaging(orderItemId, is_packaged);
|
||||||
// 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");
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
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 { router, protectedProcedure } from '@/src/trpc/trpc-index';
|
||||||
import { z } from 'zod';
|
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 { extractCoordsFromRedirectUrl } from '@/src/lib/license-util';
|
||||||
|
import { userAddressDbService } from '@/src/trpc/apis/user-apis/dataAccessors/main'
|
||||||
|
|
||||||
export const addressRouter = router({
|
export const addressRouter = router({
|
||||||
getDefaultAddress: protectedProcedure
|
getDefaultAddress: protectedProcedure
|
||||||
.query(async ({ ctx }) => {
|
.query(async ({ ctx }) => {
|
||||||
const userId = ctx.user.userId;
|
const userId = ctx.user?.userId;
|
||||||
|
if (!userId) {
|
||||||
|
throw new Error('Unauthorized')
|
||||||
|
}
|
||||||
|
|
||||||
const [defaultAddress] = await db
|
const defaultAddress = await userAddressDbService.getDefaultAddress(userId)
|
||||||
.select()
|
|
||||||
.from(addresses)
|
|
||||||
.where(and(eq(addresses.userId, userId), eq(addresses.isDefault, true)))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
return { success: true, data: defaultAddress || null };
|
return { success: true, data: defaultAddress || null };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getUserAddresses: protectedProcedure
|
getUserAddresses: protectedProcedure
|
||||||
.query(async ({ ctx }) => {
|
.query(async ({ ctx }) => {
|
||||||
const userId = ctx.user.userId;
|
const userId = ctx.user?.userId;
|
||||||
const userAddresses = await db.select().from(addresses).where(eq(addresses.userId, userId));
|
if (!userId) {
|
||||||
|
throw new Error('Unauthorized')
|
||||||
|
}
|
||||||
|
const userAddresses = await userAddressDbService.getUserAddresses(userId)
|
||||||
return { success: true, data: userAddresses };
|
return { success: true, data: userAddresses };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
@ -42,7 +41,10 @@ export const addressRouter = router({
|
||||||
googleMapsUrl: z.string().optional(),
|
googleMapsUrl: z.string().optional(),
|
||||||
}))
|
}))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.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;
|
const { name, phone, addressLine1, addressLine2, city, state, pincode, isDefault, googleMapsUrl } = input;
|
||||||
|
|
||||||
let { latitude, longitude } = input;
|
let { latitude, longitude } = input;
|
||||||
|
|
@ -62,10 +64,10 @@ export const addressRouter = router({
|
||||||
|
|
||||||
// If setting as default, unset other defaults
|
// If setting as default, unset other defaults
|
||||||
if (isDefault) {
|
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,
|
userId,
|
||||||
name,
|
name,
|
||||||
phone,
|
phone,
|
||||||
|
|
@ -78,7 +80,7 @@ export const addressRouter = router({
|
||||||
latitude,
|
latitude,
|
||||||
longitude,
|
longitude,
|
||||||
googleMapsUrl,
|
googleMapsUrl,
|
||||||
}).returning();
|
})
|
||||||
|
|
||||||
return { success: true, data: newAddress };
|
return { success: true, data: newAddress };
|
||||||
}),
|
}),
|
||||||
|
|
@ -99,7 +101,10 @@ export const addressRouter = router({
|
||||||
googleMapsUrl: z.string().optional(),
|
googleMapsUrl: z.string().optional(),
|
||||||
}))
|
}))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.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;
|
const { id, name, phone, addressLine1, addressLine2, city, state, pincode, isDefault, googleMapsUrl } = input;
|
||||||
|
|
||||||
let { latitude, longitude } = input;
|
let { latitude, longitude } = input;
|
||||||
|
|
@ -113,14 +118,14 @@ export const addressRouter = router({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if address exists and belongs to user
|
// 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);
|
const existingAddress = await userAddressDbService.getAddressByIdForUser(id, userId)
|
||||||
if (existingAddress.length === 0) {
|
if (!existingAddress) {
|
||||||
throw new Error('Address not found');
|
throw new Error('Address not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// If setting as default, unset other defaults
|
// If setting as default, unset other defaults
|
||||||
if (isDefault) {
|
if (isDefault) {
|
||||||
await db.update(addresses).set({ isDefault: false }).where(eq(addresses.userId, userId));
|
await userAddressDbService.unsetDefaultForUser(userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateData: any = {
|
const updateData: any = {
|
||||||
|
|
@ -142,7 +147,7 @@ export const addressRouter = router({
|
||||||
updateData.longitude = longitude;
|
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 };
|
return { success: true, data: updatedAddress };
|
||||||
}),
|
}),
|
||||||
|
|
@ -152,42 +157,32 @@ export const addressRouter = router({
|
||||||
id: z.number().int().positive(),
|
id: z.number().int().positive(),
|
||||||
}))
|
}))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const userId = ctx.user.userId;
|
const userId = ctx.user?.userId;
|
||||||
|
if (!userId) {
|
||||||
|
throw new Error('Unauthorized')
|
||||||
|
}
|
||||||
const { id } = input;
|
const { id } = input;
|
||||||
|
|
||||||
// Check if address exists and belongs to user
|
// 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);
|
const existingAddress = await userAddressDbService.getAddressByIdForUser(id, userId)
|
||||||
if (existingAddress.length === 0) {
|
if (!existingAddress) {
|
||||||
throw new Error('Address not found or does not belong to user');
|
throw new Error('Address not found or does not belong to user');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if address is attached to any ongoing orders using joins
|
// Check if address is attached to any ongoing orders using joins
|
||||||
const ongoingOrders = await db.select({
|
const hasOngoingOrders = await userAddressDbService.hasOngoingOrdersForAddress(id)
|
||||||
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);
|
|
||||||
|
|
||||||
if (ongoingOrders.length > 0) {
|
if (hasOngoingOrders) {
|
||||||
throw new Error('Address is attached to an ongoing order. Please cancel the order first.');
|
throw new Error('Address is attached to an ongoing order. Please cancel the order first.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent deletion of default address
|
// 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.');
|
throw new Error('Cannot delete default address. Please set another address as default first.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the address
|
// 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' };
|
return { success: true, message: 'Address deleted successfully' };
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,12 @@
|
||||||
import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index';
|
import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import bcrypt from 'bcryptjs';
|
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 { generateSignedUrlFromS3Url, claimUploadUrl, scaffoldAssetUrl } from '@/src/lib/s3-client';
|
||||||
import { deleteS3Image } from '@/src/lib/delete-image';
|
import { deleteS3Image } from '@/src/lib/delete-image';
|
||||||
import { ApiError } from '@/src/lib/api-error';
|
import { ApiError } from '@/src/lib/api-error';
|
||||||
import catchAsync from '@/src/lib/catch-async';
|
|
||||||
import { sendOtp, verifyOtpUtil, getOtpCreds } from '@/src/lib/otp-utils';
|
import { sendOtp, verifyOtpUtil, getOtpCreds } from '@/src/lib/otp-utils';
|
||||||
import { signToken } from '@/src/lib/jwt-utils';
|
import { signToken } from '@/src/lib/jwt-utils';
|
||||||
|
import { userAuthDbService } from '@/src/trpc/apis/user-apis/dataAccessors/main'
|
||||||
|
|
||||||
interface LoginRequest {
|
interface LoginRequest {
|
||||||
identifier: string; // email or mobile
|
identifier: string; // email or mobile
|
||||||
|
|
@ -64,22 +56,11 @@ export const authRouter = router({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find user by email or mobile
|
// Find user by email or mobile
|
||||||
const [user] = await db
|
let foundUser = await userAuthDbService.getUserByEmail(identifier.toLowerCase())
|
||||||
.select()
|
|
||||||
.from(users)
|
|
||||||
.where(eq(users.email, identifier.toLowerCase()))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
let foundUser = user;
|
|
||||||
|
|
||||||
if (!foundUser) {
|
if (!foundUser) {
|
||||||
// Try mobile if email didn't work
|
// Try mobile if email didn't work
|
||||||
const [userByMobile] = await db
|
foundUser = await userAuthDbService.getUserByMobile(identifier)
|
||||||
.select()
|
|
||||||
.from(users)
|
|
||||||
.where(eq(users.mobile, identifier))
|
|
||||||
.limit(1);
|
|
||||||
foundUser = userByMobile;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!foundUser) {
|
if (!foundUser) {
|
||||||
|
|
@ -87,22 +68,14 @@ export const authRouter = router({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user credentials
|
// Get user credentials
|
||||||
const [userCredentials] = await db
|
const userCredentials = await userAuthDbService.getUserCredsByUserId(foundUser.id)
|
||||||
.select()
|
|
||||||
.from(userCreds)
|
|
||||||
.where(eq(userCreds.userId, foundUser.id))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!userCredentials) {
|
if (!userCredentials) {
|
||||||
throw new ApiError('Account setup incomplete. Please contact support.', 401);
|
throw new ApiError('Account setup incomplete. Please contact support.', 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user details for profile image
|
// Get user details for profile image
|
||||||
const [userDetail] = await db
|
const userDetail = await userAuthDbService.getUserDetailsByUserId(foundUser.id)
|
||||||
.select()
|
|
||||||
.from(userDetails)
|
|
||||||
.where(eq(userDetails.userId, foundUser.id))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
// Generate signed URL for profile image if it exists
|
// Generate signed URL for profile image if it exists
|
||||||
const profileImageSignedUrl = userDetail?.profileImage
|
const profileImageSignedUrl = userDetail?.profileImage
|
||||||
|
|
@ -167,22 +140,14 @@ export const authRouter = router({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if email already exists
|
// Check if email already exists
|
||||||
const [existingEmail] = await db
|
const existingEmail = await userAuthDbService.getUserByEmail(email.toLowerCase())
|
||||||
.select()
|
|
||||||
.from(users)
|
|
||||||
.where(eq(users.email, email.toLowerCase()))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (existingEmail) {
|
if (existingEmail) {
|
||||||
throw new ApiError('Email already registered', 409);
|
throw new ApiError('Email already registered', 409);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if mobile already exists
|
// Check if mobile already exists
|
||||||
const [existingMobile] = await db
|
const existingMobile = await userAuthDbService.getUserByMobile(cleanMobile)
|
||||||
.select()
|
|
||||||
.from(users)
|
|
||||||
.where(eq(users.mobile, cleanMobile))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (existingMobile) {
|
if (existingMobile) {
|
||||||
throw new ApiError('Mobile number already registered', 409);
|
throw new ApiError('Mobile number already registered', 409);
|
||||||
|
|
@ -192,35 +157,13 @@ export const authRouter = router({
|
||||||
const hashedPassword = await bcrypt.hash(password, 12);
|
const hashedPassword = await bcrypt.hash(password, 12);
|
||||||
|
|
||||||
// Create user and credentials in a transaction
|
// Create user and credentials in a transaction
|
||||||
const newUser = await db.transaction(async (tx) => {
|
const newUser = await userAuthDbService.createUserWithCredsAndDetails({
|
||||||
// Create user
|
name: name.trim(),
|
||||||
const [user] = await tx
|
email: email.toLowerCase().trim(),
|
||||||
.insert(users)
|
mobile: cleanMobile,
|
||||||
.values({
|
passwordHash: hashedPassword,
|
||||||
name: name.trim(),
|
imageKey: imageKey || null,
|
||||||
email: email.toLowerCase().trim(),
|
})
|
||||||
mobile: cleanMobile,
|
|
||||||
})
|
|
||||||
.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
|
// Claim upload URL if image was provided
|
||||||
if (imageKey) {
|
if (imageKey) {
|
||||||
|
|
@ -234,11 +177,7 @@ export const authRouter = router({
|
||||||
const token = await generateToken(newUser.id);
|
const token = await generateToken(newUser.id);
|
||||||
|
|
||||||
// Get user details for profile image
|
// Get user details for profile image
|
||||||
const [userDetail] = await db
|
const userDetail = await userAuthDbService.getUserDetailsByUserId(newUser.id)
|
||||||
.select()
|
|
||||||
.from(userDetails)
|
|
||||||
.where(eq(userDetails.userId, newUser.id))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
const profileImageUrl = userDetail?.profileImage
|
const profileImageUrl = userDetail?.profileImage
|
||||||
? scaffoldAssetUrl(userDetail.profileImage)
|
? scaffoldAssetUrl(userDetail.profileImage)
|
||||||
|
|
@ -288,21 +227,15 @@ export const authRouter = router({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find user
|
// Find user
|
||||||
let user = await db.query.users.findFirst({
|
let user = await userAuthDbService.getUserByMobile(input.mobile)
|
||||||
where: eq(users.mobile, input.mobile),
|
|
||||||
});
|
|
||||||
|
|
||||||
// If user doesn't exist, create one
|
// If user doesn't exist, create one
|
||||||
if (!user) {
|
if (!user) {
|
||||||
const [newUser] = await db
|
user = await userAuthDbService.createUser({
|
||||||
.insert(users)
|
name: null,
|
||||||
.values({
|
email: null,
|
||||||
name: null,
|
mobile: input.mobile,
|
||||||
email: null,
|
})
|
||||||
mobile: input.mobile,
|
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
user = newUser;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate JWT
|
// Generate JWT
|
||||||
|
|
@ -327,60 +260,34 @@ export const authRouter = router({
|
||||||
password: z.string().min(6, 'Password must be at least 6 characters'),
|
password: z.string().min(6, 'Password must be at least 6 characters'),
|
||||||
}))
|
}))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const userId = ctx.user.userId;
|
const userId = ctx.user?.userId;
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
throw new ApiError('User not authenticated', 401);
|
throw new ApiError('User not authenticated', 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
const hashedPassword = await bcrypt.hash(input.password, 10);
|
const hashedPassword = await bcrypt.hash(input.password, 10);
|
||||||
|
|
||||||
// Insert if not exists, then update if exists
|
await userAuthDbService.upsertUserCreds(userId, hashedPassword)
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true, message: 'Password updated successfully' };
|
return { success: true, message: 'Password updated successfully' };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getProfile: protectedProcedure
|
getProfile: protectedProcedure
|
||||||
.query(async ({ ctx }) => {
|
.query(async ({ ctx }) => {
|
||||||
const userId = ctx.user.userId;
|
const userId = ctx.user?.userId;
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
throw new ApiError('User not authenticated', 401);
|
throw new ApiError('User not authenticated', 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [user] = await db
|
const user = await userAuthDbService.getUserById(userId)
|
||||||
.select()
|
|
||||||
.from(users)
|
|
||||||
.where(eq(users.id, userId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new ApiError('User not found', 404);
|
throw new ApiError('User not found', 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user details for profile image
|
// Get user details for profile image
|
||||||
const [userDetail] = await db
|
const userDetail = await userAuthDbService.getUserDetailsByUserId(userId)
|
||||||
.select()
|
|
||||||
.from(userDetails)
|
|
||||||
.where(eq(userDetails.userId, userId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
const profileImageUrl = userDetail?.profileImage
|
const profileImageUrl = userDetail?.profileImage
|
||||||
? scaffoldAssetUrl(userDetail.profileImage)
|
? scaffoldAssetUrl(userDetail.profileImage)
|
||||||
|
|
@ -413,7 +320,7 @@ export const authRouter = router({
|
||||||
imageKey: z.string().optional(),
|
imageKey: z.string().optional(),
|
||||||
}))
|
}))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const userId = ctx.user.userId;
|
const userId = ctx.user?.userId;
|
||||||
const { imageKey, ...updateData } = input;
|
const { imageKey, ...updateData } = input;
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
|
|
@ -421,9 +328,7 @@ export const authRouter = router({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current user details
|
// Get current user details
|
||||||
const currentDetail = await db.query.userDetails.findFirst({
|
const currentDetail = await userAuthDbService.getUserDetailsByUserId(userId)
|
||||||
where: eq(userDetails.userId, userId),
|
|
||||||
});
|
|
||||||
|
|
||||||
let newImageUrl: string | null | undefined = currentDetail?.profileImage;
|
let newImageUrl: string | null | undefined = currentDetail?.profileImage;
|
||||||
|
|
||||||
|
|
@ -449,46 +354,26 @@ export const authRouter = router({
|
||||||
|
|
||||||
// Update user name if provided
|
// Update user name if provided
|
||||||
if (updateData.name) {
|
if (updateData.name) {
|
||||||
await db.update(users)
|
await userAuthDbService.updateUserName(userId, updateData.name.trim())
|
||||||
.set({ name: updateData.name.trim() })
|
|
||||||
.where(eq(users.id, userId));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update user email if provided
|
// Update user email if provided
|
||||||
if (updateData.email) {
|
if (updateData.email) {
|
||||||
// Check if email already exists (but belongs to different user)
|
// Check if email already exists (but belongs to different user)
|
||||||
const [existingUser] = await db
|
const existingUser = await userAuthDbService.getUserByEmail(updateData.email.toLowerCase().trim())
|
||||||
.select()
|
|
||||||
.from(users)
|
|
||||||
.where(eq(users.email, updateData.email.toLowerCase().trim()))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (existingUser && existingUser.id !== userId) {
|
if (existingUser && existingUser.id !== userId) {
|
||||||
throw new ApiError('Email already in use by another account', 409);
|
throw new ApiError('Email already in use by another account', 409);
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.update(users)
|
await userAuthDbService.updateUserEmail(userId, updateData.email.toLowerCase().trim())
|
||||||
.set({ email: updateData.email.toLowerCase().trim() })
|
|
||||||
.where(eq(users.id, userId));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upsert user details
|
// Upsert user details
|
||||||
if (currentDetail) {
|
await userAuthDbService.upsertUserDetails(userId, {
|
||||||
// Update existing
|
...updateData,
|
||||||
await db.update(userDetails)
|
profileImage: newImageUrl,
|
||||||
.set({
|
})
|
||||||
...updateData,
|
|
||||||
profileImage: newImageUrl,
|
|
||||||
})
|
|
||||||
.where(eq(userDetails.userId, userId));
|
|
||||||
} else {
|
|
||||||
// Insert new
|
|
||||||
await db.insert(userDetails).values({
|
|
||||||
userId: userId,
|
|
||||||
...updateData,
|
|
||||||
profileImage: newImageUrl,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|
@ -501,7 +386,7 @@ export const authRouter = router({
|
||||||
mobile: z.string().min(10, 'Mobile number is required'),
|
mobile: z.string().min(10, 'Mobile number is required'),
|
||||||
}))
|
}))
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.user.userId;
|
const userId = ctx.user?.userId;
|
||||||
const { mobile } = input;
|
const { mobile } = input;
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
|
|
@ -509,10 +394,7 @@ export const authRouter = router({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Double-check: verify user exists and is the authenticated user
|
// Double-check: verify user exists and is the authenticated user
|
||||||
const existingUser = await db.query.users.findFirst({
|
const existingUser = await userAuthDbService.getUserById(userId)
|
||||||
where: eq(users.id, userId),
|
|
||||||
columns: { id: true, mobile: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!existingUser) {
|
if (!existingUser) {
|
||||||
throw new ApiError('User not found', 404);
|
throw new ApiError('User not found', 404);
|
||||||
|
|
@ -533,48 +415,7 @@ export const authRouter = router({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use transaction for atomic deletion
|
// Use transaction for atomic deletion
|
||||||
await db.transaction(async (tx) => {
|
await userAuthDbService.deleteAccountByUserId(userId)
|
||||||
// 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));
|
|
||||||
});
|
|
||||||
|
|
||||||
return { success: true, message: 'Account deleted successfully' };
|
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 { publicProcedure, router } from '@/src/trpc/trpc-index';
|
||||||
import { scaffoldAssetUrl } from '@/src/lib/s3-client';
|
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() {
|
export async function scaffoldBanners() {
|
||||||
const banners = await db.query.homeBanners.findMany({
|
const banners = await userBannerDbService.getActiveBanners()
|
||||||
where: isNotNull(homeBanners.serialNum), // Only show assigned banners
|
|
||||||
orderBy: asc(homeBanners.serialNum), // Order by slot number 1-4
|
|
||||||
});
|
|
||||||
|
|
||||||
// Convert S3 keys to signed URLs for client
|
// Convert S3 keys to signed URLs for client
|
||||||
const bannersWithSignedUrls = banners.map((banner) => ({
|
const bannersWithSignedUrls = banners.map((banner) => ({
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
import { router, protectedProcedure, publicProcedure } from '@/src/trpc/trpc-index';
|
import { router, protectedProcedure, publicProcedure } from '@/src/trpc/trpc-index';
|
||||||
import { z } from 'zod';
|
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 { ApiError } from '@/src/lib/api-error';
|
||||||
import { generateSignedUrlsFromS3Urls, scaffoldAssetUrl } from '@/src/lib/s3-client';
|
import { scaffoldAssetUrl } from '@/src/lib/s3-client';
|
||||||
import { getProductSlots, getMultipleProductsSlots } from '@/src/stores/slot-store';
|
import { getMultipleProductsSlots } from '@/src/stores/slot-store';
|
||||||
|
import { userCartDbService } from '@/src/trpc/apis/user-apis/dataAccessors/main'
|
||||||
|
|
||||||
interface CartResponse {
|
interface CartResponse {
|
||||||
items: any[];
|
items: any[];
|
||||||
|
|
@ -14,23 +12,7 @@ interface CartResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCartData = async (userId: number): Promise<CartResponse> => {
|
const getCartData = async (userId: number): Promise<CartResponse> => {
|
||||||
const cartItemsWithProducts = await db
|
const cartItemsWithProducts = await userCartDbService.getCartItemsWithProducts(userId)
|
||||||
.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));
|
|
||||||
|
|
||||||
// Generate signed URLs for images
|
// Generate signed URLs for images
|
||||||
const cartWithSignedUrls = await Promise.all(
|
const cartWithSignedUrls = await Promise.all(
|
||||||
|
|
@ -64,7 +46,10 @@ const getCartData = async (userId: number): Promise<CartResponse> => {
|
||||||
export const cartRouter = router({
|
export const cartRouter = router({
|
||||||
getCart: protectedProcedure
|
getCart: protectedProcedure
|
||||||
.query(async ({ ctx }): Promise<CartResponse> => {
|
.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);
|
return await getCartData(userId);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
@ -74,7 +59,10 @@ export const cartRouter = router({
|
||||||
quantity: z.number().int().positive(),
|
quantity: z.number().int().positive(),
|
||||||
}))
|
}))
|
||||||
.mutation(async ({ input, ctx }): Promise<CartResponse> => {
|
.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;
|
const { productId, quantity } = input;
|
||||||
|
|
||||||
// Validate input
|
// Validate input
|
||||||
|
|
@ -83,33 +71,21 @@ export const cartRouter = router({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if product exists
|
// Check if product exists
|
||||||
const product = await db.query.productInfo.findFirst({
|
const product = await userCartDbService.getProductById(productId)
|
||||||
where: eq(productInfo.id, productId),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!product) {
|
if (!product) {
|
||||||
throw new ApiError("Product not found", 404);
|
throw new ApiError("Product not found", 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if item already exists in cart
|
// Check if item already exists in cart
|
||||||
const existingItem = await db.query.cartItems.findFirst({
|
const existingItem = await userCartDbService.getCartItemByUserAndProduct(userId, productId)
|
||||||
where: and(eq(cartItems.userId, userId), eq(cartItems.productId, productId)),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingItem) {
|
if (existingItem) {
|
||||||
// Update quantity
|
// Update quantity
|
||||||
await db.update(cartItems)
|
await userCartDbService.incrementCartItemQuantity(existingItem.id, quantity)
|
||||||
.set({
|
|
||||||
quantity: sql`${cartItems.quantity} + ${quantity}`,
|
|
||||||
})
|
|
||||||
.where(eq(cartItems.id, existingItem.id));
|
|
||||||
} else {
|
} else {
|
||||||
// Insert new item
|
// Insert new item
|
||||||
await db.insert(cartItems).values({
|
await userCartDbService.createCartItem(userId, productId, quantity)
|
||||||
userId,
|
|
||||||
productId,
|
|
||||||
quantity: quantity.toString(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return updated cart
|
// Return updated cart
|
||||||
|
|
@ -122,20 +98,17 @@ export const cartRouter = router({
|
||||||
quantity: z.number().int().min(0),
|
quantity: z.number().int().min(0),
|
||||||
}))
|
}))
|
||||||
.mutation(async ({ input, ctx }): Promise<CartResponse> => {
|
.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;
|
const { itemId, quantity } = input;
|
||||||
|
|
||||||
if (!quantity || quantity <= 0) {
|
if (!quantity || quantity <= 0) {
|
||||||
throw new ApiError("Positive quantity required", 400);
|
throw new ApiError("Positive quantity required", 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [updatedItem] = await db.update(cartItems)
|
const updatedItem = await userCartDbService.updateCartItemQuantity(itemId, userId, quantity)
|
||||||
.set({ quantity: quantity.toString() })
|
|
||||||
.where(and(
|
|
||||||
eq(cartItems.id, itemId),
|
|
||||||
eq(cartItems.userId, userId)
|
|
||||||
))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
if (!updatedItem) {
|
if (!updatedItem) {
|
||||||
throw new ApiError("Cart item not found", 404);
|
throw new ApiError("Cart item not found", 404);
|
||||||
|
|
@ -150,15 +123,13 @@ export const cartRouter = router({
|
||||||
itemId: z.number().int().positive(),
|
itemId: z.number().int().positive(),
|
||||||
}))
|
}))
|
||||||
.mutation(async ({ input, ctx }): Promise<CartResponse> => {
|
.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 { itemId } = input;
|
||||||
|
|
||||||
const [deletedItem] = await db.delete(cartItems)
|
const deletedItem = await userCartDbService.deleteCartItem(itemId, userId)
|
||||||
.where(and(
|
|
||||||
eq(cartItems.id, itemId),
|
|
||||||
eq(cartItems.userId, userId)
|
|
||||||
))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
if (!deletedItem) {
|
if (!deletedItem) {
|
||||||
throw new ApiError("Cart item not found", 404);
|
throw new ApiError("Cart item not found", 404);
|
||||||
|
|
@ -170,9 +141,12 @@ export const cartRouter = router({
|
||||||
|
|
||||||
clearCart: protectedProcedure
|
clearCart: protectedProcedure
|
||||||
.mutation(async ({ ctx }) => {
|
.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 {
|
return {
|
||||||
items: [],
|
items: [],
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,17 @@
|
||||||
import { router, protectedProcedure } from '@/src/trpc/trpc-index';
|
import { router, protectedProcedure } from '@/src/trpc/trpc-index';
|
||||||
import { z } from 'zod';
|
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 { scaffoldAssetUrl, claimUploadUrl } from '@/src/lib/s3-client';
|
||||||
|
import { userComplaintDbService } from '@/src/trpc/apis/user-apis/dataAccessors/main'
|
||||||
|
|
||||||
export const complaintRouter = router({
|
export const complaintRouter = router({
|
||||||
getAll: protectedProcedure
|
getAll: protectedProcedure
|
||||||
.query(async ({ ctx }) => {
|
.query(async ({ ctx }) => {
|
||||||
const userId = ctx.user.userId;
|
const userId = ctx.user?.userId;
|
||||||
|
if (!userId) {
|
||||||
|
throw new Error('Unauthorized')
|
||||||
|
}
|
||||||
|
|
||||||
const userComplaints = await db
|
const userComplaints = await userComplaintDbService.getComplaintsByUserId(userId)
|
||||||
.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);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
complaints: userComplaints.map(c => ({
|
complaints: userComplaints.map(c => ({
|
||||||
|
|
@ -44,10 +33,13 @@ export const complaintRouter = router({
|
||||||
imageKeys: z.array(z.string()).optional(),
|
imageKeys: z.array(z.string()).optional(),
|
||||||
}))
|
}))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.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;
|
const { orderId, complaintBody, imageKeys } = input;
|
||||||
|
|
||||||
await db.insert(complaints).values({
|
await userComplaintDbService.createComplaint({
|
||||||
userId,
|
userId,
|
||||||
orderId: orderId || null,
|
orderId: orderId || null,
|
||||||
complaintBody: complaintBody.trim(),
|
complaintBody: complaintBody.trim(),
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,9 @@
|
||||||
import { router, protectedProcedure } from '@/src/trpc/trpc-index';
|
import { router, protectedProcedure } from '@/src/trpc/trpc-index';
|
||||||
import { z } from 'zod';
|
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 { ApiError } from '@/src/lib/api-error';
|
||||||
|
import { userCouponDbService } from '@/src/trpc/apis/user-apis/dataAccessors/main'
|
||||||
|
|
||||||
import { users } from '@/src/db/schema';
|
type CouponWithRelations = import('@/src/trpc/apis/user-apis/dataAccessors/interfaces/user-coupon-db-service.interface').CouponWithRelations
|
||||||
|
|
||||||
type CouponWithRelations = typeof coupons.$inferSelect & {
|
|
||||||
applicableUsers: (typeof couponApplicableUsers.$inferSelect & { user: typeof users.$inferSelect })[];
|
|
||||||
usages: typeof couponUsage.$inferSelect[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface EligibleCoupon {
|
export interface EligibleCoupon {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -65,33 +58,13 @@ export const userCouponRouter = router({
|
||||||
.query(async ({ ctx }) => {
|
.query(async ({ ctx }) => {
|
||||||
try {
|
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
|
// Get all active, non-expired coupons
|
||||||
const allCoupons = await db.query.coupons.findMany({
|
const allCoupons = await userCouponDbService.getActiveCouponsForUser(userId)
|
||||||
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
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Filter to only coupons applicable to current user
|
// Filter to only coupons applicable to current user
|
||||||
const applicableCoupons = allCoupons.filter(coupon => {
|
const applicableCoupons = allCoupons.filter(coupon => {
|
||||||
|
|
@ -111,34 +84,14 @@ export const userCouponRouter = router({
|
||||||
getProductCoupons: protectedProcedure
|
getProductCoupons: protectedProcedure
|
||||||
.input(z.object({ productId: z.number().int().positive() }))
|
.input(z.object({ productId: z.number().int().positive() }))
|
||||||
.query(async ({ input, ctx }) => {
|
.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;
|
const { productId } = input;
|
||||||
|
|
||||||
// Get all active, non-expired coupons
|
// Get all active, non-expired coupons
|
||||||
const allCoupons = await db.query.coupons.findMany({
|
const allCoupons = await userCouponDbService.getActiveCouponsForUser(userId)
|
||||||
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
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Filter to only coupons applicable to current user and product
|
// Filter to only coupons applicable to current user and product
|
||||||
const applicableCoupons = allCoupons.filter(coupon => {
|
const applicableCoupons = allCoupons.filter(coupon => {
|
||||||
|
|
@ -156,21 +109,13 @@ export const userCouponRouter = router({
|
||||||
|
|
||||||
getMyCoupons: protectedProcedure
|
getMyCoupons: protectedProcedure
|
||||||
.query(async ({ ctx }) => {
|
.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
|
// Get all coupons
|
||||||
const allCoupons = await db.query.coupons.findMany({
|
const allCoupons = await userCouponDbService.getAllCouponsForUser(userId)
|
||||||
with: {
|
|
||||||
usages: {
|
|
||||||
where: eq(couponUsage.userId, userId)
|
|
||||||
},
|
|
||||||
applicableUsers: {
|
|
||||||
with: {
|
|
||||||
user: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Filter coupons in JS: not invalidated, applicable to user, and not expired
|
// Filter coupons in JS: not invalidated, applicable to user, and not expired
|
||||||
const applicableCoupons = (allCoupons as CouponWithRelations[]).filter(coupon => {
|
const applicableCoupons = (allCoupons as CouponWithRelations[]).filter(coupon => {
|
||||||
|
|
@ -226,16 +171,14 @@ export const userCouponRouter = router({
|
||||||
redeemReservedCoupon: protectedProcedure
|
redeemReservedCoupon: protectedProcedure
|
||||||
.input(z.object({ secretCode: z.string() }))
|
.input(z.object({ secretCode: z.string() }))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.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;
|
const { secretCode } = input;
|
||||||
|
|
||||||
// Find the reserved coupon
|
// Find the reserved coupon
|
||||||
const reservedCoupon = await db.query.reservedCoupons.findFirst({
|
const reservedCoupon = await userCouponDbService.getReservedCouponBySecretCode(secretCode)
|
||||||
where: and(
|
|
||||||
eq(reservedCoupons.secretCode, secretCode.toUpperCase()),
|
|
||||||
eq(reservedCoupons.isRedeemed, false)
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!reservedCoupon) {
|
if (!reservedCoupon) {
|
||||||
throw new ApiError("Invalid or already redeemed coupon code", 400);
|
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
|
// Create the coupon in the main table
|
||||||
const couponResult = await db.transaction(async (tx) => {
|
const couponResult = await userCouponDbService.redeemReservedCoupon(userId, reservedCoupon)
|
||||||
// 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;
|
|
||||||
});
|
|
||||||
|
|
||||||
return { success: true, coupon: couponResult };
|
return { success: true, coupon: couponResult };
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,22 @@
|
||||||
import { router, protectedProcedure } from "@/src/trpc/trpc-index";
|
import { z } from 'zod'
|
||||||
import { z } from "zod";
|
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
|
||||||
import { db } from "@/src/db/db_index";
|
import { userOrderDbService } from '@/src/trpc/apis/user-apis/dataAccessors/main'
|
||||||
import {
|
import type {
|
||||||
orders,
|
OrderCoupon,
|
||||||
orderItems,
|
OrderInsert,
|
||||||
orderStatus,
|
OrderItemInsert,
|
||||||
addresses,
|
OrderStatusInsert,
|
||||||
productInfo,
|
} from '@/src/trpc/apis/user-apis/dataAccessors/main'
|
||||||
paymentInfoTable,
|
import { scaffoldAssetUrl } from '@/src/lib/s3-client'
|
||||||
coupons,
|
import { ApiError } from '@/src/lib/api-error'
|
||||||
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 {
|
import {
|
||||||
sendOrderPlacedNotification,
|
sendOrderPlacedNotification,
|
||||||
sendOrderCancelledNotification,
|
sendOrderCancelledNotification,
|
||||||
} from "@/src/lib/notif-job";
|
} from '@/src/lib/notif-job'
|
||||||
import { RazorpayPaymentService } from "@/src/lib/payments-utils";
|
import { getNextDeliveryDate } from '@/src/trpc/apis/common-apis/common'
|
||||||
import { getNextDeliveryDate } from "@/src/trpc/apis/common-apis/common";
|
import { CONST_KEYS, getConstant, getConstants } from '@/src/lib/const-store'
|
||||||
import { CONST_KEYS, getConstant, getConstants } from "@/src/lib/const-store";
|
import { publishFormattedOrder, publishCancellation } from '@/src/lib/post-order-handler'
|
||||||
import { publishFormattedOrder, publishCancellation } from "@/src/lib/post-order-handler";
|
import { getSlotById } from '@/src/stores/slot-store'
|
||||||
import { getSlotById } from "@/src/stores/slot-store";
|
|
||||||
|
|
||||||
|
|
||||||
const validateAndGetCoupon = async (
|
const validateAndGetCoupon = async (
|
||||||
|
|
@ -35,40 +24,35 @@ const validateAndGetCoupon = async (
|
||||||
userId: number,
|
userId: number,
|
||||||
totalAmount: number
|
totalAmount: number
|
||||||
) => {
|
) => {
|
||||||
if (!couponId) return null;
|
if (!couponId) return null
|
||||||
|
|
||||||
const coupon = await db.query.coupons.findFirst({
|
const coupon = await userOrderDbService.getCouponWithUsage(couponId, userId)
|
||||||
where: eq(coupons.id, couponId),
|
|
||||||
with: {
|
|
||||||
usages: { where: eq(couponUsage.userId, userId) },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!coupon) throw new ApiError("Invalid coupon", 400);
|
if (!coupon) throw new ApiError('Invalid coupon', 400)
|
||||||
if (coupon.isInvalidated)
|
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())
|
if (coupon.validTill && new Date(coupon.validTill) < new Date())
|
||||||
throw new ApiError("Coupon has expired", 400);
|
throw new ApiError('Coupon has expired', 400)
|
||||||
if (
|
if (
|
||||||
coupon.maxLimitForUser &&
|
coupon.maxLimitForUser &&
|
||||||
coupon.usages.length >= coupon.maxLimitForUser
|
coupon.usages.length >= coupon.maxLimitForUser
|
||||||
)
|
)
|
||||||
throw new ApiError("Coupon usage limit exceeded", 400);
|
throw new ApiError('Coupon usage limit exceeded', 400)
|
||||||
if (
|
if (
|
||||||
coupon.minOrder &&
|
coupon.minOrder &&
|
||||||
parseFloat(coupon.minOrder.toString()) > totalAmount
|
parseFloat(coupon.minOrder.toString()) > totalAmount
|
||||||
)
|
)
|
||||||
throw new ApiError(
|
throw new ApiError(
|
||||||
"Order amount does not meet coupon minimum requirement",
|
'Order amount does not meet coupon minimum requirement',
|
||||||
400
|
400
|
||||||
);
|
)
|
||||||
|
|
||||||
return coupon;
|
return coupon
|
||||||
};
|
};
|
||||||
|
|
||||||
const applyDiscountToOrder = (
|
const applyDiscountToOrder = (
|
||||||
orderTotal: number,
|
orderTotal: number,
|
||||||
appliedCoupon: typeof coupons.$inferSelect | null,
|
appliedCoupon: OrderCoupon | null,
|
||||||
proportion: number
|
proportion: number
|
||||||
) => {
|
) => {
|
||||||
let finalOrderTotal = orderTotal;
|
let finalOrderTotal = orderTotal;
|
||||||
|
|
@ -140,11 +124,9 @@ const placeOrderUtil = async (params: {
|
||||||
|
|
||||||
const orderGroupId = `${Date.now()}-${userId}`;
|
const orderGroupId = `${Date.now()}-${userId}`;
|
||||||
|
|
||||||
const address = await db.query.addresses.findFirst({
|
const address = await userOrderDbService.getAddressByUserId(userId, addressId)
|
||||||
where: and(eq(addresses.userId, userId), eq(addresses.id, addressId)),
|
|
||||||
});
|
|
||||||
if (!address) {
|
if (!address) {
|
||||||
throw new ApiError("Invalid address", 400);
|
throw new ApiError('Invalid address', 400)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ordersBySlot = new Map<
|
const ordersBySlot = new Map<
|
||||||
|
|
@ -158,11 +140,9 @@ const placeOrderUtil = async (params: {
|
||||||
>();
|
>();
|
||||||
|
|
||||||
for (const item of selectedItems) {
|
for (const item of selectedItems) {
|
||||||
const product = await db.query.productInfo.findFirst({
|
const product = await userOrderDbService.getProductById(item.productId)
|
||||||
where: eq(productInfo.id, item.productId),
|
|
||||||
});
|
|
||||||
if (!product) {
|
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)) {
|
if (!ordersBySlot.has(item.slotId)) {
|
||||||
|
|
@ -173,11 +153,12 @@ const placeOrderUtil = async (params: {
|
||||||
|
|
||||||
if (params.isFlash) {
|
if (params.isFlash) {
|
||||||
for (const item of selectedItems) {
|
for (const item of selectedItems) {
|
||||||
const product = await db.query.productInfo.findFirst({
|
const product = await userOrderDbService.getProductById(item.productId)
|
||||||
where: eq(productInfo.id, item.productId),
|
|
||||||
});
|
|
||||||
if (!product?.isFlashAvailable) {
|
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;
|
const totalWithDelivery = totalAmount + expectedDeliveryCharge;
|
||||||
|
|
||||||
type OrderData = {
|
type OrderData = {
|
||||||
order: Omit<typeof orders.$inferInsert, "id">;
|
order: Omit<OrderInsert, 'id'>
|
||||||
orderItems: Omit<typeof orderItems.$inferInsert, "id">[];
|
orderItems: Omit<OrderItemInsert, 'id'>[]
|
||||||
orderStatus: Omit<typeof orderStatus.$inferInsert, "id">;
|
orderStatus: Omit<OrderStatusInsert, 'id'>
|
||||||
};
|
}
|
||||||
|
|
||||||
const ordersData: OrderData[] = [];
|
const ordersData: OrderData[] = [];
|
||||||
let isFirstOrder = true;
|
let isFirstOrder = true;
|
||||||
|
|
@ -233,7 +214,7 @@ const placeOrderUtil = async (params: {
|
||||||
orderGroupProportion
|
orderGroupProportion
|
||||||
);
|
);
|
||||||
|
|
||||||
const order: Omit<typeof orders.$inferInsert, "id"> = {
|
const order: Omit<OrderInsert, 'id'> = {
|
||||||
userId,
|
userId,
|
||||||
addressId,
|
addressId,
|
||||||
slotId: params.isFlash ? null : slotId,
|
slotId: params.isFlash ? null : slotId,
|
||||||
|
|
@ -249,7 +230,7 @@ const placeOrderUtil = async (params: {
|
||||||
isFlashDelivery: params.isFlash,
|
isFlashDelivery: params.isFlash,
|
||||||
};
|
};
|
||||||
|
|
||||||
const orderItemsData: Omit<typeof orderItems.$inferInsert, "id">[] = items.map(
|
const orderItemsData: Omit<OrderItemInsert, 'id'>[] = items.map(
|
||||||
(item) => ({
|
(item) => ({
|
||||||
orderId: 0,
|
orderId: 0,
|
||||||
productId: item.productId,
|
productId: item.productId,
|
||||||
|
|
@ -265,7 +246,7 @@ const placeOrderUtil = async (params: {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const orderStatusData: Omit<typeof orderStatus.$inferInsert, "id"> = {
|
const orderStatusData: Omit<OrderStatusInsert, 'id'> = {
|
||||||
userId,
|
userId,
|
||||||
orderId: 0,
|
orderId: 0,
|
||||||
paymentStatus: paymentMethod === "cod" ? "cod" : "pending",
|
paymentStatus: paymentMethod === "cod" ? "cod" : "pending",
|
||||||
|
|
@ -275,79 +256,22 @@ const placeOrderUtil = async (params: {
|
||||||
isFirstOrder = false;
|
isFirstOrder = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const createdOrders = await db.transaction(async (tx) => {
|
const createdOrders = await userOrderDbService.createOrdersWithItems({
|
||||||
let sharedPaymentInfoId: number | null = null;
|
ordersData,
|
||||||
if (paymentMethod === "online") {
|
paymentMethod,
|
||||||
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(
|
await userOrderDbService.deleteCartItemsByUserAndProductIds(
|
||||||
(od) => ({
|
userId,
|
||||||
...od.order,
|
selectedItems.map((item) => item.productId)
|
||||||
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,
|
|
||||||
selectedItems.map((item) => item.productId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (appliedCoupon && createdOrders.length > 0) {
|
if (appliedCoupon && createdOrders.length > 0) {
|
||||||
await db.insert(couponUsage).values({
|
await userOrderDbService.createCouponUsage({
|
||||||
userId,
|
userId,
|
||||||
couponId: appliedCoupon.id,
|
couponId: appliedCoupon.id,
|
||||||
orderId: createdOrders[0].id as number,
|
orderId: createdOrders[0].id as number,
|
||||||
orderItemId: null,
|
})
|
||||||
usedAt: new Date(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const order of createdOrders) {
|
for (const order of createdOrders) {
|
||||||
|
|
@ -378,12 +302,13 @@ export const orderRouter = router({
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.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
|
// Check if user is suspended from placing orders
|
||||||
const userDetail = await db.query.userDetails.findFirst({
|
const userDetail = await userOrderDbService.getUserDetailByUserId(userId)
|
||||||
where: eq(userDetails.userId, userId),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (userDetail?.isSuspended) {
|
if (userDetail?.isSuspended) {
|
||||||
throw new ApiError("Unable to place order", 403);
|
throw new ApiError("Unable to place order", 403);
|
||||||
|
|
@ -402,7 +327,10 @@ export const orderRouter = router({
|
||||||
if (isFlashDelivery) {
|
if (isFlashDelivery) {
|
||||||
const isFlashDeliveryEnabled = await getConstant<boolean>(CONST_KEYS.isFlashDeliveryEnabled);
|
const isFlashDeliveryEnabled = await getConstant<boolean>(CONST_KEYS.isFlashDeliveryEnabled);
|
||||||
if (!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) {
|
if (!isFlashDelivery) {
|
||||||
const slotIds = [...new Set(selectedItems.filter(i => i.slotId !== null).map(i => i.slotId as number))];
|
const slotIds = [...new Set(selectedItems.filter(i => i.slotId !== null).map(i => i.slotId as number))];
|
||||||
for (const slotId of slotIds) {
|
for (const slotId of slotIds) {
|
||||||
const slot = await getSlotById(slotId);
|
const slot = await getSlotById(slotId)
|
||||||
if (slot?.isCapacityFull) {
|
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
|
// Handle flash delivery slot resolution
|
||||||
if (isFlashDelivery) {
|
if (isFlashDelivery) {
|
||||||
// For flash delivery, set slotId to null (no specific slot assigned)
|
// For flash delivery, set slotId to null (no specific slot assigned)
|
||||||
processedItems = selectedItems.map(item => ({
|
processedItems = selectedItems.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
slotId: null as any, // Type override for flash delivery
|
slotId: null as any, // Type override for flash delivery
|
||||||
}));
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
return await placeOrderUtil({
|
return await placeOrderUtil({
|
||||||
|
|
@ -450,33 +381,20 @@ export const orderRouter = router({
|
||||||
)
|
)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const { page = 1, pageSize = 10 } = input || {};
|
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;
|
const offset = (page - 1) * pageSize;
|
||||||
|
|
||||||
// Get total count for pagination
|
// Get total count for pagination
|
||||||
const totalCountResult = await db.$count(
|
const totalCount = await userOrderDbService.getOrdersCount(userId)
|
||||||
orders,
|
|
||||||
eq(orders.userId, userId)
|
|
||||||
);
|
|
||||||
const totalCount = totalCountResult;
|
|
||||||
|
|
||||||
const userOrders = await db.query.orders.findMany({
|
const userOrders = await userOrderDbService.getOrdersWithRelations(
|
||||||
where: eq(orders.userId, userId),
|
userId,
|
||||||
with: {
|
pageSize,
|
||||||
orderItems: {
|
offset
|
||||||
with: {
|
)
|
||||||
product: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
slot: true,
|
|
||||||
paymentInfo: true,
|
|
||||||
orderStatus: true,
|
|
||||||
refunds: true,
|
|
||||||
},
|
|
||||||
orderBy: (orders, { desc }) => [desc(orders.createdAt)],
|
|
||||||
limit: pageSize,
|
|
||||||
offset: offset,
|
|
||||||
});
|
|
||||||
|
|
||||||
const mappedOrders = await Promise.all(
|
const mappedOrders = await Promise.all(
|
||||||
userOrders.map(async (order) => {
|
userOrders.map(async (order) => {
|
||||||
|
|
@ -574,38 +492,24 @@ export const orderRouter = router({
|
||||||
.input(z.object({ orderId: z.string() }))
|
.input(z.object({ orderId: z.string() }))
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const { orderId } = input;
|
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({
|
const order = await userOrderDbService.getOrderWithDetailsById(
|
||||||
where: and(eq(orders.id, parseInt(orderId)), eq(orders.userId, userId)),
|
parseInt(orderId),
|
||||||
with: {
|
userId
|
||||||
orderItems: {
|
)
|
||||||
with: {
|
|
||||||
product: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
slot: true,
|
|
||||||
paymentInfo: true,
|
|
||||||
orderStatus: {
|
|
||||||
with: {
|
|
||||||
refundCoupon: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
refunds: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!order) {
|
if (!order) {
|
||||||
throw new Error("Order not found");
|
throw new Error("Order not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get coupon usage for this specific order using new orderId field
|
// Get coupon usage for this specific order using new orderId field
|
||||||
const couponUsageData = await db.query.couponUsage.findMany({
|
const couponUsageData = await userOrderDbService.getCouponUsagesByOrderId(
|
||||||
where: eq(couponUsage.orderId, order.id), // Use new orderId field
|
order.id
|
||||||
with: {
|
)
|
||||||
coupon: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
let couponData = null;
|
let couponData = null;
|
||||||
if (couponUsageData.length > 0) {
|
if (couponUsageData.length > 0) {
|
||||||
|
|
@ -734,16 +638,14 @@ export const orderRouter = router({
|
||||||
)
|
)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
const userId = ctx.user.userId;
|
const userId = ctx.user?.userId
|
||||||
|
if (!userId) {
|
||||||
|
throw new ApiError('Unauthorized', 401)
|
||||||
|
}
|
||||||
const { id, reason } = input;
|
const { id, reason } = input;
|
||||||
|
|
||||||
// Check if order exists and belongs to user
|
// Check if order exists and belongs to user
|
||||||
const order = await db.query.orders.findFirst({
|
const order = await userOrderDbService.getOrderWithStatus(Number(id))
|
||||||
where: eq(orders.id, Number(id)),
|
|
||||||
with: {
|
|
||||||
orderStatus: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!order) {
|
if (!order) {
|
||||||
console.error("Order not found:", id);
|
console.error("Order not found:", id);
|
||||||
|
|
@ -777,29 +679,17 @@ export const orderRouter = router({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform database operations in transaction
|
// Perform database operations in transaction
|
||||||
const result = await db.transaction(async (tx) => {
|
// Determine refund status based on payment method
|
||||||
// Update order status
|
const refundStatus = order.isCod ? 'na' : 'pending'
|
||||||
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
|
await userOrderDbService.cancelOrderTransaction({
|
||||||
const refundStatus = order.isCod ? "na" : "pending";
|
statusId: status.id,
|
||||||
|
reason,
|
||||||
|
orderId: order.id,
|
||||||
|
refundStatus,
|
||||||
|
})
|
||||||
|
|
||||||
// Insert refund record
|
const result = { orderId: order.id, userId }
|
||||||
await tx.insert(refunds).values({
|
|
||||||
orderId: order.id,
|
|
||||||
refundStatus,
|
|
||||||
});
|
|
||||||
|
|
||||||
return { orderId: order.id, userId };
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send notification outside transaction (idempotent operation)
|
// Send notification outside transaction (idempotent operation)
|
||||||
await sendOrderCancelledNotification(
|
await sendOrderCancelledNotification(
|
||||||
|
|
@ -810,10 +700,10 @@ export const orderRouter = router({
|
||||||
// Publish to Redis for Telegram notification
|
// Publish to Redis for Telegram notification
|
||||||
await publishCancellation(result.orderId, 'user', reason);
|
await publishCancellation(result.orderId, 'user', reason);
|
||||||
|
|
||||||
return { success: true, message: "Order cancelled successfully" };
|
return { success: true, message: 'Order cancelled successfully' }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(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 }) => {
|
.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;
|
const { id, userNotes } = input;
|
||||||
|
|
||||||
// Extract readable ID from orderId (e.g., ORD001 -> 1)
|
// Extract readable ID from orderId (e.g., ORD001 -> 1)
|
||||||
|
|
@ -837,12 +730,7 @@ export const orderRouter = router({
|
||||||
// const readableId = parseInt(readableIdMatch[1]);
|
// const readableId = parseInt(readableIdMatch[1]);
|
||||||
|
|
||||||
// Check if order exists and belongs to user
|
// Check if order exists and belongs to user
|
||||||
const order = await db.query.orders.findFirst({
|
const order = await userOrderDbService.getOrderWithStatus(Number(id))
|
||||||
where: eq(orders.id, Number(id)),
|
|
||||||
with: {
|
|
||||||
orderStatus: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!order) {
|
if (!order) {
|
||||||
console.error("Order not found:", id);
|
console.error("Order not found:", id);
|
||||||
|
|
@ -876,14 +764,9 @@ export const orderRouter = router({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update user notes
|
// Update user notes
|
||||||
await db
|
await userOrderDbService.updateOrderNotes(order.id, userNotes || null)
|
||||||
.update(orders)
|
|
||||||
.set({
|
|
||||||
userNotes: userNotes || null,
|
|
||||||
})
|
|
||||||
.where(eq(orders.id, order.id));
|
|
||||||
|
|
||||||
return { success: true, message: "Notes updated successfully" };
|
return { success: true, message: 'Notes updated successfully' }
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getRecentlyOrderedProducts: protectedProcedure
|
getRecentlyOrderedProducts: protectedProcedure
|
||||||
|
|
@ -896,25 +779,20 @@ export const orderRouter = router({
|
||||||
)
|
)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const { limit = 20 } = input || {};
|
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)
|
// Get user's recent delivered orders (last 30 days)
|
||||||
const thirtyDaysAgo = new Date();
|
const thirtyDaysAgo = new Date();
|
||||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||||
|
|
||||||
const recentOrders = await db
|
const recentOrders = await userOrderDbService.getRecentDeliveredOrderIds(
|
||||||
.select({ id: orders.id })
|
userId,
|
||||||
.from(orders)
|
thirtyDaysAgo,
|
||||||
.innerJoin(orderStatus, eq(orders.id, orderStatus.orderId))
|
10
|
||||||
.where(
|
)
|
||||||
and(
|
|
||||||
eq(orders.userId, userId),
|
|
||||||
eq(orderStatus.isDelivered, true),
|
|
||||||
gte(orders.createdAt, thirtyDaysAgo)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.orderBy(desc(orders.createdAt))
|
|
||||||
.limit(10); // Get last 10 orders
|
|
||||||
|
|
||||||
if (recentOrders.length === 0) {
|
if (recentOrders.length === 0) {
|
||||||
return { success: true, products: [] };
|
return { success: true, products: [] };
|
||||||
|
|
@ -923,10 +801,9 @@ export const orderRouter = router({
|
||||||
const orderIds = recentOrders.map((order) => order.id);
|
const orderIds = recentOrders.map((order) => order.id);
|
||||||
|
|
||||||
// Get unique product IDs from recent orders
|
// Get unique product IDs from recent orders
|
||||||
const orderItemsResult = await db
|
const orderItemsResult = await userOrderDbService.getProductIdsByOrderIds(
|
||||||
.select({ productId: orderItems.productId })
|
orderIds
|
||||||
.from(orderItems)
|
)
|
||||||
.where(inArray(orderItems.orderId, orderIds));
|
|
||||||
|
|
||||||
const productIds = [
|
const productIds = [
|
||||||
...new Set(orderItemsResult.map((item) => item.productId)),
|
...new Set(orderItemsResult.map((item) => item.productId)),
|
||||||
|
|
@ -937,27 +814,10 @@ export const orderRouter = router({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get product details
|
// Get product details
|
||||||
const productsWithUnits = await db
|
const productsWithUnits = await userOrderDbService.getProductsWithUnitsByIds(
|
||||||
.select({
|
productIds,
|
||||||
id: productInfo.id,
|
limit
|
||||||
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);
|
|
||||||
|
|
||||||
// Generate signed URLs for product images
|
// Generate signed URLs for product images
|
||||||
const formattedProducts = await Promise.all(
|
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 { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index';
|
||||||
import { z } from 'zod';
|
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 { claimUploadUrl, extractKeyFromPresignedUrl, scaffoldAssetUrl } from '@/src/lib/s3-client';
|
||||||
import { ApiError } from '@/src/lib/api-error';
|
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 { getProductById as getProductByIdFromCache, getAllProducts as getAllProductsFromCache } from '@/src/stores/product-store';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import { userProductDbService } from '@/src/trpc/apis/user-apis/dataAccessors/main'
|
||||||
|
|
||||||
// Uniform Product Type
|
// Uniform Product Type
|
||||||
interface Product {
|
interface Product {
|
||||||
|
|
@ -60,75 +58,20 @@ export const productRouter = router({
|
||||||
}
|
}
|
||||||
|
|
||||||
// If not in cache, fetch from database (fallback)
|
// If not in cache, fetch from database (fallback)
|
||||||
const productData = await db
|
const product = await userProductDbService.getProductById(productId)
|
||||||
.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);
|
|
||||||
|
|
||||||
if (productData.length === 0) {
|
if (!product) {
|
||||||
throw new Error('Product not found');
|
throw new Error('Product not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const product = productData[0];
|
|
||||||
|
|
||||||
// Fetch store info for this product
|
// Fetch store info for this product
|
||||||
const storeData = product.storeId ? await db.query.storeInfo.findFirst({
|
const storeData = product.storeId ? await userProductDbService.getStoreBasicById(product.storeId) : null
|
||||||
where: eq(storeInfo.id, product.storeId),
|
|
||||||
columns: { id: true, name: true, description: true },
|
|
||||||
}) : null;
|
|
||||||
|
|
||||||
// Fetch delivery slots for this product
|
// Fetch delivery slots for this product
|
||||||
const deliverySlotsData = await db
|
const deliverySlotsData = await userProductDbService.getDeliverySlotsForProduct(productId)
|
||||||
.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);
|
|
||||||
|
|
||||||
// Fetch special deals for this product
|
// Fetch special deals for this product
|
||||||
const specialDealsData = await db
|
const specialDealsData = await userProductDbService.getSpecialDealsForProduct(productId)
|
||||||
.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);
|
|
||||||
|
|
||||||
// Generate signed URLs for images
|
// Generate signed URLs for images
|
||||||
const signedImages = scaffoldAssetUrl((product.images as string[]) || []);
|
const signedImages = scaffoldAssetUrl((product.images as string[]) || []);
|
||||||
|
|
@ -140,7 +83,7 @@ export const productRouter = router({
|
||||||
longDescription: product.longDescription,
|
longDescription: product.longDescription,
|
||||||
price: product.price.toString(),
|
price: product.price.toString(),
|
||||||
marketPrice: product.marketPrice?.toString() || null,
|
marketPrice: product.marketPrice?.toString() || null,
|
||||||
unitNotation: product.unitShortNotation,
|
unitNotation: product.unitShortNotation || '',
|
||||||
images: signedImages,
|
images: signedImages,
|
||||||
isOutOfStock: product.isOutOfStock,
|
isOutOfStock: product.isOutOfStock,
|
||||||
store: storeData ? {
|
store: storeData ? {
|
||||||
|
|
@ -168,21 +111,7 @@ export const productRouter = router({
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const { productId, limit, offset } = input;
|
const { productId, limit, offset } = input;
|
||||||
|
|
||||||
const reviews = await db
|
const reviews = await userProductDbService.getProductReviews(productId, limit, offset)
|
||||||
.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);
|
|
||||||
|
|
||||||
// Generate signed URLs for images
|
// Generate signed URLs for images
|
||||||
const reviewsWithSignedUrls = await Promise.all(
|
const reviewsWithSignedUrls = await Promise.all(
|
||||||
|
|
@ -193,12 +122,7 @@ export const productRouter = router({
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if more reviews exist
|
// Check if more reviews exist
|
||||||
const totalCountResult = await db
|
const totalCount = await userProductDbService.getReviewCount(productId)
|
||||||
.select({ count: sql`count(*)` })
|
|
||||||
.from(productReviews)
|
|
||||||
.where(eq(productReviews.productId, productId));
|
|
||||||
|
|
||||||
const totalCount = Number(totalCountResult[0].count);
|
|
||||||
const hasMore = offset + limit < totalCount;
|
const hasMore = offset + limit < totalCount;
|
||||||
|
|
||||||
return { reviews: reviewsWithSignedUrls, hasMore };
|
return { reviews: reviewsWithSignedUrls, hasMore };
|
||||||
|
|
@ -214,24 +138,25 @@ export const productRouter = router({
|
||||||
}))
|
}))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const { productId, reviewBody, ratings, imageUrls, uploadUrls } = input;
|
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
|
// Optional: Check if product exists
|
||||||
const product = await db.query.productInfo.findFirst({
|
const product = await userProductDbService.getProductById(productId)
|
||||||
where: eq(productInfo.id, productId),
|
|
||||||
});
|
|
||||||
if (!product) {
|
if (!product) {
|
||||||
throw new ApiError('Product not found', 404);
|
throw new ApiError('Product not found', 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert review
|
// Insert review
|
||||||
const [newReview] = await db.insert(productReviews).values({
|
const newReview = await userProductDbService.createReview({
|
||||||
userId,
|
userId,
|
||||||
productId,
|
productId,
|
||||||
reviewBody,
|
reviewBody,
|
||||||
ratings,
|
ratings,
|
||||||
imageUrls: uploadUrls.map(item => extractKeyFromPresignedUrl(item)),
|
imageUrls: uploadUrls.map(item => extractKeyFromPresignedUrl(item)),
|
||||||
}).returning();
|
})
|
||||||
|
|
||||||
// Claim upload URLs
|
// Claim upload URLs
|
||||||
if (uploadUrls && uploadUrls.length > 0) {
|
if (uploadUrls && uploadUrls.length > 0) {
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,8 @@
|
||||||
import { router, publicProcedure } from "@/src/trpc/trpc-index";
|
import { router, publicProcedure } from "@/src/trpc/trpc-index";
|
||||||
import { z } from "zod";
|
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 { getAllSlots as getAllSlotsFromCache, getSlotById as getSlotByIdFromCache } from "@/src/stores/slot-store";
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import { userSlotDbService } from '@/src/trpc/apis/user-apis/dataAccessors/main'
|
||||||
|
|
||||||
// Helper method to get formatted slot data by ID
|
// Helper method to get formatted slot data by ID
|
||||||
async function getSlotData(slotId: number) {
|
async function getSlotData(slotId: number) {
|
||||||
|
|
@ -44,15 +37,7 @@ export async function scaffoldSlotsWithProducts() {
|
||||||
.sort((a, b) => dayjs(a.deliveryTime).valueOf() - dayjs(b.deliveryTime).valueOf());
|
.sort((a, b) => dayjs(a.deliveryTime).valueOf() - dayjs(b.deliveryTime).valueOf());
|
||||||
|
|
||||||
// Fetch all products for availability info
|
// Fetch all products for availability info
|
||||||
const allProducts = await db
|
const allProducts = await userSlotDbService.getProductAvailability()
|
||||||
.select({
|
|
||||||
id: productInfo.id,
|
|
||||||
name: productInfo.name,
|
|
||||||
isOutOfStock: productInfo.isOutOfStock,
|
|
||||||
isFlashAvailable: productInfo.isFlashAvailable,
|
|
||||||
})
|
|
||||||
.from(productInfo)
|
|
||||||
.where(eq(productInfo.isSuspended, false));
|
|
||||||
|
|
||||||
const productAvailability = allProducts.map(product => ({
|
const productAvailability = allProducts.map(product => ({
|
||||||
id: product.id,
|
id: product.id,
|
||||||
|
|
@ -70,9 +55,7 @@ export async function scaffoldSlotsWithProducts() {
|
||||||
|
|
||||||
export const slotsRouter = router({
|
export const slotsRouter = router({
|
||||||
getSlots: publicProcedure.query(async () => {
|
getSlots: publicProcedure.query(async () => {
|
||||||
const slots = await db.query.deliverySlotInfo.findMany({
|
const slots = await userSlotDbService.getActiveSlots()
|
||||||
where: eq(deliverySlotInfo.isActive, true),
|
|
||||||
});
|
|
||||||
return {
|
return {
|
||||||
slots,
|
slots,
|
||||||
count: slots.length,
|
count: slots.length,
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,12 @@
|
||||||
import { router, publicProcedure } from '@/src/trpc/trpc-index';
|
import { router, publicProcedure } from '@/src/trpc/trpc-index';
|
||||||
import { z } from 'zod';
|
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 { scaffoldAssetUrl } from '@/src/lib/s3-client';
|
||||||
import { ApiError } from '@/src/lib/api-error';
|
import { ApiError } from '@/src/lib/api-error';
|
||||||
import { getTagsByStoreId } from '@/src/stores/product-tag-store';
|
import { getTagsByStoreId } from '@/src/stores/product-tag-store';
|
||||||
|
import { userStoreDbService } from '@/src/trpc/apis/user-apis/dataAccessors/main'
|
||||||
|
|
||||||
export async function scaffoldStores() {
|
export async function scaffoldStores() {
|
||||||
const storesData = await db
|
const storesData = await userStoreDbService.getStoresWithProductCount()
|
||||||
.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);
|
|
||||||
|
|
||||||
// Generate signed URLs for store images and fetch sample products
|
// Generate signed URLs for store images and fetch sample products
|
||||||
const storesWithDetails = await Promise.all(
|
const storesWithDetails = await Promise.all(
|
||||||
|
|
@ -29,15 +14,7 @@ export async function scaffoldStores() {
|
||||||
const signedImageUrl = store.imageUrl ? scaffoldAssetUrl(store.imageUrl) : null;
|
const signedImageUrl = store.imageUrl ? scaffoldAssetUrl(store.imageUrl) : null;
|
||||||
|
|
||||||
// Fetch up to 3 products for this store
|
// Fetch up to 3 products for this store
|
||||||
const sampleProducts = await db
|
const sampleProducts = await userStoreDbService.getSampleProductsByStoreId(store.id, 3)
|
||||||
.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);
|
|
||||||
|
|
||||||
// Generate signed URLs for product images
|
// Generate signed URLs for product images
|
||||||
const productsWithSignedUrls = await Promise.all(
|
const productsWithSignedUrls = await Promise.all(
|
||||||
|
|
@ -69,15 +46,7 @@ export async function scaffoldStores() {
|
||||||
|
|
||||||
export async function scaffoldStoreWithProducts(storeId: number) {
|
export async function scaffoldStoreWithProducts(storeId: number) {
|
||||||
// Fetch store info
|
// Fetch store info
|
||||||
const storeData = await db.query.storeInfo.findFirst({
|
const storeData = await userStoreDbService.getStoreById(storeId)
|
||||||
where: eq(storeInfo.id, storeId),
|
|
||||||
columns: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
description: true,
|
|
||||||
imageUrl: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!storeData) {
|
if (!storeData) {
|
||||||
throw new ApiError('Store not found', 404);
|
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;
|
const signedImageUrl = storeData.imageUrl ? scaffoldAssetUrl(storeData.imageUrl) : null;
|
||||||
|
|
||||||
// Fetch products for this store
|
// Fetch products for this store
|
||||||
const productsData = await db
|
const productsData = await userStoreDbService.getStoreProductsWithUnits(storeId)
|
||||||
.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)));
|
|
||||||
|
|
||||||
|
|
||||||
// Generate signed URLs for product images
|
// 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 { slotsRouter } from '@/src/trpc/apis/user-apis/apis/slots';
|
||||||
import { userRouter as userDataRouter } from '@/src/trpc/apis/user-apis/apis/user';
|
import { userRouter as userDataRouter } from '@/src/trpc/apis/user-apis/apis/user';
|
||||||
import { userCouponRouter } from '@/src/trpc/apis/user-apis/apis/coupon';
|
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 { storesRouter } from '@/src/trpc/apis/user-apis/apis/stores';
|
||||||
import { fileUploadRouter } from '@/src/trpc/apis/user-apis/apis/file-upload';
|
import { fileUploadRouter } from '@/src/trpc/apis/user-apis/apis/file-upload';
|
||||||
import { tagsRouter } from '@/src/trpc/apis/user-apis/apis/tags';
|
import { tagsRouter } from '@/src/trpc/apis/user-apis/apis/tags';
|
||||||
|
|
@ -25,7 +24,6 @@ export const userRouter = router({
|
||||||
slots: slotsRouter,
|
slots: slotsRouter,
|
||||||
user: userDataRouter,
|
user: userDataRouter,
|
||||||
coupon: userCouponRouter,
|
coupon: userCouponRouter,
|
||||||
payment: paymentRouter,
|
|
||||||
stores: storesRouter,
|
stores: storesRouter,
|
||||||
fileUpload: fileUploadRouter,
|
fileUpload: fileUploadRouter,
|
||||||
tags: tagsRouter,
|
tags: tagsRouter,
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
import { router, protectedProcedure, publicProcedure } from '@/src/trpc/trpc-index';
|
import { router, protectedProcedure, publicProcedure } from '@/src/trpc/trpc-index';
|
||||||
import { eq, and } from 'drizzle-orm';
|
|
||||||
import { z } from 'zod';
|
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 { ApiError } from '@/src/lib/api-error';
|
||||||
import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client';
|
import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client';
|
||||||
import { signToken } from '@/src/lib/jwt-utils';
|
import { signToken } from '@/src/lib/jwt-utils';
|
||||||
|
import { userProfileDbService } from '@/src/trpc/apis/user-apis/dataAccessors/main'
|
||||||
|
|
||||||
interface AuthResponse {
|
interface AuthResponse {
|
||||||
token: string;
|
token: string;
|
||||||
|
|
@ -29,28 +27,20 @@ const generateToken = async (userId: number): Promise<string> => {
|
||||||
export const userRouter = router({
|
export const userRouter = router({
|
||||||
getSelfData: protectedProcedure
|
getSelfData: protectedProcedure
|
||||||
.query(async ({ ctx }) => {
|
.query(async ({ ctx }) => {
|
||||||
const userId = ctx.user.userId;
|
const userId = ctx.user?.userId;
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
throw new ApiError('User not authenticated', 401);
|
throw new ApiError('User not authenticated', 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [user] = await db
|
const user = await userProfileDbService.getUserById(userId)
|
||||||
.select()
|
|
||||||
.from(users)
|
|
||||||
.where(eq(users.id, userId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new ApiError('User not found', 404);
|
throw new ApiError('User not found', 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user details for profile image
|
// Get user details for profile image
|
||||||
const [userDetail] = await db
|
const userDetail = await userProfileDbService.getUserDetailByUserId(userId)
|
||||||
.select()
|
|
||||||
.from(userDetails)
|
|
||||||
.where(eq(userDetails.userId, userId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
// Generate signed URL for profile image if it exists
|
// Generate signed URL for profile image if it exists
|
||||||
const profileImageSignedUrl = userDetail?.profileImage
|
const profileImageSignedUrl = userDetail?.profileImage
|
||||||
|
|
@ -79,24 +69,19 @@ export const userRouter = router({
|
||||||
|
|
||||||
checkProfileComplete: protectedProcedure
|
checkProfileComplete: protectedProcedure
|
||||||
.query(async ({ ctx }) => {
|
.query(async ({ ctx }) => {
|
||||||
const userId = ctx.user.userId;
|
const userId = ctx.user?.userId;
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
throw new ApiError('User not authenticated', 401);
|
throw new ApiError('User not authenticated', 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await db
|
const result = await userProfileDbService.getUserWithCreds(userId)
|
||||||
.select()
|
|
||||||
.from(users)
|
|
||||||
.leftJoin(userCreds, eq(users.id, userCreds.userId))
|
|
||||||
.where(eq(users.id, userId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (result.length === 0) {
|
if (!result) {
|
||||||
throw new ApiError('User not found', 404);
|
throw new ApiError('User not found', 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { users: user, user_creds: creds } = result[0];
|
const { user, creds } = result
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isComplete: !!(user.name && user.email && creds),
|
isComplete: !!(user.name && user.email && creds),
|
||||||
|
|
@ -112,50 +97,28 @@ export const userRouter = router({
|
||||||
if (userId) {
|
if (userId) {
|
||||||
// AUTHENTICATED USER
|
// AUTHENTICATED USER
|
||||||
// Check if token exists in notif_creds for this user
|
// Check if token exists in notif_creds for this user
|
||||||
const existing = await db.query.notifCreds.findFirst({
|
const existing = await userProfileDbService.getNotifCredByUserAndToken(userId, token)
|
||||||
where: and(
|
|
||||||
eq(notifCreds.userId, userId),
|
|
||||||
eq(notifCreds.token, token)
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
// Update lastVerified timestamp
|
// Update lastVerified timestamp
|
||||||
await db
|
await userProfileDbService.updateNotifCredLastVerified(existing.id)
|
||||||
.update(notifCreds)
|
|
||||||
.set({ lastVerified: new Date() })
|
|
||||||
.where(eq(notifCreds.id, existing.id));
|
|
||||||
} else {
|
} else {
|
||||||
// Insert new token into notif_creds
|
// Insert new token into notif_creds
|
||||||
await db.insert(notifCreds).values({
|
await userProfileDbService.insertNotifCred(userId, token)
|
||||||
userId,
|
|
||||||
token,
|
|
||||||
lastVerified: new Date(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove from unlogged_user_tokens if it exists
|
// Remove from unlogged_user_tokens if it exists
|
||||||
await db
|
await userProfileDbService.deleteUnloggedToken(token)
|
||||||
.delete(unloggedUserTokens)
|
|
||||||
.where(eq(unloggedUserTokens.token, token));
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// UNAUTHENTICATED USER
|
// UNAUTHENTICATED USER
|
||||||
// Save/update in unlogged_user_tokens
|
// Save/update in unlogged_user_tokens
|
||||||
const existing = await db.query.unloggedUserTokens.findFirst({
|
const existing = await userProfileDbService.getUnloggedToken(token)
|
||||||
where: eq(unloggedUserTokens.token, token),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
await db
|
await userProfileDbService.updateUnloggedTokenLastVerified(existing.id)
|
||||||
.update(unloggedUserTokens)
|
|
||||||
.set({ lastVerified: new Date() })
|
|
||||||
.where(eq(unloggedUserTokens.id, existing.id));
|
|
||||||
} else {
|
} else {
|
||||||
await db.insert(unloggedUserTokens).values({
|
await userProfileDbService.insertUnloggedToken(token)
|
||||||
token,
|
|
||||||
lastVerified: new Date(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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