Compare commits

..

No commits in common. "95d2c861c0aab00ffc8f5ef5badec39b348d32bc" and "cd5ab79f4457457a25ac74da66e3fe6144e8045d" have entirely different histories.

138 changed files with 5227 additions and 8953 deletions

File diff suppressed because one or more lines are too long

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
import { drizzle } from "drizzle-orm/node-postgres"
import { migrate } from "drizzle-orm/node-postgres/migrator"
import path from "path"
import * as schema from "@/src/db/schema-postgres"
import * as schema from "@/src/db/schema"
const db = drizzle({ connection: process.env.DATABASE_URL!, casing: "snake_case", schema: schema })
// const db = drizzle('postgresql://postgres:postgres@localhost:2345/pooler');

View file

@ -1,10 +0,0 @@
import { drizzle } from 'drizzle-orm/bun-sqlite'
import { Database } from 'bun:sqlite'
import * as schema from '@/src/db/schema-sqlite'
const sqlitePath = process.env.SQLITE_DB_PATH || 'sqlite.db'
const sqlite = new Database(sqlitePath)
const db = drizzle(sqlite, { schema })
export { db }

View file

@ -1,706 +0,0 @@
import { pgTable, pgSchema, integer, varchar, date, boolean, timestamp, numeric, jsonb, pgEnum, unique, real, text, check, decimal } from "drizzle-orm/pg-core";
import { relations, sql } from "drizzle-orm";
const mf = pgSchema('mf');
export const users = mf.table('users', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
name: varchar({ length: 255 }),
email: varchar({ length: 255 }),
mobile: varchar({ length: 255 }),
createdAt: timestamp('created_at').notNull().defaultNow(),
}, (t) => ({
unq_email: unique('unique_email').on(t.email),
}));
export const userDetails = mf.table('user_details', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
userId: integer('user_id').notNull().references(() => users.id).unique(),
bio: varchar('bio', { length: 500 }),
dateOfBirth: date('date_of_birth'),
gender: varchar('gender', { length: 20 }),
occupation: varchar('occupation', { length: 100 }),
profileImage: varchar('profile_image', { length: 500 }),
isSuspended: boolean('is_suspended').notNull().default(false),
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
});
export const userCreds = mf.table('user_creds', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
userId: integer('user_id').notNull().references(() => users.id),
userPassword: varchar('user_password', { length: 255 }).notNull(),
createdAt: timestamp('created_at').notNull().defaultNow(),
});
export const addresses = mf.table('addresses', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
userId: integer('user_id').notNull().references(() => users.id),
name: varchar('name', { length: 255 }).notNull(),
phone: varchar('phone', { length: 15 }).notNull(),
addressLine1: varchar('address_line1', { length: 255 }).notNull(),
addressLine2: varchar('address_line2', { length: 255 }),
city: varchar('city', { length: 100 }).notNull(),
state: varchar('state', { length: 100 }).notNull(),
pincode: varchar('pincode', { length: 10 }).notNull(),
isDefault: boolean('is_default').notNull().default(false),
latitude: real('latitude'),
longitude: real('longitude'),
googleMapsUrl: varchar('google_maps_url', { length: 500 }),
adminLatitude: real('admin_latitude'),
adminLongitude: real('admin_longitude'),
zoneId: integer('zone_id').references(() => addressZones.id),
createdAt: timestamp('created_at').notNull().defaultNow(),
});
export const addressZones = mf.table('address_zones', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
zoneName: varchar('zone_name', { length: 255 }).notNull(),
addedAt: timestamp('added_at').notNull().defaultNow(),
});
export const addressAreas = mf.table('address_areas', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
placeName: varchar('place_name', { length: 255 }).notNull(),
zoneId: integer('zone_id').references(() => addressZones.id),
createdAt: timestamp('created_at').notNull().defaultNow(),
});
export const staffUsers = mf.table('staff_users', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
name: varchar({ length: 255 }).notNull(),
password: varchar({ length: 255 }).notNull(),
staffRoleId: integer('staff_role_id').references(() => staffRoles.id),
createdAt: timestamp('created_at').notNull().defaultNow(),
});
export const storeInfo = mf.table('store_info', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
name: varchar({ length: 255 }).notNull(),
description: varchar({ length: 500 }),
imageUrl: varchar('image_url', { length: 500 }),
createdAt: timestamp('created_at').notNull().defaultNow(),
owner: integer('owner').notNull().references(() => staffUsers.id),
});
export const units = mf.table('units', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
shortNotation: varchar('short_notation', { length: 50 }).notNull(),
fullName: varchar('full_name', { length: 100 }).notNull(),
}, (t) => ({
unq_short_notation: unique('unique_short_notation').on(t.shortNotation),
}));
export const productAvailabilityActionEnum = pgEnum('product_availability_action', ['in', 'out']);
export const productInfo = mf.table('product_info', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
name: varchar({ length: 255 }).notNull(),
shortDescription: varchar('short_description', { length: 500 }),
longDescription: varchar('long_description', { length: 1000 }),
unitId: integer('unit_id').notNull().references(() => units.id),
price: numeric({ precision: 10, scale: 2 }).notNull(),
marketPrice: numeric('market_price', { precision: 10, scale: 2 }),
images: jsonb('images'),
isOutOfStock: boolean('is_out_of_stock').notNull().default(false),
isSuspended: boolean('is_suspended').notNull().default(false),
isFlashAvailable: boolean('is_flash_available').notNull().default(false),
flashPrice: numeric('flash_price', { precision: 10, scale: 2 }),
createdAt: timestamp('created_at').notNull().defaultNow(),
incrementStep: real('increment_step').notNull().default(1),
productQuantity: real('product_quantity').notNull().default(1),
storeId: integer('store_id').references(() => storeInfo.id),
scheduledAvailability: boolean('scheduled_availability').notNull().default(true),
});
export const productAvailabilitySchedules = mf.table('product_availability_schedules', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
time: varchar('time', { length: 10 }).notNull(),
scheduleName: varchar('schedule_name', { length: 255 }).notNull().unique(),
action: productAvailabilityActionEnum('action').notNull(),
productIds: integer('product_ids').array().notNull().default([]),
groupIds: integer('group_ids').array().notNull().default([]),
createdAt: timestamp('created_at').notNull().defaultNow(),
lastUpdated: timestamp('last_updated').notNull().defaultNow(),
});
export const productGroupInfo = mf.table('product_group_info', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
groupName: varchar('group_name', { length: 255 }).notNull(),
description: varchar({ length: 500 }),
createdAt: timestamp('created_at').notNull().defaultNow(),
});
export const productGroupMembership = mf.table('product_group_membership', {
productId: integer('product_id').notNull().references(() => productInfo.id),
groupId: integer('group_id').notNull().references(() => productGroupInfo.id),
addedAt: timestamp('added_at').notNull().defaultNow(),
}, (t) => ({
pk: unique('product_group_membership_pk').on(t.productId, t.groupId),
}));
export const homeBanners = mf.table('home_banners', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
name: varchar('name', { length: 255 }).notNull(),
imageUrl: varchar('image_url', { length: 500 }).notNull(),
description: varchar('description', { length: 500 }),
productIds: integer('product_ids').array(),
redirectUrl: varchar('redirect_url', { length: 500 }),
serialNum: integer('serial_num'),
isActive: boolean('is_active').notNull().default(false),
createdAt: timestamp('created_at').notNull().defaultNow(),
lastUpdated: timestamp('last_updated').notNull().defaultNow(),
});
export const productReviews = mf.table('product_reviews', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
userId: integer('user_id').notNull().references(() => users.id),
productId: integer('product_id').notNull().references(() => productInfo.id),
reviewBody: text('review_body').notNull(),
imageUrls: jsonb('image_urls').$defaultFn(() => []),
reviewTime: timestamp('review_time').notNull().defaultNow(),
ratings: real('ratings').notNull(),
adminResponse: text('admin_response'),
adminResponseImages: jsonb('admin_response_images').$defaultFn(() => []),
}, (t) => ({
ratingCheck: check('rating_check', sql`${t.ratings} >= 1 AND ${t.ratings} <= 5`),
}));
export const uploadStatusEnum = pgEnum('upload_status', ['pending', 'claimed']);
export const staffRoleEnum = pgEnum('staff_role', ['super_admin', 'admin', 'marketer', 'delivery_staff']);
export const staffPermissionEnum = pgEnum('staff_permission', ['crud_product', 'make_coupon', 'crud_staff_users']);
export const uploadUrlStatus = mf.table('upload_url_status', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
createdAt: timestamp('created_at').notNull().defaultNow(),
key: varchar('key', { length: 500 }).notNull(),
status: uploadStatusEnum('status').notNull().default('pending'),
});
export const productTagInfo = mf.table('product_tag_info', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
tagName: varchar('tag_name', { length: 100 }).notNull().unique(),
tagDescription: varchar('tag_description', { length: 500 }),
imageUrl: varchar('image_url', { length: 500 }),
isDashboardTag: boolean('is_dashboard_tag').notNull().default(false),
relatedStores: jsonb('related_stores').$defaultFn(() => []),
createdAt: timestamp('created_at').notNull().defaultNow(),
});
export const productTags = mf.table('product_tags', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
productId: integer('product_id').notNull().references(() => productInfo.id),
tagId: integer('tag_id').notNull().references(() => productTagInfo.id),
assignedAt: timestamp('assigned_at').notNull().defaultNow(),
}, (t) => ({
unq_product_tag: unique('unique_product_tag').on(t.productId, t.tagId),
}));
export const deliverySlotInfo = mf.table('delivery_slot_info', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
deliveryTime: timestamp('delivery_time').notNull(),
freezeTime: timestamp('freeze_time').notNull(),
isActive: boolean('is_active').notNull().default(true),
isFlash: boolean('is_flash').notNull().default(false),
isCapacityFull: boolean('is_capacity_full').notNull().default(false),
deliverySequence: jsonb('delivery_sequence').$defaultFn(() => {}),
groupIds: jsonb('group_ids').$defaultFn(() => []),
});
export const vendorSnippets = mf.table('vendor_snippets', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
snippetCode: varchar('snippet_code', { length: 255 }).notNull().unique(),
slotId: integer('slot_id').references(() => deliverySlotInfo.id),
isPermanent: boolean('is_permanent').notNull().default(false),
productIds: integer('product_ids').array().notNull(),
validTill: timestamp('valid_till'),
createdAt: timestamp('created_at').notNull().defaultNow(),
});
export const vendorSnippetsRelations = relations(vendorSnippets, ({ one }) => ({
slot: one(deliverySlotInfo, { fields: [vendorSnippets.slotId], references: [deliverySlotInfo.id] }),
}));
export const productSlots = mf.table('product_slots', {
productId: integer('product_id').notNull().references(() => productInfo.id),
slotId: integer('slot_id').notNull().references(() => deliverySlotInfo.id),
}, (t) => ({
pk: unique('product_slot_pk').on(t.productId, t.slotId),
}));
export const specialDeals = mf.table('special_deals', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
productId: integer('product_id').notNull().references(() => productInfo.id),
quantity: numeric({ precision: 10, scale: 2 }).notNull(),
price: numeric({ precision: 10, scale: 2 }).notNull(),
validTill: timestamp('valid_till').notNull(),
});
export const orders = mf.table('orders', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
userId: integer('user_id').notNull().references(() => users.id),
addressId: integer('address_id').notNull().references(() => addresses.id),
slotId: integer('slot_id').references(() => deliverySlotInfo.id),
isCod: boolean('is_cod').notNull().default(false),
isOnlinePayment: boolean('is_online_payment').notNull().default(false),
paymentInfoId: integer('payment_info_id').references(() => paymentInfoTable.id),
totalAmount: numeric('total_amount', { precision: 10, scale: 2 }).notNull(),
deliveryCharge: numeric('delivery_charge', { precision: 10, scale: 2 }).notNull().default('0'),
readableId: integer('readable_id').notNull(),
adminNotes: text('admin_notes'),
userNotes: text('user_notes'),
orderGroupId: varchar('order_group_id', { length: 255 }),
orderGroupProportion: decimal('order_group_proportion', { precision: 10, scale: 4 }),
isFlashDelivery: boolean('is_flash_delivery').notNull().default(false),
createdAt: timestamp('created_at').notNull().defaultNow(),
});
export const orderItems = mf.table('order_items', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
orderId: integer('order_id').notNull().references(() => orders.id),
productId: integer('product_id').notNull().references(() => productInfo.id),
quantity: varchar('quantity', { length: 50 }).notNull(),
price: numeric({ precision: 10, scale: 2 }).notNull(),
discountedPrice: numeric('discounted_price', { precision: 10, scale: 2 }),
is_packaged: boolean('is_packaged').notNull().default(false),
is_package_verified: boolean('is_package_verified').notNull().default(false),
});
export const paymentStatusEnum = pgEnum('payment_status', ['pending', 'success', 'cod', 'failed']);
export const orderStatus = mf.table('order_status', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
orderTime: timestamp('order_time').notNull().defaultNow(),
userId: integer('user_id').notNull().references(() => users.id),
orderId: integer('order_id').notNull().references(() => orders.id),
isPackaged: boolean('is_packaged').notNull().default(false),
isDelivered: boolean('is_delivered').notNull().default(false),
isCancelled: boolean('is_cancelled').notNull().default(false),
cancelReason: varchar('cancel_reason', { length: 255 }),
isCancelledByAdmin: boolean('is_cancelled_by_admin'),
paymentStatus: paymentStatusEnum('payment_state').notNull().default('pending'),
cancellationUserNotes: text('cancellation_user_notes'),
cancellationAdminNotes: text('cancellation_admin_notes'),
cancellationReviewed: boolean('cancellation_reviewed').notNull().default(false),
cancellationReviewedAt: timestamp('cancellation_reviewed_at'),
refundCouponId: integer('refund_coupon_id').references(() => coupons.id),
});
export const paymentInfoTable = mf.table('payment_info', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
status: varchar({ length: 50 }).notNull(),
gateway: varchar({ length: 50 }).notNull(),
orderId: varchar('order_id', { length: 500 }),
token: varchar({ length: 500 }),
merchantOrderId: varchar('merchant_order_id', { length: 255 }).notNull().unique(),
payload: jsonb('payload'),
});
export const payments = mf.table('payments', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
status: varchar({ length: 50 }).notNull(),
gateway: varchar({ length: 50 }).notNull(),
orderId: integer('order_id').notNull().references(() => orders.id),
token: varchar({ length: 500 }),
merchantOrderId: varchar('merchant_order_id', { length: 255 }).notNull().unique(),
payload: jsonb('payload'),
});
export const refunds = mf.table('refunds', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
orderId: integer('order_id').notNull().references(() => orders.id),
refundAmount: numeric('refund_amount', { precision: 10, scale: 2 }),
refundStatus: varchar('refund_status', { length: 50 }).default('none'),
merchantRefundId: varchar('merchant_refund_id', { length: 255 }),
refundProcessedAt: timestamp('refund_processed_at'),
createdAt: timestamp('created_at').notNull().defaultNow(),
});
export const keyValStore = mf.table('key_val_store', {
key: varchar('key', { length: 255 }).primaryKey(),
value: jsonb('value'),
});
export const notifications = mf.table('notifications', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
userId: integer('user_id').notNull().references(() => users.id),
title: varchar({ length: 255 }).notNull(),
body: varchar({ length: 512 }).notNull(),
type: varchar({ length: 50 }),
isRead: boolean('is_read').notNull().default(false),
createdAt: timestamp('created_at').notNull().defaultNow(),
});
export const productCategories = mf.table('product_categories', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
name: varchar({ length: 255 }).notNull(),
description: varchar({ length: 500 }),
});
export const cartItems = mf.table('cart_items', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
userId: integer('user_id').notNull().references(() => users.id),
productId: integer('product_id').notNull().references(() => productInfo.id),
quantity: numeric({ precision: 10, scale: 2 }).notNull(),
addedAt: timestamp('added_at').notNull().defaultNow(),
}, (t) => ({
unq_user_product: unique('unique_user_product').on(t.userId, t.productId),
}));
export const complaints = mf.table('complaints', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
userId: integer('user_id').notNull().references(() => users.id),
orderId: integer('order_id').references(() => orders.id),
complaintBody: varchar('complaint_body', { length: 1000 }).notNull(),
images: jsonb('images'),
response: varchar('response', { length: 1000 }),
isResolved: boolean('is_resolved').notNull().default(false),
createdAt: timestamp('created_at').notNull().defaultNow(),
});
export const coupons = mf.table('coupons', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
couponCode: varchar('coupon_code', { length: 50 }).notNull().unique('unique_coupon_code'),
isUserBased: boolean('is_user_based').notNull().default(false),
discountPercent: numeric('discount_percent', { precision: 5, scale: 2 }),
flatDiscount: numeric('flat_discount', { precision: 10, scale: 2 }),
minOrder: numeric('min_order', { precision: 10, scale: 2 }),
productIds: jsonb('product_ids'),
createdBy: integer('created_by').references(() => staffUsers.id),
maxValue: numeric('max_value', { precision: 10, scale: 2 }),
isApplyForAll: boolean('is_apply_for_all').notNull().default(false),
validTill: timestamp('valid_till'),
maxLimitForUser: integer('max_limit_for_user'),
isInvalidated: boolean('is_invalidated').notNull().default(false),
exclusiveApply: boolean('exclusive_apply').notNull().default(false),
createdAt: timestamp('created_at').notNull().defaultNow(),
});
export const couponUsage = mf.table('coupon_usage', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
userId: integer('user_id').notNull().references(() => users.id),
couponId: integer('coupon_id').notNull().references(() => coupons.id),
orderId: integer('order_id').references(() => orders.id),
orderItemId: integer('order_item_id').references(() => orderItems.id),
usedAt: timestamp('used_at').notNull().defaultNow(),
});
export const couponApplicableUsers = mf.table('coupon_applicable_users', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
couponId: integer('coupon_id').notNull().references(() => coupons.id),
userId: integer('user_id').notNull().references(() => users.id),
}, (t) => ({
unq_coupon_user: unique('unique_coupon_user').on(t.couponId, t.userId),
}));
export const couponApplicableProducts = mf.table('coupon_applicable_products', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
couponId: integer('coupon_id').notNull().references(() => coupons.id),
productId: integer('product_id').notNull().references(() => productInfo.id),
}, (t) => ({
unq_coupon_product: unique('unique_coupon_product').on(t.couponId, t.productId),
}));
export const userIncidents = mf.table('user_incidents', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
userId: integer('user_id').notNull().references(() => users.id),
orderId: integer('order_id').references(() => orders.id),
dateAdded: timestamp('date_added').notNull().defaultNow(),
adminComment: text('admin_comment'),
addedBy: integer('added_by').references(() => staffUsers.id),
negativityScore: integer('negativity_score'),
});
export const reservedCoupons = mf.table('reserved_coupons', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
secretCode: varchar('secret_code', { length: 50 }).notNull().unique(),
couponCode: varchar('coupon_code', { length: 50 }).notNull(),
discountPercent: numeric('discount_percent', { precision: 5, scale: 2 }),
flatDiscount: numeric('flat_discount', { precision: 10, scale: 2 }),
minOrder: numeric('min_order', { precision: 10, scale: 2 }),
productIds: jsonb('product_ids'),
maxValue: numeric('max_value', { precision: 10, scale: 2 }),
validTill: timestamp('valid_till'),
maxLimitForUser: integer('max_limit_for_user'),
exclusiveApply: boolean('exclusive_apply').notNull().default(false),
isRedeemed: boolean('is_redeemed').notNull().default(false),
redeemedBy: integer('redeemed_by').references(() => users.id),
redeemedAt: timestamp('redeemed_at'),
createdBy: integer('created_by').notNull().references(() => staffUsers.id),
createdAt: timestamp('created_at').notNull().defaultNow(),
}, (t) => ({
unq_secret_code: unique('unique_secret_code').on(t.secretCode),
}));
export const notifCreds = mf.table('notif_creds', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
token: varchar({ length: 500 }).notNull().unique(),
addedAt: timestamp('added_at').notNull().defaultNow(),
userId: integer('user_id').notNull().references(() => users.id),
lastVerified: timestamp('last_verified'),
});
export const unloggedUserTokens = mf.table('unlogged_user_tokens', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
token: varchar({ length: 500 }).notNull().unique(),
addedAt: timestamp('added_at').notNull().defaultNow(),
lastVerified: timestamp('last_verified'),
});
export const userNotifications = mf.table('user_notifications', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
title: varchar('title', { length: 255 }).notNull(),
imageUrl: varchar('image_url', { length: 500 }),
createdAt: timestamp('created_at').notNull().defaultNow(),
body: text('body').notNull(),
applicableUsers: jsonb('applicable_users'),
});
export const staffRoles = mf.table('staff_roles', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
roleName: staffRoleEnum('role_name').notNull(),
createdAt: timestamp('created_at').notNull().defaultNow(),
}, (t) => ({
unq_role_name: unique('unique_role_name').on(t.roleName),
}));
export const staffPermissions = mf.table('staff_permissions', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
permissionName: staffPermissionEnum('permission_name').notNull(),
createdAt: timestamp('created_at').notNull().defaultNow(),
}, (t) => ({
unq_permission_name: unique('unique_permission_name').on(t.permissionName),
}));
export const staffRolePermissions = mf.table('staff_role_permissions', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
staffRoleId: integer('staff_role_id').notNull().references(() => staffRoles.id),
staffPermissionId: integer('staff_permission_id').notNull().references(() => staffPermissions.id),
createdAt: timestamp('created_at').notNull().defaultNow(),
}, (t) => ({
unq_role_permission: unique('unique_role_permission').on(t.staffRoleId, t.staffPermissionId),
}));
// Relations
export const usersRelations = relations(users, ({ many, one }) => ({
addresses: many(addresses),
orders: many(orders),
notifications: many(notifications),
cartItems: many(cartItems),
userCreds: one(userCreds),
coupons: many(coupons),
couponUsages: many(couponUsage),
applicableCoupons: many(couponApplicableUsers),
userDetails: one(userDetails),
notifCreds: many(notifCreds),
userIncidents: many(userIncidents),
}));
export const userCredsRelations = relations(userCreds, ({ one }) => ({
user: one(users, { fields: [userCreds.userId], references: [users.id] }),
}));
export const staffUsersRelations = relations(staffUsers, ({ one, many }) => ({
role: one(staffRoles, { fields: [staffUsers.staffRoleId], references: [staffRoles.id] }),
coupons: many(coupons),
stores: many(storeInfo),
}));
export const addressesRelations = relations(addresses, ({ one, many }) => ({
user: one(users, { fields: [addresses.userId], references: [users.id] }),
orders: many(orders),
zone: one(addressZones, { fields: [addresses.zoneId], references: [addressZones.id] }),
}));
export const unitsRelations = relations(units, ({ many }) => ({
products: many(productInfo),
}));
export const productInfoRelations = relations(productInfo, ({ one, many }) => ({
unit: one(units, { fields: [productInfo.unitId], references: [units.id] }),
store: one(storeInfo, { fields: [productInfo.storeId], references: [storeInfo.id] }),
productSlots: many(productSlots),
specialDeals: many(specialDeals),
orderItems: many(orderItems),
cartItems: many(cartItems),
tags: many(productTags),
applicableCoupons: many(couponApplicableProducts),
reviews: many(productReviews),
groups: many(productGroupMembership),
}));
export const productTagInfoRelations = relations(productTagInfo, ({ many }) => ({
products: many(productTags),
}));
export const productTagsRelations = relations(productTags, ({ one }) => ({
product: one(productInfo, { fields: [productTags.productId], references: [productInfo.id] }),
tag: one(productTagInfo, { fields: [productTags.tagId], references: [productTagInfo.id] }),
}));
export const deliverySlotInfoRelations = relations(deliverySlotInfo, ({ many }) => ({
productSlots: many(productSlots),
orders: many(orders),
vendorSnippets: many(vendorSnippets),
}));
export const productSlotsRelations = relations(productSlots, ({ one }) => ({
product: one(productInfo, { fields: [productSlots.productId], references: [productInfo.id] }),
slot: one(deliverySlotInfo, { fields: [productSlots.slotId], references: [deliverySlotInfo.id] }),
}));
export const specialDealsRelations = relations(specialDeals, ({ one }) => ({
product: one(productInfo, { fields: [specialDeals.productId], references: [productInfo.id] }),
}));
export const ordersRelations = relations(orders, ({ one, many }) => ({
user: one(users, { fields: [orders.userId], references: [users.id] }),
address: one(addresses, { fields: [orders.addressId], references: [addresses.id] }),
slot: one(deliverySlotInfo, { fields: [orders.slotId], references: [deliverySlotInfo.id] }),
orderItems: many(orderItems),
payment: one(payments),
paymentInfo: one(paymentInfoTable, { fields: [orders.paymentInfoId], references: [paymentInfoTable.id] }),
orderStatus: many(orderStatus),
refunds: many(refunds),
couponUsages: many(couponUsage),
userIncidents: many(userIncidents),
}));
export const orderItemsRelations = relations(orderItems, ({ one }) => ({
order: one(orders, { fields: [orderItems.orderId], references: [orders.id] }),
product: one(productInfo, { fields: [orderItems.productId], references: [productInfo.id] }),
}));
export const orderStatusRelations = relations(orderStatus, ({ one }) => ({
order: one(orders, { fields: [orderStatus.orderId], references: [orders.id] }),
user: one(users, { fields: [orderStatus.userId], references: [users.id] }),
refundCoupon: one(coupons, { fields: [orderStatus.refundCouponId], references: [coupons.id] }),
}));
export const paymentInfoRelations = relations(paymentInfoTable, ({ one }) => ({
order: one(orders, { fields: [paymentInfoTable.id], references: [orders.paymentInfoId] }),
}));
export const paymentsRelations = relations(payments, ({ one }) => ({
order: one(orders, { fields: [payments.orderId], references: [orders.id] }),
}));
export const refundsRelations = relations(refunds, ({ one }) => ({
order: one(orders, { fields: [refunds.orderId], references: [orders.id] }),
}));
export const notificationsRelations = relations(notifications, ({ one }) => ({
user: one(users, { fields: [notifications.userId], references: [users.id] }),
}));
export const productCategoriesRelations = relations(productCategories, ({}) => ({}));
export const cartItemsRelations = relations(cartItems, ({ one }) => ({
user: one(users, { fields: [cartItems.userId], references: [users.id] }),
product: one(productInfo, { fields: [cartItems.productId], references: [productInfo.id] }),
}));
export const complaintsRelations = relations(complaints, ({ one }) => ({
user: one(users, { fields: [complaints.userId], references: [users.id] }),
order: one(orders, { fields: [complaints.orderId], references: [orders.id] }),
}));
export const couponsRelations = relations(coupons, ({ one, many }) => ({
creator: one(staffUsers, { fields: [coupons.createdBy], references: [staffUsers.id] }),
usages: many(couponUsage),
applicableUsers: many(couponApplicableUsers),
applicableProducts: many(couponApplicableProducts),
}));
export const couponUsageRelations = relations(couponUsage, ({ one }) => ({
user: one(users, { fields: [couponUsage.userId], references: [users.id] }),
coupon: one(coupons, { fields: [couponUsage.couponId], references: [coupons.id] }),
order: one(orders, { fields: [couponUsage.orderId], references: [orders.id] }),
orderItem: one(orderItems, { fields: [couponUsage.orderItemId], references: [orderItems.id] }),
}));
export const userDetailsRelations = relations(userDetails, ({ one }) => ({
user: one(users, { fields: [userDetails.userId], references: [users.id] }),
}));
export const notifCredsRelations = relations(notifCreds, ({ one }) => ({
user: one(users, { fields: [notifCreds.userId], references: [users.id] }),
}));
export const userNotificationsRelations = relations(userNotifications, ({}) => ({
// No relations needed for now
}));
export const storeInfoRelations = relations(storeInfo, ({ one, many }) => ({
owner: one(staffUsers, { fields: [storeInfo.owner], references: [staffUsers.id] }),
products: many(productInfo),
}));
export const couponApplicableUsersRelations = relations(couponApplicableUsers, ({ one }) => ({
coupon: one(coupons, { fields: [couponApplicableUsers.couponId], references: [coupons.id] }),
user: one(users, { fields: [couponApplicableUsers.userId], references: [users.id] }),
}));
export const couponApplicableProductsRelations = relations(couponApplicableProducts, ({ one }) => ({
coupon: one(coupons, { fields: [couponApplicableProducts.couponId], references: [coupons.id] }),
product: one(productInfo, { fields: [couponApplicableProducts.productId], references: [productInfo.id] }),
}));
export const reservedCouponsRelations = relations(reservedCoupons, ({ one }) => ({
redeemedUser: one(users, { fields: [reservedCoupons.redeemedBy], references: [users.id] }),
creator: one(staffUsers, { fields: [reservedCoupons.createdBy], references: [staffUsers.id] }),
}));
export const productReviewsRelations = relations(productReviews, ({ one }) => ({
user: one(users, { fields: [productReviews.userId], references: [users.id] }),
product: one(productInfo, { fields: [productReviews.productId], references: [productInfo.id] }),
}));
export const addressZonesRelations = relations(addressZones, ({ many }) => ({
addresses: many(addresses),
areas: many(addressAreas),
}));
export const addressAreasRelations = relations(addressAreas, ({ one }) => ({
zone: one(addressZones, { fields: [addressAreas.zoneId], references: [addressZones.id] }),
}));
export const productGroupInfoRelations = relations(productGroupInfo, ({ many }) => ({
memberships: many(productGroupMembership),
}));
export const productGroupMembershipRelations = relations(productGroupMembership, ({ one }) => ({
product: one(productInfo, { fields: [productGroupMembership.productId], references: [productInfo.id] }),
group: one(productGroupInfo, { fields: [productGroupMembership.groupId], references: [productGroupInfo.id] }),
}));
export const homeBannersRelations = relations(homeBanners, ({}) => ({
// Relations for productIds array would be more complex, skipping for now
}));
export const staffRolesRelations = relations(staffRoles, ({ many }) => ({
staffUsers: many(staffUsers),
rolePermissions: many(staffRolePermissions),
}));
export const staffPermissionsRelations = relations(staffPermissions, ({ many }) => ({
rolePermissions: many(staffRolePermissions),
}));
export const staffRolePermissionsRelations = relations(staffRolePermissions, ({ one }) => ({
role: one(staffRoles, { fields: [staffRolePermissions.staffRoleId], references: [staffRoles.id] }),
permission: one(staffPermissions, { fields: [staffRolePermissions.staffPermissionId], references: [staffPermissions.id] }),
}));
export const userIncidentsRelations = relations(userIncidents, ({ one }) => ({
user: one(users, { fields: [userIncidents.userId], references: [users.id] }),
order: one(orders, { fields: [userIncidents.orderId], references: [orders.id] }),
addedBy: one(staffUsers, { fields: [userIncidents.addedBy], references: [staffUsers.id] }),
}));
export const productAvailabilitySchedulesRelations = relations(productAvailabilitySchedules, ({}) => ({
}));

View file

@ -1,735 +0,0 @@
import {
sqliteTable,
integer,
text,
real,
unique,
check,
} from 'drizzle-orm/sqlite-core'
import { relations, sql } from 'drizzle-orm'
const epochSeconds = sql`(strftime('%s','now'))`
const sqliteEnum = <T extends readonly [string, ...string[]]>(
_name: string,
values: T
) => (columnName: string) => text(columnName, { enum: values })
export const users = sqliteTable('users', {
id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name'),
email: text('email'),
mobile: text('mobile'),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(epochSeconds),
}, (t) => ({
unq_email: unique('unique_email').on(t.email),
}))
export const userDetails = sqliteTable('user_details', {
id: integer('id').primaryKey({ autoIncrement: true }),
userId: integer('user_id').notNull().references(() => users.id).unique(),
bio: text('bio'),
dateOfBirth: integer('date_of_birth', { mode: 'timestamp' }),
gender: text('gender'),
occupation: text('occupation'),
profileImage: text('profile_image'),
isSuspended: integer('is_suspended', { mode: 'boolean' }).notNull().default(false),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(epochSeconds),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().default(epochSeconds),
})
export const userCreds = sqliteTable('user_creds', {
id: integer('id').primaryKey({ autoIncrement: true }),
userId: integer('user_id').notNull().references(() => users.id),
userPassword: text('user_password').notNull(),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(epochSeconds),
})
export const addressZones = sqliteTable('address_zones', {
id: integer('id').primaryKey({ autoIncrement: true }),
zoneName: text('zone_name').notNull(),
addedAt: integer('added_at', { mode: 'timestamp' }).notNull().default(epochSeconds),
})
export const addresses = sqliteTable('addresses', {
id: integer('id').primaryKey({ autoIncrement: true }),
userId: integer('user_id').notNull().references(() => users.id),
name: text('name').notNull(),
phone: text('phone').notNull(),
addressLine1: text('address_line1').notNull(),
addressLine2: text('address_line2'),
city: text('city').notNull(),
state: text('state').notNull(),
pincode: text('pincode').notNull(),
isDefault: integer('is_default', { mode: 'boolean' }).notNull().default(false),
latitude: real('latitude'),
longitude: real('longitude'),
googleMapsUrl: text('google_maps_url'),
adminLatitude: real('admin_latitude'),
adminLongitude: real('admin_longitude'),
zoneId: integer('zone_id').references(() => addressZones.id),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(epochSeconds),
})
export const addressAreas = sqliteTable('address_areas', {
id: integer('id').primaryKey({ autoIncrement: true }),
placeName: text('place_name').notNull(),
zoneId: integer('zone_id').references(() => addressZones.id),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(epochSeconds),
})
export const staffRoleEnum = sqliteEnum('staff_role', [
'super_admin',
'admin',
'marketer',
'delivery_staff',
])
export const staffPermissionEnum = sqliteEnum('staff_permission', [
'crud_product',
'make_coupon',
'crud_staff_users',
])
export const staffRoles = sqliteTable('staff_roles', {
id: integer('id').primaryKey({ autoIncrement: true }),
roleName: staffRoleEnum('role_name').notNull(),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(epochSeconds),
}, (t) => ({
unq_role_name: unique('unique_role_name').on(t.roleName),
}))
export const staffPermissions = sqliteTable('staff_permissions', {
id: integer('id').primaryKey({ autoIncrement: true }),
permissionName: staffPermissionEnum('permission_name').notNull(),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(epochSeconds),
}, (t) => ({
unq_permission_name: unique('unique_permission_name').on(t.permissionName),
}))
export const staffRolePermissions = sqliteTable('staff_role_permissions', {
id: integer('id').primaryKey({ autoIncrement: true }),
staffRoleId: integer('staff_role_id').notNull().references(() => staffRoles.id),
staffPermissionId: integer('staff_permission_id').notNull().references(() => staffPermissions.id),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(epochSeconds),
}, (t) => ({
unq_role_permission: unique('unique_role_permission').on(
t.staffRoleId,
t.staffPermissionId
),
}))
export const staffUsers = sqliteTable('staff_users', {
id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name').notNull(),
password: text('password').notNull(),
staffRoleId: integer('staff_role_id').references(() => staffRoles.id),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(epochSeconds),
})
export const storeInfo = sqliteTable('store_info', {
id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name').notNull(),
description: text('description'),
imageUrl: text('image_url'),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(epochSeconds),
owner: integer('owner').notNull().references(() => staffUsers.id),
})
export const units = sqliteTable('units', {
id: integer('id').primaryKey({ autoIncrement: true }),
shortNotation: text('short_notation').notNull(),
fullName: text('full_name').notNull(),
}, (t) => ({
unq_short_notation: unique('unique_short_notation').on(t.shortNotation),
}))
export const productAvailabilityActionEnum = sqliteEnum(
'product_availability_action',
['in', 'out']
)
export const productInfo = sqliteTable('product_info', {
id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name').notNull(),
shortDescription: text('short_description'),
longDescription: text('long_description'),
unitId: integer('unit_id').notNull().references(() => units.id),
price: text('price').notNull(),
marketPrice: text('market_price'),
images: text('images'),
isOutOfStock: integer('is_out_of_stock', { mode: 'boolean' }).notNull().default(false),
isSuspended: integer('is_suspended', { mode: 'boolean' }).notNull().default(false),
isFlashAvailable: integer('is_flash_available', { mode: 'boolean' }).notNull().default(false),
flashPrice: text('flash_price'),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(epochSeconds),
incrementStep: real('increment_step').notNull().default(1),
productQuantity: real('product_quantity').notNull().default(1),
storeId: integer('store_id').references(() => storeInfo.id),
scheduledAvailability: integer('scheduled_availability', { mode: 'boolean' }).notNull().default(true),
})
export const productAvailabilitySchedules = sqliteTable('product_availability_schedules', {
id: integer('id').primaryKey({ autoIncrement: true }),
time: text('time').notNull(),
scheduleName: text('schedule_name').notNull().unique(),
action: productAvailabilityActionEnum('action').notNull(),
productIds: text('product_ids').notNull().default('[]'),
groupIds: text('group_ids').notNull().default('[]'),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(epochSeconds),
lastUpdated: integer('last_updated', { mode: 'timestamp' }).notNull().default(epochSeconds),
})
export const productGroupInfo = sqliteTable('product_group_info', {
id: integer('id').primaryKey({ autoIncrement: true }),
groupName: text('group_name').notNull(),
description: text('description'),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(epochSeconds),
})
export const productGroupMembership = sqliteTable('product_group_membership', {
productId: integer('product_id').notNull().references(() => productInfo.id),
groupId: integer('group_id').notNull().references(() => productGroupInfo.id),
addedAt: integer('added_at', { mode: 'timestamp' }).notNull().default(epochSeconds),
}, (t) => ({
pk: unique('product_group_membership_pk').on(t.productId, t.groupId),
}))
export const homeBanners = sqliteTable('home_banners', {
id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name').notNull(),
imageUrl: text('image_url').notNull(),
description: text('description'),
productIds: text('product_ids'),
redirectUrl: text('redirect_url'),
serialNum: integer('serial_num'),
isActive: integer('is_active', { mode: 'boolean' }).notNull().default(false),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(epochSeconds),
lastUpdated: integer('last_updated', { mode: 'timestamp' }).notNull().default(epochSeconds),
})
export const productReviews = sqliteTable('product_reviews', {
id: integer('id').primaryKey({ autoIncrement: true }),
userId: integer('user_id').notNull().references(() => users.id),
productId: integer('product_id').notNull().references(() => productInfo.id),
reviewBody: text('review_body').notNull(),
imageUrls: text('image_urls').default('[]'),
reviewTime: integer('review_time', { mode: 'timestamp' }).notNull().default(epochSeconds),
ratings: real('ratings').notNull(),
adminResponse: text('admin_response'),
adminResponseImages: text('admin_response_images').default('[]'),
}, (t) => ({
ratingCheck: check('rating_check', sql`${t.ratings} >= 1 AND ${t.ratings} <= 5`),
}))
export const uploadStatusEnum = sqliteEnum('upload_status', ['pending', 'claimed'])
export const uploadUrlStatus = sqliteTable('upload_url_status', {
id: integer('id').primaryKey({ autoIncrement: true }),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(epochSeconds),
key: text('key').notNull(),
status: uploadStatusEnum('status').notNull().default('pending'),
})
export const productTagInfo = sqliteTable('product_tag_info', {
id: integer('id').primaryKey({ autoIncrement: true }),
tagName: text('tag_name').notNull().unique(),
tagDescription: text('tag_description'),
imageUrl: text('image_url'),
isDashboardTag: integer('is_dashboard_tag', { mode: 'boolean' }).notNull().default(false),
relatedStores: text('related_stores').default('[]'),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(epochSeconds),
})
export const productTags = sqliteTable('product_tags', {
id: integer('id').primaryKey({ autoIncrement: true }),
productId: integer('product_id').notNull().references(() => productInfo.id),
tagId: integer('tag_id').notNull().references(() => productTagInfo.id),
assignedAt: integer('assigned_at', { mode: 'timestamp' }).notNull().default(epochSeconds),
}, (t) => ({
unq_product_tag: unique('unique_product_tag').on(t.productId, t.tagId),
}))
export const deliverySlotInfo = sqliteTable('delivery_slot_info', {
id: integer('id').primaryKey({ autoIncrement: true }),
deliveryTime: integer('delivery_time', { mode: 'timestamp' }).notNull(),
freezeTime: integer('freeze_time', { mode: 'timestamp' }).notNull(),
isActive: integer('is_active', { mode: 'boolean' }).notNull().default(true),
isFlash: integer('is_flash', { mode: 'boolean' }).notNull().default(false),
isCapacityFull: integer('is_capacity_full', { mode: 'boolean' }).notNull().default(false),
deliverySequence: text('delivery_sequence').default('{}'),
groupIds: text('group_ids').default('[]'),
})
export const vendorSnippets = sqliteTable('vendor_snippets', {
id: integer('id').primaryKey({ autoIncrement: true }),
snippetCode: text('snippet_code').notNull().unique(),
slotId: integer('slot_id').references(() => deliverySlotInfo.id),
isPermanent: integer('is_permanent', { mode: 'boolean' }).notNull().default(false),
productIds: text('product_ids').notNull(),
validTill: integer('valid_till', { mode: 'timestamp' }),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(epochSeconds),
})
export const productSlots = sqliteTable('product_slots', {
productId: integer('product_id').notNull().references(() => productInfo.id),
slotId: integer('slot_id').notNull().references(() => deliverySlotInfo.id),
}, (t) => ({
pk: unique('product_slot_pk').on(t.productId, t.slotId),
}))
export const specialDeals = sqliteTable('special_deals', {
id: integer('id').primaryKey({ autoIncrement: true }),
productId: integer('product_id').notNull().references(() => productInfo.id),
quantity: text('quantity').notNull(),
price: text('price').notNull(),
validTill: integer('valid_till', { mode: 'timestamp' }).notNull(),
})
export const paymentInfoTable = sqliteTable('payment_info', {
id: integer('id').primaryKey({ autoIncrement: true }),
status: text('status').notNull(),
gateway: text('gateway').notNull(),
orderId: text('order_id'),
token: text('token'),
merchantOrderId: text('merchant_order_id').notNull().unique(),
payload: text('payload'),
})
export const orders = sqliteTable('orders', {
id: integer('id').primaryKey({ autoIncrement: true }),
userId: integer('user_id').notNull().references(() => users.id),
addressId: integer('address_id').notNull().references(() => addresses.id),
slotId: integer('slot_id').references(() => deliverySlotInfo.id),
isCod: integer('is_cod', { mode: 'boolean' }).notNull().default(false),
isOnlinePayment: integer('is_online_payment', { mode: 'boolean' }).notNull().default(false),
paymentInfoId: integer('payment_info_id').references(() => paymentInfoTable.id),
totalAmount: text('total_amount').notNull(),
deliveryCharge: text('delivery_charge').notNull().default('0'),
readableId: integer('readable_id').notNull(),
adminNotes: text('admin_notes'),
userNotes: text('user_notes'),
orderGroupId: text('order_group_id'),
orderGroupProportion: text('order_group_proportion'),
isFlashDelivery: integer('is_flash_delivery', { mode: 'boolean' }).notNull().default(false),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(epochSeconds),
})
export const orderItems = sqliteTable('order_items', {
id: integer('id').primaryKey({ autoIncrement: true }),
orderId: integer('order_id').notNull().references(() => orders.id),
productId: integer('product_id').notNull().references(() => productInfo.id),
quantity: text('quantity').notNull(),
price: text('price').notNull(),
discountedPrice: text('discounted_price'),
is_packaged: integer('is_packaged', { mode: 'boolean' }).notNull().default(false),
is_package_verified: integer('is_package_verified', { mode: 'boolean' }).notNull().default(false),
})
export const paymentStatusEnum = sqliteEnum('payment_status', [
'pending',
'success',
'cod',
'failed',
])
export const orderStatus = sqliteTable('order_status', {
id: integer('id').primaryKey({ autoIncrement: true }),
orderTime: integer('order_time', { mode: 'timestamp' }).notNull().default(epochSeconds),
userId: integer('user_id').notNull().references(() => users.id),
orderId: integer('order_id').notNull().references(() => orders.id),
isPackaged: integer('is_packaged', { mode: 'boolean' }).notNull().default(false),
isDelivered: integer('is_delivered', { mode: 'boolean' }).notNull().default(false),
isCancelled: integer('is_cancelled', { mode: 'boolean' }).notNull().default(false),
cancelReason: text('cancel_reason'),
isCancelledByAdmin: integer('is_cancelled_by_admin', { mode: 'boolean' }),
paymentStatus: paymentStatusEnum('payment_state').notNull().default('pending'),
cancellationUserNotes: text('cancellation_user_notes'),
cancellationAdminNotes: text('cancellation_admin_notes'),
cancellationReviewed: integer('cancellation_reviewed', { mode: 'boolean' }).notNull().default(false),
cancellationReviewedAt: integer('cancellation_reviewed_at', { mode: 'timestamp' }),
refundCouponId: integer('refund_coupon_id').references(() => coupons.id),
})
export const payments = sqliteTable('payments', {
id: integer('id').primaryKey({ autoIncrement: true }),
status: text('status').notNull(),
gateway: text('gateway').notNull(),
orderId: integer('order_id').notNull().references(() => orders.id),
token: text('token'),
merchantOrderId: text('merchant_order_id').notNull().unique(),
payload: text('payload'),
})
export const refunds = sqliteTable('refunds', {
id: integer('id').primaryKey({ autoIncrement: true }),
orderId: integer('order_id').notNull().references(() => orders.id),
refundAmount: text('refund_amount'),
refundStatus: text('refund_status').default('none'),
merchantRefundId: text('merchant_refund_id'),
refundProcessedAt: integer('refund_processed_at', { mode: 'timestamp' }),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(epochSeconds),
})
export const keyValStore = sqliteTable('key_val_store', {
key: text('key').primaryKey(),
value: text('value'),
})
export const notifications = sqliteTable('notifications', {
id: integer('id').primaryKey({ autoIncrement: true }),
userId: integer('user_id').notNull().references(() => users.id),
title: text('title').notNull(),
body: text('body').notNull(),
type: text('type'),
isRead: integer('is_read', { mode: 'boolean' }).notNull().default(false),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(epochSeconds),
})
export const productCategories = sqliteTable('product_categories', {
id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name').notNull(),
description: text('description'),
})
export const cartItems = sqliteTable('cart_items', {
id: integer('id').primaryKey({ autoIncrement: true }),
userId: integer('user_id').notNull().references(() => users.id),
productId: integer('product_id').notNull().references(() => productInfo.id),
quantity: text('quantity').notNull(),
addedAt: integer('added_at', { mode: 'timestamp' }).notNull().default(epochSeconds),
}, (t) => ({
unq_user_product: unique('unique_user_product').on(t.userId, t.productId),
}))
export const complaints = sqliteTable('complaints', {
id: integer('id').primaryKey({ autoIncrement: true }),
userId: integer('user_id').notNull().references(() => users.id),
orderId: integer('order_id').references(() => orders.id),
complaintBody: text('complaint_body').notNull(),
images: text('images'),
response: text('response'),
isResolved: integer('is_resolved', { mode: 'boolean' }).notNull().default(false),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(epochSeconds),
})
export const coupons = sqliteTable('coupons', {
id: integer('id').primaryKey({ autoIncrement: true }),
couponCode: text('coupon_code').notNull().unique('unique_coupon_code'),
isUserBased: integer('is_user_based', { mode: 'boolean' }).notNull().default(false),
discountPercent: text('discount_percent'),
flatDiscount: text('flat_discount'),
minOrder: text('min_order'),
productIds: text('product_ids'),
createdBy: integer('created_by').references(() => staffUsers.id),
maxValue: text('max_value'),
isApplyForAll: integer('is_apply_for_all', { mode: 'boolean' }).notNull().default(false),
validTill: integer('valid_till', { mode: 'timestamp' }),
maxLimitForUser: integer('max_limit_for_user'),
isInvalidated: integer('is_invalidated', { mode: 'boolean' }).notNull().default(false),
exclusiveApply: integer('exclusive_apply', { mode: 'boolean' }).notNull().default(false),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(epochSeconds),
})
export const couponUsage = sqliteTable('coupon_usage', {
id: integer('id').primaryKey({ autoIncrement: true }),
userId: integer('user_id').notNull().references(() => users.id),
couponId: integer('coupon_id').notNull().references(() => coupons.id),
orderId: integer('order_id').references(() => orders.id),
orderItemId: integer('order_item_id').references(() => orderItems.id),
usedAt: integer('used_at', { mode: 'timestamp' }).notNull().default(epochSeconds),
})
export const couponApplicableUsers = sqliteTable('coupon_applicable_users', {
id: integer('id').primaryKey({ autoIncrement: true }),
couponId: integer('coupon_id').notNull().references(() => coupons.id),
userId: integer('user_id').notNull().references(() => users.id),
}, (t) => ({
unq_coupon_user: unique('unique_coupon_user').on(t.couponId, t.userId),
}))
export const couponApplicableProducts = sqliteTable('coupon_applicable_products', {
id: integer('id').primaryKey({ autoIncrement: true }),
couponId: integer('coupon_id').notNull().references(() => coupons.id),
productId: integer('product_id').notNull().references(() => productInfo.id),
}, (t) => ({
unq_coupon_product: unique('unique_coupon_product').on(t.couponId, t.productId),
}))
export const userIncidents = sqliteTable('user_incidents', {
id: integer('id').primaryKey({ autoIncrement: true }),
userId: integer('user_id').notNull().references(() => users.id),
orderId: integer('order_id').references(() => orders.id),
dateAdded: integer('date_added', { mode: 'timestamp' }).notNull().default(epochSeconds),
adminComment: text('admin_comment'),
addedBy: integer('added_by').references(() => staffUsers.id),
negativityScore: integer('negativity_score'),
})
export const reservedCoupons = sqliteTable('reserved_coupons', {
id: integer('id').primaryKey({ autoIncrement: true }),
secretCode: text('secret_code').notNull().unique(),
couponCode: text('coupon_code').notNull(),
discountPercent: text('discount_percent'),
flatDiscount: text('flat_discount'),
minOrder: text('min_order'),
productIds: text('product_ids'),
maxValue: text('max_value'),
validTill: integer('valid_till', { mode: 'timestamp' }),
maxLimitForUser: integer('max_limit_for_user'),
exclusiveApply: integer('exclusive_apply', { mode: 'boolean' }).notNull().default(false),
isRedeemed: integer('is_redeemed', { mode: 'boolean' }).notNull().default(false),
redeemedBy: integer('redeemed_by').references(() => users.id),
redeemedAt: integer('redeemed_at', { mode: 'timestamp' }),
createdBy: integer('created_by').notNull().references(() => staffUsers.id),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(epochSeconds),
}, (t) => ({
unq_secret_code: unique('unique_secret_code').on(t.secretCode),
}))
export const notifCreds = sqliteTable('notif_creds', {
id: integer('id').primaryKey({ autoIncrement: true }),
token: text('token').notNull().unique(),
addedAt: integer('added_at', { mode: 'timestamp' }).notNull().default(epochSeconds),
userId: integer('user_id').notNull().references(() => users.id),
lastVerified: integer('last_verified', { mode: 'timestamp' }),
})
export const unloggedUserTokens = sqliteTable('unlogged_user_tokens', {
id: integer('id').primaryKey({ autoIncrement: true }),
token: text('token').notNull().unique(),
addedAt: integer('added_at', { mode: 'timestamp' }).notNull().default(epochSeconds),
lastVerified: integer('last_verified', { mode: 'timestamp' }),
})
export const userNotifications = sqliteTable('user_notifications', {
id: integer('id').primaryKey({ autoIncrement: true }),
title: text('title').notNull(),
imageUrl: text('image_url'),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(epochSeconds),
body: text('body').notNull(),
applicableUsers: text('applicable_users'),
})
export const uploadUrlStatusRelations = relations(uploadUrlStatus, ({}) => ({}))
export const userCredsRelations = relations(userCreds, ({ one }) => ({
user: one(users, { fields: [userCreds.userId], references: [users.id] }),
}))
export const staffUsersRelations = relations(staffUsers, ({ one, many }) => ({
role: one(staffRoles, { fields: [staffUsers.staffRoleId], references: [staffRoles.id] }),
coupons: many(coupons),
stores: many(storeInfo),
}))
export const addressesRelations = relations(addresses, ({ one, many }) => ({
user: one(users, { fields: [addresses.userId], references: [users.id] }),
orders: many(orders),
zone: one(addressZones, { fields: [addresses.zoneId], references: [addressZones.id] }),
}))
export const unitsRelations = relations(units, ({ many }) => ({
products: many(productInfo),
}))
export const productInfoRelations = relations(productInfo, ({ one, many }) => ({
unit: one(units, { fields: [productInfo.unitId], references: [units.id] }),
store: one(storeInfo, { fields: [productInfo.storeId], references: [storeInfo.id] }),
productSlots: many(productSlots),
specialDeals: many(specialDeals),
orderItems: many(orderItems),
cartItems: many(cartItems),
tags: many(productTags),
applicableCoupons: many(couponApplicableProducts),
reviews: many(productReviews),
groups: many(productGroupMembership),
}))
export const productTagInfoRelations = relations(productTagInfo, ({ many }) => ({
products: many(productTags),
}))
export const productTagsRelations = relations(productTags, ({ one }) => ({
product: one(productInfo, { fields: [productTags.productId], references: [productInfo.id] }),
tag: one(productTagInfo, { fields: [productTags.tagId], references: [productTagInfo.id] }),
}))
export const deliverySlotInfoRelations = relations(deliverySlotInfo, ({ many }) => ({
productSlots: many(productSlots),
orders: many(orders),
vendorSnippets: many(vendorSnippets),
}))
export const vendorSnippetsRelations = relations(vendorSnippets, ({ one }) => ({
slot: one(deliverySlotInfo, { fields: [vendorSnippets.slotId], references: [deliverySlotInfo.id] }),
}))
export const productSlotsRelations = relations(productSlots, ({ one }) => ({
product: one(productInfo, { fields: [productSlots.productId], references: [productInfo.id] }),
slot: one(deliverySlotInfo, { fields: [productSlots.slotId], references: [deliverySlotInfo.id] }),
}))
export const specialDealsRelations = relations(specialDeals, ({ one }) => ({
product: one(productInfo, { fields: [specialDeals.productId], references: [productInfo.id] }),
}))
export const ordersRelations = relations(orders, ({ one, many }) => ({
user: one(users, { fields: [orders.userId], references: [users.id] }),
address: one(addresses, { fields: [orders.addressId], references: [addresses.id] }),
slot: one(deliverySlotInfo, { fields: [orders.slotId], references: [deliverySlotInfo.id] }),
orderItems: many(orderItems),
payment: one(payments),
paymentInfo: one(paymentInfoTable, { fields: [orders.paymentInfoId], references: [paymentInfoTable.id] }),
orderStatus: many(orderStatus),
refunds: many(refunds),
couponUsages: many(couponUsage),
userIncidents: many(userIncidents),
}))
export const orderItemsRelations = relations(orderItems, ({ one }) => ({
order: one(orders, { fields: [orderItems.orderId], references: [orders.id] }),
product: one(productInfo, { fields: [orderItems.productId], references: [productInfo.id] }),
}))
export const orderStatusRelations = relations(orderStatus, ({ one }) => ({
order: one(orders, { fields: [orderStatus.orderId], references: [orders.id] }),
user: one(users, { fields: [orderStatus.userId], references: [users.id] }),
refundCoupon: one(coupons, { fields: [orderStatus.refundCouponId], references: [coupons.id] }),
}))
export const paymentInfoRelations = relations(paymentInfoTable, ({ one }) => ({
order: one(orders, { fields: [paymentInfoTable.id], references: [orders.paymentInfoId] }),
}))
export const paymentsRelations = relations(payments, ({ one }) => ({
order: one(orders, { fields: [payments.orderId], references: [orders.id] }),
}))
export const refundsRelations = relations(refunds, ({ one }) => ({
order: one(orders, { fields: [refunds.orderId], references: [orders.id] }),
}))
export const notificationsRelations = relations(notifications, ({ one }) => ({
user: one(users, { fields: [notifications.userId], references: [users.id] }),
}))
export const productCategoriesRelations = relations(productCategories, ({}) => ({}))
export const cartItemsRelations = relations(cartItems, ({ one }) => ({
user: one(users, { fields: [cartItems.userId], references: [users.id] }),
product: one(productInfo, { fields: [cartItems.productId], references: [productInfo.id] }),
}))
export const complaintsRelations = relations(complaints, ({ one }) => ({
user: one(users, { fields: [complaints.userId], references: [users.id] }),
order: one(orders, { fields: [complaints.orderId], references: [orders.id] }),
}))
export const couponsRelations = relations(coupons, ({ one, many }) => ({
creator: one(staffUsers, { fields: [coupons.createdBy], references: [staffUsers.id] }),
usages: many(couponUsage),
applicableUsers: many(couponApplicableUsers),
applicableProducts: many(couponApplicableProducts),
}))
export const couponUsageRelations = relations(couponUsage, ({ one }) => ({
user: one(users, { fields: [couponUsage.userId], references: [users.id] }),
coupon: one(coupons, { fields: [couponUsage.couponId], references: [coupons.id] }),
order: one(orders, { fields: [couponUsage.orderId], references: [orders.id] }),
orderItem: one(orderItems, { fields: [couponUsage.orderItemId], references: [orderItems.id] }),
}))
export const userDetailsRelations = relations(userDetails, ({ one }) => ({
user: one(users, { fields: [userDetails.userId], references: [users.id] }),
}))
export const notifCredsRelations = relations(notifCreds, ({ one }) => ({
user: one(users, { fields: [notifCreds.userId], references: [users.id] }),
}))
export const userNotificationsRelations = relations(userNotifications, ({}) => ({}))
export const storeInfoRelations = relations(storeInfo, ({ one, many }) => ({
owner: one(staffUsers, { fields: [storeInfo.owner], references: [staffUsers.id] }),
products: many(productInfo),
}))
export const couponApplicableUsersRelations = relations(couponApplicableUsers, ({ one }) => ({
coupon: one(coupons, { fields: [couponApplicableUsers.couponId], references: [coupons.id] }),
user: one(users, { fields: [couponApplicableUsers.userId], references: [users.id] }),
}))
export const couponApplicableProductsRelations = relations(couponApplicableProducts, ({ one }) => ({
coupon: one(coupons, { fields: [couponApplicableProducts.couponId], references: [coupons.id] }),
product: one(productInfo, { fields: [couponApplicableProducts.productId], references: [productInfo.id] }),
}))
export const reservedCouponsRelations = relations(reservedCoupons, ({ one }) => ({
redeemedUser: one(users, { fields: [reservedCoupons.redeemedBy], references: [users.id] }),
creator: one(staffUsers, { fields: [reservedCoupons.createdBy], references: [staffUsers.id] }),
}))
export const productReviewsRelations = relations(productReviews, ({ one }) => ({
user: one(users, { fields: [productReviews.userId], references: [users.id] }),
product: one(productInfo, { fields: [productReviews.productId], references: [productInfo.id] }),
}))
export const addressZonesRelations = relations(addressZones, ({ many }) => ({
addresses: many(addresses),
areas: many(addressAreas),
}))
export const addressAreasRelations = relations(addressAreas, ({ one }) => ({
zone: one(addressZones, { fields: [addressAreas.zoneId], references: [addressZones.id] }),
}))
export const productGroupInfoRelations = relations(productGroupInfo, ({ many }) => ({
memberships: many(productGroupMembership),
}))
export const productGroupMembershipRelations = relations(productGroupMembership, ({ one }) => ({
product: one(productInfo, { fields: [productGroupMembership.productId], references: [productInfo.id] }),
group: one(productGroupInfo, { fields: [productGroupMembership.groupId], references: [productGroupInfo.id] }),
}))
export const homeBannersRelations = relations(homeBanners, ({}) => ({}))
export const staffRolesRelations = relations(staffRoles, ({ many }) => ({
staffUsers: many(staffUsers),
rolePermissions: many(staffRolePermissions),
}))
export const staffPermissionsRelations = relations(staffPermissions, ({ many }) => ({
rolePermissions: many(staffRolePermissions),
}))
export const staffRolePermissionsRelations = relations(staffRolePermissions, ({ one }) => ({
role: one(staffRoles, { fields: [staffRolePermissions.staffRoleId], references: [staffRoles.id] }),
permission: one(staffPermissions, { fields: [staffRolePermissions.staffPermissionId], references: [staffPermissions.id] }),
}))
export const userIncidentsRelations = relations(userIncidents, ({ one }) => ({
user: one(users, { fields: [userIncidents.userId], references: [users.id] }),
order: one(orders, { fields: [userIncidents.orderId], references: [orders.id] }),
addedBy: one(staffUsers, { fields: [userIncidents.addedBy], references: [staffUsers.id] }),
}))
export const productAvailabilitySchedulesRelations = relations(
productAvailabilitySchedules,
({}) => ({})
)
export const usersRelations = relations(users, ({ many, one }) => ({
addresses: many(addresses),
orders: many(orders),
notifications: many(notifications),
cartItems: many(cartItems),
userCreds: one(userCreds),
coupons: many(coupons),
couponUsages: many(couponUsage),
applicableCoupons: many(couponApplicableUsers),
userDetails: one(userDetails),
notifCreds: many(notifCreds),
userIncidents: many(userIncidents),
}))

707
apps/backend/src/db/schema.ts Normal file → Executable file
View file

@ -1 +1,706 @@
export * from './schema-sqlite'
import { pgTable, pgSchema, integer, varchar, date, boolean, timestamp, numeric, jsonb, pgEnum, unique, real, text, check, decimal } from "drizzle-orm/pg-core";
import { relations, sql } from "drizzle-orm";
const mf = pgSchema('mf');
export const users = mf.table('users', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
name: varchar({ length: 255 }),
email: varchar({ length: 255 }),
mobile: varchar({ length: 255 }),
createdAt: timestamp('created_at').notNull().defaultNow(),
}, (t) => ({
unq_email: unique('unique_email').on(t.email),
}));
export const userDetails = mf.table('user_details', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
userId: integer('user_id').notNull().references(() => users.id).unique(),
bio: varchar('bio', { length: 500 }),
dateOfBirth: date('date_of_birth'),
gender: varchar('gender', { length: 20 }),
occupation: varchar('occupation', { length: 100 }),
profileImage: varchar('profile_image', { length: 500 }),
isSuspended: boolean('is_suspended').notNull().default(false),
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
});
export const userCreds = mf.table('user_creds', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
userId: integer('user_id').notNull().references(() => users.id),
userPassword: varchar('user_password', { length: 255 }).notNull(),
createdAt: timestamp('created_at').notNull().defaultNow(),
});
export const addresses = mf.table('addresses', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
userId: integer('user_id').notNull().references(() => users.id),
name: varchar('name', { length: 255 }).notNull(),
phone: varchar('phone', { length: 15 }).notNull(),
addressLine1: varchar('address_line1', { length: 255 }).notNull(),
addressLine2: varchar('address_line2', { length: 255 }),
city: varchar('city', { length: 100 }).notNull(),
state: varchar('state', { length: 100 }).notNull(),
pincode: varchar('pincode', { length: 10 }).notNull(),
isDefault: boolean('is_default').notNull().default(false),
latitude: real('latitude'),
longitude: real('longitude'),
googleMapsUrl: varchar('google_maps_url', { length: 500 }),
adminLatitude: real('admin_latitude'),
adminLongitude: real('admin_longitude'),
zoneId: integer('zone_id').references(() => addressZones.id),
createdAt: timestamp('created_at').notNull().defaultNow(),
});
export const addressZones = mf.table('address_zones', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
zoneName: varchar('zone_name', { length: 255 }).notNull(),
addedAt: timestamp('added_at').notNull().defaultNow(),
});
export const addressAreas = mf.table('address_areas', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
placeName: varchar('place_name', { length: 255 }).notNull(),
zoneId: integer('zone_id').references(() => addressZones.id),
createdAt: timestamp('created_at').notNull().defaultNow(),
});
export const staffUsers = mf.table('staff_users', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
name: varchar({ length: 255 }).notNull(),
password: varchar({ length: 255 }).notNull(),
staffRoleId: integer('staff_role_id').references(() => staffRoles.id),
createdAt: timestamp('created_at').notNull().defaultNow(),
});
export const storeInfo = mf.table('store_info', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
name: varchar({ length: 255 }).notNull(),
description: varchar({ length: 500 }),
imageUrl: varchar('image_url', { length: 500 }),
createdAt: timestamp('created_at').notNull().defaultNow(),
owner: integer('owner').notNull().references(() => staffUsers.id),
});
export const units = mf.table('units', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
shortNotation: varchar('short_notation', { length: 50 }).notNull(),
fullName: varchar('full_name', { length: 100 }).notNull(),
}, (t) => ({
unq_short_notation: unique('unique_short_notation').on(t.shortNotation),
}));
export const productAvailabilityActionEnum = pgEnum('product_availability_action', ['in', 'out']);
export const productInfo = mf.table('product_info', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
name: varchar({ length: 255 }).notNull(),
shortDescription: varchar('short_description', { length: 500 }),
longDescription: varchar('long_description', { length: 1000 }),
unitId: integer('unit_id').notNull().references(() => units.id),
price: numeric({ precision: 10, scale: 2 }).notNull(),
marketPrice: numeric('market_price', { precision: 10, scale: 2 }),
images: jsonb('images'),
isOutOfStock: boolean('is_out_of_stock').notNull().default(false),
isSuspended: boolean('is_suspended').notNull().default(false),
isFlashAvailable: boolean('is_flash_available').notNull().default(false),
flashPrice: numeric('flash_price', { precision: 10, scale: 2 }),
createdAt: timestamp('created_at').notNull().defaultNow(),
incrementStep: real('increment_step').notNull().default(1),
productQuantity: real('product_quantity').notNull().default(1),
storeId: integer('store_id').references(() => storeInfo.id),
scheduledAvailability: boolean('scheduled_availability').notNull().default(true),
});
export const productAvailabilitySchedules = mf.table('product_availability_schedules', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
time: varchar('time', { length: 10 }).notNull(),
scheduleName: varchar('schedule_name', { length: 255 }).notNull().unique(),
action: productAvailabilityActionEnum('action').notNull(),
productIds: integer('product_ids').array().notNull().default([]),
groupIds: integer('group_ids').array().notNull().default([]),
createdAt: timestamp('created_at').notNull().defaultNow(),
lastUpdated: timestamp('last_updated').notNull().defaultNow(),
});
export const productGroupInfo = mf.table('product_group_info', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
groupName: varchar('group_name', { length: 255 }).notNull(),
description: varchar({ length: 500 }),
createdAt: timestamp('created_at').notNull().defaultNow(),
});
export const productGroupMembership = mf.table('product_group_membership', {
productId: integer('product_id').notNull().references(() => productInfo.id),
groupId: integer('group_id').notNull().references(() => productGroupInfo.id),
addedAt: timestamp('added_at').notNull().defaultNow(),
}, (t) => ({
pk: unique('product_group_membership_pk').on(t.productId, t.groupId),
}));
export const homeBanners = mf.table('home_banners', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
name: varchar('name', { length: 255 }).notNull(),
imageUrl: varchar('image_url', { length: 500 }).notNull(),
description: varchar('description', { length: 500 }),
productIds: integer('product_ids').array(),
redirectUrl: varchar('redirect_url', { length: 500 }),
serialNum: integer('serial_num'),
isActive: boolean('is_active').notNull().default(false),
createdAt: timestamp('created_at').notNull().defaultNow(),
lastUpdated: timestamp('last_updated').notNull().defaultNow(),
});
export const productReviews = mf.table('product_reviews', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
userId: integer('user_id').notNull().references(() => users.id),
productId: integer('product_id').notNull().references(() => productInfo.id),
reviewBody: text('review_body').notNull(),
imageUrls: jsonb('image_urls').$defaultFn(() => []),
reviewTime: timestamp('review_time').notNull().defaultNow(),
ratings: real('ratings').notNull(),
adminResponse: text('admin_response'),
adminResponseImages: jsonb('admin_response_images').$defaultFn(() => []),
}, (t) => ({
ratingCheck: check('rating_check', sql`${t.ratings} >= 1 AND ${t.ratings} <= 5`),
}));
export const uploadStatusEnum = pgEnum('upload_status', ['pending', 'claimed']);
export const staffRoleEnum = pgEnum('staff_role', ['super_admin', 'admin', 'marketer', 'delivery_staff']);
export const staffPermissionEnum = pgEnum('staff_permission', ['crud_product', 'make_coupon', 'crud_staff_users']);
export const uploadUrlStatus = mf.table('upload_url_status', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
createdAt: timestamp('created_at').notNull().defaultNow(),
key: varchar('key', { length: 500 }).notNull(),
status: uploadStatusEnum('status').notNull().default('pending'),
});
export const productTagInfo = mf.table('product_tag_info', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
tagName: varchar('tag_name', { length: 100 }).notNull().unique(),
tagDescription: varchar('tag_description', { length: 500 }),
imageUrl: varchar('image_url', { length: 500 }),
isDashboardTag: boolean('is_dashboard_tag').notNull().default(false),
relatedStores: jsonb('related_stores').$defaultFn(() => []),
createdAt: timestamp('created_at').notNull().defaultNow(),
});
export const productTags = mf.table('product_tags', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
productId: integer('product_id').notNull().references(() => productInfo.id),
tagId: integer('tag_id').notNull().references(() => productTagInfo.id),
assignedAt: timestamp('assigned_at').notNull().defaultNow(),
}, (t) => ({
unq_product_tag: unique('unique_product_tag').on(t.productId, t.tagId),
}));
export const deliverySlotInfo = mf.table('delivery_slot_info', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
deliveryTime: timestamp('delivery_time').notNull(),
freezeTime: timestamp('freeze_time').notNull(),
isActive: boolean('is_active').notNull().default(true),
isFlash: boolean('is_flash').notNull().default(false),
isCapacityFull: boolean('is_capacity_full').notNull().default(false),
deliverySequence: jsonb('delivery_sequence').$defaultFn(() => {}),
groupIds: jsonb('group_ids').$defaultFn(() => []),
});
export const vendorSnippets = mf.table('vendor_snippets', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
snippetCode: varchar('snippet_code', { length: 255 }).notNull().unique(),
slotId: integer('slot_id').references(() => deliverySlotInfo.id),
isPermanent: boolean('is_permanent').notNull().default(false),
productIds: integer('product_ids').array().notNull(),
validTill: timestamp('valid_till'),
createdAt: timestamp('created_at').notNull().defaultNow(),
});
export const vendorSnippetsRelations = relations(vendorSnippets, ({ one }) => ({
slot: one(deliverySlotInfo, { fields: [vendorSnippets.slotId], references: [deliverySlotInfo.id] }),
}));
export const productSlots = mf.table('product_slots', {
productId: integer('product_id').notNull().references(() => productInfo.id),
slotId: integer('slot_id').notNull().references(() => deliverySlotInfo.id),
}, (t) => ({
pk: unique('product_slot_pk').on(t.productId, t.slotId),
}));
export const specialDeals = mf.table('special_deals', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
productId: integer('product_id').notNull().references(() => productInfo.id),
quantity: numeric({ precision: 10, scale: 2 }).notNull(),
price: numeric({ precision: 10, scale: 2 }).notNull(),
validTill: timestamp('valid_till').notNull(),
});
export const orders = mf.table('orders', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
userId: integer('user_id').notNull().references(() => users.id),
addressId: integer('address_id').notNull().references(() => addresses.id),
slotId: integer('slot_id').references(() => deliverySlotInfo.id),
isCod: boolean('is_cod').notNull().default(false),
isOnlinePayment: boolean('is_online_payment').notNull().default(false),
paymentInfoId: integer('payment_info_id').references(() => paymentInfoTable.id),
totalAmount: numeric('total_amount', { precision: 10, scale: 2 }).notNull(),
deliveryCharge: numeric('delivery_charge', { precision: 10, scale: 2 }).notNull().default('0'),
readableId: integer('readable_id').notNull(),
adminNotes: text('admin_notes'),
userNotes: text('user_notes'),
orderGroupId: varchar('order_group_id', { length: 255 }),
orderGroupProportion: decimal('order_group_proportion', { precision: 10, scale: 4 }),
isFlashDelivery: boolean('is_flash_delivery').notNull().default(false),
createdAt: timestamp('created_at').notNull().defaultNow(),
});
export const orderItems = mf.table('order_items', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
orderId: integer('order_id').notNull().references(() => orders.id),
productId: integer('product_id').notNull().references(() => productInfo.id),
quantity: varchar('quantity', { length: 50 }).notNull(),
price: numeric({ precision: 10, scale: 2 }).notNull(),
discountedPrice: numeric('discounted_price', { precision: 10, scale: 2 }),
is_packaged: boolean('is_packaged').notNull().default(false),
is_package_verified: boolean('is_package_verified').notNull().default(false),
});
export const paymentStatusEnum = pgEnum('payment_status', ['pending', 'success', 'cod', 'failed']);
export const orderStatus = mf.table('order_status', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
orderTime: timestamp('order_time').notNull().defaultNow(),
userId: integer('user_id').notNull().references(() => users.id),
orderId: integer('order_id').notNull().references(() => orders.id),
isPackaged: boolean('is_packaged').notNull().default(false),
isDelivered: boolean('is_delivered').notNull().default(false),
isCancelled: boolean('is_cancelled').notNull().default(false),
cancelReason: varchar('cancel_reason', { length: 255 }),
isCancelledByAdmin: boolean('is_cancelled_by_admin'),
paymentStatus: paymentStatusEnum('payment_state').notNull().default('pending'),
cancellationUserNotes: text('cancellation_user_notes'),
cancellationAdminNotes: text('cancellation_admin_notes'),
cancellationReviewed: boolean('cancellation_reviewed').notNull().default(false),
cancellationReviewedAt: timestamp('cancellation_reviewed_at'),
refundCouponId: integer('refund_coupon_id').references(() => coupons.id),
});
export const paymentInfoTable = mf.table('payment_info', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
status: varchar({ length: 50 }).notNull(),
gateway: varchar({ length: 50 }).notNull(),
orderId: varchar('order_id', { length: 500 }),
token: varchar({ length: 500 }),
merchantOrderId: varchar('merchant_order_id', { length: 255 }).notNull().unique(),
payload: jsonb('payload'),
});
export const payments = mf.table('payments', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
status: varchar({ length: 50 }).notNull(),
gateway: varchar({ length: 50 }).notNull(),
orderId: integer('order_id').notNull().references(() => orders.id),
token: varchar({ length: 500 }),
merchantOrderId: varchar('merchant_order_id', { length: 255 }).notNull().unique(),
payload: jsonb('payload'),
});
export const refunds = mf.table('refunds', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
orderId: integer('order_id').notNull().references(() => orders.id),
refundAmount: numeric('refund_amount', { precision: 10, scale: 2 }),
refundStatus: varchar('refund_status', { length: 50 }).default('none'),
merchantRefundId: varchar('merchant_refund_id', { length: 255 }),
refundProcessedAt: timestamp('refund_processed_at'),
createdAt: timestamp('created_at').notNull().defaultNow(),
});
export const keyValStore = mf.table('key_val_store', {
key: varchar('key', { length: 255 }).primaryKey(),
value: jsonb('value'),
});
export const notifications = mf.table('notifications', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
userId: integer('user_id').notNull().references(() => users.id),
title: varchar({ length: 255 }).notNull(),
body: varchar({ length: 512 }).notNull(),
type: varchar({ length: 50 }),
isRead: boolean('is_read').notNull().default(false),
createdAt: timestamp('created_at').notNull().defaultNow(),
});
export const productCategories = mf.table('product_categories', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
name: varchar({ length: 255 }).notNull(),
description: varchar({ length: 500 }),
});
export const cartItems = mf.table('cart_items', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
userId: integer('user_id').notNull().references(() => users.id),
productId: integer('product_id').notNull().references(() => productInfo.id),
quantity: numeric({ precision: 10, scale: 2 }).notNull(),
addedAt: timestamp('added_at').notNull().defaultNow(),
}, (t) => ({
unq_user_product: unique('unique_user_product').on(t.userId, t.productId),
}));
export const complaints = mf.table('complaints', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
userId: integer('user_id').notNull().references(() => users.id),
orderId: integer('order_id').references(() => orders.id),
complaintBody: varchar('complaint_body', { length: 1000 }).notNull(),
images: jsonb('images'),
response: varchar('response', { length: 1000 }),
isResolved: boolean('is_resolved').notNull().default(false),
createdAt: timestamp('created_at').notNull().defaultNow(),
});
export const coupons = mf.table('coupons', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
couponCode: varchar('coupon_code', { length: 50 }).notNull().unique('unique_coupon_code'),
isUserBased: boolean('is_user_based').notNull().default(false),
discountPercent: numeric('discount_percent', { precision: 5, scale: 2 }),
flatDiscount: numeric('flat_discount', { precision: 10, scale: 2 }),
minOrder: numeric('min_order', { precision: 10, scale: 2 }),
productIds: jsonb('product_ids'),
createdBy: integer('created_by').references(() => staffUsers.id),
maxValue: numeric('max_value', { precision: 10, scale: 2 }),
isApplyForAll: boolean('is_apply_for_all').notNull().default(false),
validTill: timestamp('valid_till'),
maxLimitForUser: integer('max_limit_for_user'),
isInvalidated: boolean('is_invalidated').notNull().default(false),
exclusiveApply: boolean('exclusive_apply').notNull().default(false),
createdAt: timestamp('created_at').notNull().defaultNow(),
});
export const couponUsage = mf.table('coupon_usage', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
userId: integer('user_id').notNull().references(() => users.id),
couponId: integer('coupon_id').notNull().references(() => coupons.id),
orderId: integer('order_id').references(() => orders.id),
orderItemId: integer('order_item_id').references(() => orderItems.id),
usedAt: timestamp('used_at').notNull().defaultNow(),
});
export const couponApplicableUsers = mf.table('coupon_applicable_users', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
couponId: integer('coupon_id').notNull().references(() => coupons.id),
userId: integer('user_id').notNull().references(() => users.id),
}, (t) => ({
unq_coupon_user: unique('unique_coupon_user').on(t.couponId, t.userId),
}));
export const couponApplicableProducts = mf.table('coupon_applicable_products', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
couponId: integer('coupon_id').notNull().references(() => coupons.id),
productId: integer('product_id').notNull().references(() => productInfo.id),
}, (t) => ({
unq_coupon_product: unique('unique_coupon_product').on(t.couponId, t.productId),
}));
export const userIncidents = mf.table('user_incidents', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
userId: integer('user_id').notNull().references(() => users.id),
orderId: integer('order_id').references(() => orders.id),
dateAdded: timestamp('date_added').notNull().defaultNow(),
adminComment: text('admin_comment'),
addedBy: integer('added_by').references(() => staffUsers.id),
negativityScore: integer('negativity_score'),
});
export const reservedCoupons = mf.table('reserved_coupons', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
secretCode: varchar('secret_code', { length: 50 }).notNull().unique(),
couponCode: varchar('coupon_code', { length: 50 }).notNull(),
discountPercent: numeric('discount_percent', { precision: 5, scale: 2 }),
flatDiscount: numeric('flat_discount', { precision: 10, scale: 2 }),
minOrder: numeric('min_order', { precision: 10, scale: 2 }),
productIds: jsonb('product_ids'),
maxValue: numeric('max_value', { precision: 10, scale: 2 }),
validTill: timestamp('valid_till'),
maxLimitForUser: integer('max_limit_for_user'),
exclusiveApply: boolean('exclusive_apply').notNull().default(false),
isRedeemed: boolean('is_redeemed').notNull().default(false),
redeemedBy: integer('redeemed_by').references(() => users.id),
redeemedAt: timestamp('redeemed_at'),
createdBy: integer('created_by').notNull().references(() => staffUsers.id),
createdAt: timestamp('created_at').notNull().defaultNow(),
}, (t) => ({
unq_secret_code: unique('unique_secret_code').on(t.secretCode),
}));
export const notifCreds = mf.table('notif_creds', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
token: varchar({ length: 500 }).notNull().unique(),
addedAt: timestamp('added_at').notNull().defaultNow(),
userId: integer('user_id').notNull().references(() => users.id),
lastVerified: timestamp('last_verified'),
});
export const unloggedUserTokens = mf.table('unlogged_user_tokens', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
token: varchar({ length: 500 }).notNull().unique(),
addedAt: timestamp('added_at').notNull().defaultNow(),
lastVerified: timestamp('last_verified'),
});
export const userNotifications = mf.table('user_notifications', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
title: varchar('title', { length: 255 }).notNull(),
imageUrl: varchar('image_url', { length: 500 }),
createdAt: timestamp('created_at').notNull().defaultNow(),
body: text('body').notNull(),
applicableUsers: jsonb('applicable_users'),
});
export const staffRoles = mf.table('staff_roles', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
roleName: staffRoleEnum('role_name').notNull(),
createdAt: timestamp('created_at').notNull().defaultNow(),
}, (t) => ({
unq_role_name: unique('unique_role_name').on(t.roleName),
}));
export const staffPermissions = mf.table('staff_permissions', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
permissionName: staffPermissionEnum('permission_name').notNull(),
createdAt: timestamp('created_at').notNull().defaultNow(),
}, (t) => ({
unq_permission_name: unique('unique_permission_name').on(t.permissionName),
}));
export const staffRolePermissions = mf.table('staff_role_permissions', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
staffRoleId: integer('staff_role_id').notNull().references(() => staffRoles.id),
staffPermissionId: integer('staff_permission_id').notNull().references(() => staffPermissions.id),
createdAt: timestamp('created_at').notNull().defaultNow(),
}, (t) => ({
unq_role_permission: unique('unique_role_permission').on(t.staffRoleId, t.staffPermissionId),
}));
// Relations
export const usersRelations = relations(users, ({ many, one }) => ({
addresses: many(addresses),
orders: many(orders),
notifications: many(notifications),
cartItems: many(cartItems),
userCreds: one(userCreds),
coupons: many(coupons),
couponUsages: many(couponUsage),
applicableCoupons: many(couponApplicableUsers),
userDetails: one(userDetails),
notifCreds: many(notifCreds),
userIncidents: many(userIncidents),
}));
export const userCredsRelations = relations(userCreds, ({ one }) => ({
user: one(users, { fields: [userCreds.userId], references: [users.id] }),
}));
export const staffUsersRelations = relations(staffUsers, ({ one, many }) => ({
role: one(staffRoles, { fields: [staffUsers.staffRoleId], references: [staffRoles.id] }),
coupons: many(coupons),
stores: many(storeInfo),
}));
export const addressesRelations = relations(addresses, ({ one, many }) => ({
user: one(users, { fields: [addresses.userId], references: [users.id] }),
orders: many(orders),
zone: one(addressZones, { fields: [addresses.zoneId], references: [addressZones.id] }),
}));
export const unitsRelations = relations(units, ({ many }) => ({
products: many(productInfo),
}));
export const productInfoRelations = relations(productInfo, ({ one, many }) => ({
unit: one(units, { fields: [productInfo.unitId], references: [units.id] }),
store: one(storeInfo, { fields: [productInfo.storeId], references: [storeInfo.id] }),
productSlots: many(productSlots),
specialDeals: many(specialDeals),
orderItems: many(orderItems),
cartItems: many(cartItems),
tags: many(productTags),
applicableCoupons: many(couponApplicableProducts),
reviews: many(productReviews),
groups: many(productGroupMembership),
}));
export const productTagInfoRelations = relations(productTagInfo, ({ many }) => ({
products: many(productTags),
}));
export const productTagsRelations = relations(productTags, ({ one }) => ({
product: one(productInfo, { fields: [productTags.productId], references: [productInfo.id] }),
tag: one(productTagInfo, { fields: [productTags.tagId], references: [productTagInfo.id] }),
}));
export const deliverySlotInfoRelations = relations(deliverySlotInfo, ({ many }) => ({
productSlots: many(productSlots),
orders: many(orders),
vendorSnippets: many(vendorSnippets),
}));
export const productSlotsRelations = relations(productSlots, ({ one }) => ({
product: one(productInfo, { fields: [productSlots.productId], references: [productInfo.id] }),
slot: one(deliverySlotInfo, { fields: [productSlots.slotId], references: [deliverySlotInfo.id] }),
}));
export const specialDealsRelations = relations(specialDeals, ({ one }) => ({
product: one(productInfo, { fields: [specialDeals.productId], references: [productInfo.id] }),
}));
export const ordersRelations = relations(orders, ({ one, many }) => ({
user: one(users, { fields: [orders.userId], references: [users.id] }),
address: one(addresses, { fields: [orders.addressId], references: [addresses.id] }),
slot: one(deliverySlotInfo, { fields: [orders.slotId], references: [deliverySlotInfo.id] }),
orderItems: many(orderItems),
payment: one(payments),
paymentInfo: one(paymentInfoTable, { fields: [orders.paymentInfoId], references: [paymentInfoTable.id] }),
orderStatus: many(orderStatus),
refunds: many(refunds),
couponUsages: many(couponUsage),
userIncidents: many(userIncidents),
}));
export const orderItemsRelations = relations(orderItems, ({ one }) => ({
order: one(orders, { fields: [orderItems.orderId], references: [orders.id] }),
product: one(productInfo, { fields: [orderItems.productId], references: [productInfo.id] }),
}));
export const orderStatusRelations = relations(orderStatus, ({ one }) => ({
order: one(orders, { fields: [orderStatus.orderId], references: [orders.id] }),
user: one(users, { fields: [orderStatus.userId], references: [users.id] }),
refundCoupon: one(coupons, { fields: [orderStatus.refundCouponId], references: [coupons.id] }),
}));
export const paymentInfoRelations = relations(paymentInfoTable, ({ one }) => ({
order: one(orders, { fields: [paymentInfoTable.id], references: [orders.paymentInfoId] }),
}));
export const paymentsRelations = relations(payments, ({ one }) => ({
order: one(orders, { fields: [payments.orderId], references: [orders.id] }),
}));
export const refundsRelations = relations(refunds, ({ one }) => ({
order: one(orders, { fields: [refunds.orderId], references: [orders.id] }),
}));
export const notificationsRelations = relations(notifications, ({ one }) => ({
user: one(users, { fields: [notifications.userId], references: [users.id] }),
}));
export const productCategoriesRelations = relations(productCategories, ({}) => ({}));
export const cartItemsRelations = relations(cartItems, ({ one }) => ({
user: one(users, { fields: [cartItems.userId], references: [users.id] }),
product: one(productInfo, { fields: [cartItems.productId], references: [productInfo.id] }),
}));
export const complaintsRelations = relations(complaints, ({ one }) => ({
user: one(users, { fields: [complaints.userId], references: [users.id] }),
order: one(orders, { fields: [complaints.orderId], references: [orders.id] }),
}));
export const couponsRelations = relations(coupons, ({ one, many }) => ({
creator: one(staffUsers, { fields: [coupons.createdBy], references: [staffUsers.id] }),
usages: many(couponUsage),
applicableUsers: many(couponApplicableUsers),
applicableProducts: many(couponApplicableProducts),
}));
export const couponUsageRelations = relations(couponUsage, ({ one }) => ({
user: one(users, { fields: [couponUsage.userId], references: [users.id] }),
coupon: one(coupons, { fields: [couponUsage.couponId], references: [coupons.id] }),
order: one(orders, { fields: [couponUsage.orderId], references: [orders.id] }),
orderItem: one(orderItems, { fields: [couponUsage.orderItemId], references: [orderItems.id] }),
}));
export const userDetailsRelations = relations(userDetails, ({ one }) => ({
user: one(users, { fields: [userDetails.userId], references: [users.id] }),
}));
export const notifCredsRelations = relations(notifCreds, ({ one }) => ({
user: one(users, { fields: [notifCreds.userId], references: [users.id] }),
}));
export const userNotificationsRelations = relations(userNotifications, ({}) => ({
// No relations needed for now
}));
export const storeInfoRelations = relations(storeInfo, ({ one, many }) => ({
owner: one(staffUsers, { fields: [storeInfo.owner], references: [staffUsers.id] }),
products: many(productInfo),
}));
export const couponApplicableUsersRelations = relations(couponApplicableUsers, ({ one }) => ({
coupon: one(coupons, { fields: [couponApplicableUsers.couponId], references: [coupons.id] }),
user: one(users, { fields: [couponApplicableUsers.userId], references: [users.id] }),
}));
export const couponApplicableProductsRelations = relations(couponApplicableProducts, ({ one }) => ({
coupon: one(coupons, { fields: [couponApplicableProducts.couponId], references: [coupons.id] }),
product: one(productInfo, { fields: [couponApplicableProducts.productId], references: [productInfo.id] }),
}));
export const reservedCouponsRelations = relations(reservedCoupons, ({ one }) => ({
redeemedUser: one(users, { fields: [reservedCoupons.redeemedBy], references: [users.id] }),
creator: one(staffUsers, { fields: [reservedCoupons.createdBy], references: [staffUsers.id] }),
}));
export const productReviewsRelations = relations(productReviews, ({ one }) => ({
user: one(users, { fields: [productReviews.userId], references: [users.id] }),
product: one(productInfo, { fields: [productReviews.productId], references: [productInfo.id] }),
}));
export const addressZonesRelations = relations(addressZones, ({ many }) => ({
addresses: many(addresses),
areas: many(addressAreas),
}));
export const addressAreasRelations = relations(addressAreas, ({ one }) => ({
zone: one(addressZones, { fields: [addressAreas.zoneId], references: [addressZones.id] }),
}));
export const productGroupInfoRelations = relations(productGroupInfo, ({ many }) => ({
memberships: many(productGroupMembership),
}));
export const productGroupMembershipRelations = relations(productGroupMembership, ({ one }) => ({
product: one(productInfo, { fields: [productGroupMembership.productId], references: [productInfo.id] }),
group: one(productGroupInfo, { fields: [productGroupMembership.groupId], references: [productGroupInfo.id] }),
}));
export const homeBannersRelations = relations(homeBanners, ({}) => ({
// Relations for productIds array would be more complex, skipping for now
}));
export const staffRolesRelations = relations(staffRoles, ({ many }) => ({
staffUsers: many(staffUsers),
rolePermissions: many(staffRolePermissions),
}));
export const staffPermissionsRelations = relations(staffPermissions, ({ many }) => ({
rolePermissions: many(staffRolePermissions),
}));
export const staffRolePermissionsRelations = relations(staffRolePermissions, ({ one }) => ({
role: one(staffRoles, { fields: [staffRolePermissions.staffRoleId], references: [staffRoles.id] }),
permission: one(staffPermissions, { fields: [staffRolePermissions.staffPermissionId], references: [staffPermissions.id] }),
}));
export const userIncidentsRelations = relations(userIncidents, ({ one }) => ({
user: one(users, { fields: [userIncidents.userId], references: [users.id] }),
order: one(orders, { fields: [userIncidents.orderId], references: [orders.id] }),
addedBy: one(staffUsers, { fields: [userIncidents.addedBy], references: [staffUsers.id] }),
}));
export const productAvailabilitySchedulesRelations = relations(productAvailabilitySchedules, ({}) => ({
}));

View file

@ -1,34 +0,0 @@
export const parseJsonValue = <T>(value: unknown, fallback: T): T => {
if (value === null || value === undefined) return fallback
if (typeof value === 'string') {
try {
return JSON.parse(value) as T
} catch {
return fallback
}
}
return value as T
}
export const parseNumberArray = (value: unknown): number[] => {
const parsed = parseJsonValue<unknown[]>(value, [])
return parsed
.map((item) => Number(item))
.filter((item) => !Number.isNaN(item))
}
export const toJsonString = (value: unknown, fallback: string): string => {
if (value === null || value === undefined) return fallback
if (typeof value === 'string') return value
return JSON.stringify(value)
}
export const toEpochSeconds = (value: Date | number): number => {
if (typeof value === 'number') return value
return Math.floor(value.getTime() / 1000)
}
export const fromEpochSeconds = (value: number | null | undefined): Date | null => {
if (value === null || value === undefined) return null
return new Date(value * 1000)
}

View file

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

View file

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

View file

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

View file

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

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

@ -1,3 +1,8 @@
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;
@ -11,7 +16,18 @@ class SignedURLCache {
constructor() {
this.originalToSignedCache = 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')
}
}
/**
@ -94,7 +110,7 @@ class SignedURLCache {
clear(): void {
this.originalToSignedCache.clear();
this.signedToOriginalCache.clear();
console.log('SignedURLCache: Cleared all entries');
this.saveToDisk();
}
/**
@ -129,27 +145,119 @@ class SignedURLCache {
}
/**
* Get cache statistics
* Save the cache to disk
*/
getStats(): { totalEntries: number } {
return {
totalEntries: this.originalToSignedCache.size
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
};
}
/**
* Stub methods for backward compatibility - do nothing in in-memory mode
*/
saveToDisk(): void {
// No-op: In-memory cache only
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 {
// No-op: In-memory cache only
try {
if (fs.existsSync(CACHE_FILE_PATH)) {
// Read from file
const data = fs.readFileSync(CACHE_FILE_PATH, 'utf8');
// Parse the data
const parsedData = JSON.parse(data) as {
originalToSigned: Record<string, { value: string; expiresAt: number }>,
signedToOriginal: Record<string, { value: string; expiresAt: number }>
};
// Only load entries that haven't expired yet
const now = Date.now();
let loadedCount = 0;
let expiredCount = 0;
// Load original to signed mappings
if (parsedData.originalToSigned) {
for (const [originalUrl, entry] of Object.entries(parsedData.originalToSigned)) {
if (now <= entry.expiresAt) {
this.originalToSignedCache.set(originalUrl, entry);
loadedCount++;
} else {
expiredCount++;
}
}
}
// Load signed to original mappings
if (parsedData.signedToOriginal) {
for (const [signedUrl, entry] of Object.entries(parsedData.signedToOriginal)) {
if (now <= entry.expiresAt) {
this.signedToOriginalCache.set(signedUrl, entry);
// Don't increment loadedCount as these are pairs of what we already counted
} else {
// Don't increment expiredCount as these are pairs of what we already counted
}
}
}
console.log(`SignedURLCache: Loaded ${loadedCount} valid entries from disk (skipped ${expiredCount} expired entries)`);
} else {
console.log('SignedURLCache: No cache file found, starting with empty cache');
}
} catch (error) {
console.error('Error loading SignedURLCache from disk:', error);
// Start with empty caches if loading fails
this.originalToSignedCache = new Map();
this.signedToOriginalCache = new Map();
}
}
}
// Create a singleton instance to be used throughout the application
const signedUrlCache = new SignedURLCache();
process.on('SIGINT', () => {
console.log('SignedURLCache: Saving cache before shutdown...');
signedUrlCache.saveToDisk();
process.exit(0);
});
process.on('SIGTERM', () => {
console.log('SignedURLCache: Saving cache before shutdown...');
signedUrlCache.saveToDisk();
process.exit(0);
});
export default signedUrlCache;

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,32 @@
import { z } from 'zod';
import { addressZones, addressAreas } from '@/src/db/schema'
import { eq, desc } from 'drizzle-orm';
import { db } from '@/src/db/db_index'
import { router,protectedProcedure } from '@/src/trpc/trpc-index'
const addressRouter = router({
getZones: protectedProcedure.query(async () => {
const zones = await db.select().from(addressZones).orderBy(desc(addressZones.addedAt));
return zones
}),
getAreas: protectedProcedure.query(async () => {
const areas = await db.select().from(addressAreas).orderBy(desc(addressAreas.createdAt));
return areas
}),
createZone: protectedProcedure.input(z.object({ zoneName: z.string().min(1) })).mutation(async ({ input }) => {
const zone = await db.insert(addressZones).values({ zoneName: input.zoneName }).returning();
return {zone: zone};
}),
createArea: protectedProcedure.input(z.object({ placeName: z.string().min(1), zoneId: z.number().nullable() })).mutation(async ({ input }) => {
const area = await db.insert(addressAreas).values({ placeName: input.placeName, zoneId: input.zoneId }).returning();
return {area};
}),
// TODO: Add update and delete mutations if needed
});
export default addressRouter;

View file

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

View file

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

View file

@ -0,0 +1,179 @@
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod';
import { db } from '@/src/db/db_index'
import { orders, orderStatus, users, addresses, orderItems, productInfo, units, refunds } from '@/src/db/schema'
import { eq, desc } from 'drizzle-orm';
const updateCancellationReviewSchema = z.object({
orderId: z.number(),
cancellationReviewed: z.boolean(),
adminNotes: z.string().optional(),
});
const updateRefundSchema = z.object({
orderId: z.number(),
isRefundDone: z.boolean(),
});
export const cancelledOrdersRouter = router({
getAll: protectedProcedure
.query(async () => {
// First get cancelled order statuses with order details
const cancelledOrderStatuses = await db.query.orderStatus.findMany({
where: eq(orderStatus.isCancelled, true),
with: {
order: {
with: {
user: true,
address: true,
orderItems: {
with: {
product: {
with: {
unit: true,
},
},
},
},
refunds: true,
},
},
},
orderBy: [desc(orderStatus.orderTime)],
});
const filteredStatuses = cancelledOrderStatuses.filter(status => {
return status.order.isCod || status.paymentStatus === 'success';
});
return filteredStatuses.map(status => {
const refund = status.order.refunds[0];
return {
id: status.order.id,
readableId: status.order.id,
customerName: `${status.order.user.name}`,
address: `${status.order.address.addressLine1}, ${status.order.address.city}`,
totalAmount: status.order.totalAmount,
cancellationReviewed: status.cancellationReviewed || false,
isRefundDone: refund?.refundStatus === 'processed' || false,
adminNotes: status.order.adminNotes,
cancelReason: status.cancelReason,
paymentMode: status.order.isCod ? 'COD' : 'Online',
paymentStatus: status.paymentStatus || 'pending',
items: status.order.orderItems.map(item => ({
name: item.product.name,
quantity: item.quantity,
price: item.price,
unit: item.product.unit?.shortNotation,
amount: parseFloat(item.price.toString()) * parseFloat(item.quantity || '0'),
})),
createdAt: status.order.createdAt,
};
});
}),
updateReview: protectedProcedure
.input(updateCancellationReviewSchema)
.mutation(async ({ input }) => {
const { orderId, cancellationReviewed, adminNotes } = input;
const result = await db.update(orderStatus)
.set({
cancellationReviewed,
cancellationAdminNotes: adminNotes || null,
cancellationReviewedAt: new Date(),
})
.where(eq(orderStatus.orderId, orderId))
.returning();
if (result.length === 0) {
throw new Error("Cancellation record not found");
}
return result[0];
}),
getById: protectedProcedure
.input(z.object({ id: z.number() }))
.query(async ({ input }) => {
const { id } = input;
// Get cancelled order with full details
const cancelledOrderStatus = await db.query.orderStatus.findFirst({
where: eq(orderStatus.id, id),
with: {
order: {
with: {
user: true,
address: true,
orderItems: {
with: {
product: {
with: {
unit: true,
},
},
},
},
},
},
},
});
if (!cancelledOrderStatus || !cancelledOrderStatus.isCancelled) {
throw new Error("Cancelled order not found");
}
// Get refund details separately
const refund = await db.query.refunds.findFirst({
where: eq(refunds.orderId, cancelledOrderStatus.orderId),
});
const order = cancelledOrderStatus.order;
// Format the response similar to the getAll method
const formattedOrder = {
id: order.id,
readableId: order.id,
customerName: order.user.name,
address: `${order.address.addressLine1}${order.address.addressLine2 ? ', ' + order.address.addressLine2 : ''}, ${order.address.city}, ${order.address.state} ${order.address.pincode}`,
totalAmount: order.totalAmount,
cancellationReviewed: cancelledOrderStatus.cancellationReviewed || false,
isRefundDone: refund?.refundStatus === 'processed' || false,
adminNotes: cancelledOrderStatus.cancellationAdminNotes || null,
cancelReason: cancelledOrderStatus.cancelReason || null,
items: order.orderItems.map((item: any) => ({
name: item.product.name,
quantity: item.quantity,
price: parseFloat(item.price.toString()),
unit: item.product.unit?.shortNotation || 'unit',
amount: parseFloat(item.price.toString()) * parseFloat(item.quantity),
image: item.product.images?.[0] || null,
})),
createdAt: order.createdAt.toISOString(),
};
return { order: formattedOrder };
}),
updateRefund: protectedProcedure
.input(updateRefundSchema)
.mutation(async ({ input }) => {
const { orderId, isRefundDone } = input;
const refundStatus = isRefundDone ? 'processed' : 'none';
const result = await db.update(refunds)
.set({
refundStatus,
refundProcessedAt: isRefundDone ? new Date() : null,
})
.where(eq(refunds.orderId, orderId))
.returning();
if (result.length === 0) {
throw new Error("Cancellation record not found");
}
return result[0];
}),
});

View file

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

View file

@ -1,13 +1,15 @@
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod';
import { db } from '@/src/db/db_index'
import { keyValStore } from '@/src/db/schema'
import { computeConstants } from '@/src/lib/const-store'
import { CONST_KEYS } from '@/src/lib/const-keys'
import { constantDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/main'
export const constRouter = router({
getConstants: protectedProcedure
.query(async () => {
const constants = await constantDbService.getAllConstants();
const constants = await db.select().from(keyValStore);
const resp = constants.map(c => ({
key: c.key,
@ -36,14 +38,23 @@ export const constRouter = router({
throw new Error(`Invalid constant keys: ${invalidKeys.join(', ')}`);
}
const updatedCount = await constantDbService.upsertConstants(constants);
await db.transaction(async (tx) => {
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
await computeConstants();
return {
success: true,
updatedCount,
updatedCount: constants.length,
keys: constants.map(c => c.key),
};
}),

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,55 +0,0 @@
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 { bannerDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/sqlite/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 { complaintDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/sqlite/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 { constantDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/sqlite/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 { couponDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/sqlite/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 { orderDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/sqlite/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 { productDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/sqlite/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 { refundDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/sqlite/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 { scheduleDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/sqlite/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 { slotDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/sqlite/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 { staffUserDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/sqlite/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 { storeDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/sqlite/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 { tagDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/sqlite/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 { userDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/sqlite/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'
export { vendorSnippetDbService } from '@/src/trpc/apis/admin-apis/dataAccessors/sqlite/vendor-snippets-queries'

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,48 +0,0 @@
import { db } from '@/src/db/db_index_sqlite'
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'
import { toJsonString } from '@/src/db/sqlite-casts'
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 normalized = {
...data,
productIds: data.productIds ? toJsonString(data.productIds, '[]') : data.productIds,
}
const [banner] = await db.insert(homeBanners).values(normalized).returning()
return banner
}
async updateBannerById(id: number, data: Partial<NewBanner>): Promise<Banner> {
const normalized = {
...data,
productIds: data.productIds ? toJsonString(data.productIds, '[]') : data.productIds,
lastUpdated: new Date(),
}
const [banner] = await db
.update(homeBanners)
.set(normalized)
.where(eq(homeBanners.id, id))
.returning()
return banner
}
async deleteBannerById(id: number): Promise<void> {
await db.delete(homeBanners).where(eq(homeBanners.id, id))
}
}
export const bannerDbService: IBannerDbService = new BannerDbService()

View file

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

View file

@ -1,26 +0,0 @@
import { db } from '@/src/db/db_index_sqlite'
import { keyValStore } from '@/src/db/schema'
import { IConstantDbService, Constant, NewConstant } from '@/src/trpc/apis/admin-apis/dataAccessors/interfaces/constant-db-service.interface'
import { toJsonString } from '@/src/db/sqlite-casts'
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: toJsonString(value, 'null') })
.onConflictDoUpdate({
target: keyValStore.key,
set: { value: toJsonString(value, 'null') },
})
}
})
return constants.length
}
}
export const constantDbService: IConstantDbService = new ConstantDbService()

View file

@ -1,204 +0,0 @@
import { db } from '@/src/db/db_index_sqlite'
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'
import { parseNumberArray, toJsonString } from '@/src/db/sqlite-casts'
export class CouponDbService implements ICouponDbService {
async createCoupon(data: NewCoupon): Promise<Coupon> {
const normalized = {
...data,
productIds: data.productIds ? toJsonString(data.productIds, '[]') : data.productIds,
}
const [coupon] = await db.insert(coupons).values(normalized).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: parseNumberArray(result.productIds),
} 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: (couponsRef, { desc }) => [desc(couponsRef.createdAt)],
limit: limit + 1,
})
return result.map((coupon) => ({
...coupon,
productIds: parseNumberArray(coupon.productIds),
})) as CouponWithRelations[]
}
async updateCoupon(id: number, data: Partial<NewCoupon>): Promise<Coupon> {
const normalized = {
...data,
productIds: data.productIds ? toJsonString(data.productIds, '[]') : data.productIds,
}
const [coupon] = await db.update(coupons).set(normalized).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 normalized = {
...data,
productIds: data.productIds ? toJsonString(data.productIds, '[]') : data.productIds,
}
const [coupon] = await db.insert(reservedCoupons).values(normalized).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: (reservedCouponsRef, { desc }) => [desc(reservedCouponsRef.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: (usersRef, { asc }) => [asc(usersRef.name)],
})
}
async createUser(data: Partial<typeof users.$inferInsert>): Promise<typeof users.$inferSelect> {
const [user] = await db.insert(users).values(data).returning()
return user
}
async getUserByMobile(mobile: string): Promise<typeof users.$inferSelect | undefined> {
return db.query.users.findFirst({
where: eq(users.mobile, mobile),
})
}
async getOrderByIdWithUserAndStatus(id: number): Promise<typeof orders.$inferSelect & { user?: typeof users.$inferSelect; orderStatus?: any[] } | undefined> {
return db.query.orders.findFirst({
where: eq(orders.id, id),
with: {
user: true,
orderStatus: true,
},
})
}
async updateOrderStatusRefundCoupon(orderId: number, couponId: number): Promise<void> {
await db.update(orderStatus)
.set({ refundCouponId: couponId })
.where(eq(orderStatus.orderId, orderId))
}
async withTransaction<T>(fn: (tx: any) => Promise<T>): Promise<T> {
return db.transaction(fn)
}
}
export const couponDbService: ICouponDbService = new CouponDbService()

View file

@ -1,332 +0,0 @@
import { db } from '@/src/db/db_index_sqlite'
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[]> {
const conditions: Array<SQL> = [
gte(orders.createdAt, start),
lt(orders.createdAt, end),
]
if (slotId !== undefined) {
conditions.push(eq(orders.slotId, slotId))
}
return db.query.orders.findMany({
where: and(...conditions),
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
const conditions: Array<SQL> = []
if (cursor) {
conditions.push(lt(orders.id, cursor))
}
if (slotId !== undefined && slotId !== null) {
conditions.push(eq(orders.slotId, slotId))
}
if (packagedFilter === 'packaged') {
conditions.push(eq(orderStatus.isPackaged, true))
} else if (packagedFilter === 'not_packaged') {
conditions.push(eq(orderStatus.isPackaged, false))
}
if (deliveredFilter === 'delivered') {
conditions.push(eq(orderStatus.isDelivered, true))
} else if (deliveredFilter === 'not_delivered') {
conditions.push(eq(orderStatus.isDelivered, false))
}
if (cancellationFilter === 'cancelled') {
conditions.push(eq(orderStatus.isCancelled, true))
} else if (cancellationFilter === 'not_cancelled') {
conditions.push(eq(orderStatus.isCancelled, false))
}
if (flashDeliveryFilter === 'flash') {
conditions.push(eq(orders.isFlashDelivery, true))
} else if (flashDeliveryFilter === 'regular') {
conditions.push(eq(orders.isFlashDelivery, false))
}
const whereCondition = conditions.length > 0 ? and(...conditions) : undefined
return db.query.orders.findMany({
where: whereCondition,
with: {
user: true,
address: true,
slot: true,
orderItems: {
with: {
product: { with: { unit: true } },
},
},
orderStatus: true,
},
orderBy: (ordersRef, { desc }) => [desc(ordersRef.createdAt)],
limit: limit + 1,
}) 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 order of data) {
await tx.update(orders)
.set({ totalAmount: order.totalAmount })
.where(eq(orders.id, order.orderId))
for (const item of order.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(couponUsage).where(eq(couponUsage.orderId, orderId))
await tx.delete(orderStatus).where(eq(orderStatus.orderId, orderId))
await tx.delete(orderItems).where(eq(orderItems.orderId, orderId))
await tx.delete(refunds).where(eq(refunds.orderId, orderId))
await tx.delete(payments).where(eq(payments.orderId, orderId))
await tx.delete(complaints).where(eq(complaints.orderId, orderId))
await tx.delete(orders).where(eq(orders.id, orderId))
})
}
}
export const orderDbService: IOrderDbService = new OrderDbService()

View file

@ -1,244 +0,0 @@
import { db } from '@/src/db/db_index_sqlite'
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'
import { toJsonString } from '@/src/db/sqlite-casts'
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 normalized = {
...data,
images: data.images ? toJsonString(data.images, '[]') : data.images,
}
const [product] = await db.insert(productInfo).values(normalized).returning()
return product
}
async updateProduct(id: number, data: Partial<NewProduct>): Promise<Product> {
const normalized = {
...data,
images: data.images ? toJsonString(data.images, '[]') : data.images,
}
const [product] = await db
.update(productInfo)
.set(normalized)
.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 normalized = {
...data,
imageUrls: data.imageUrls ? toJsonString(data.imageUrls, '[]') : data.imageUrls,
adminResponseImages: data.adminResponseImages ? toJsonString(data.adminResponseImages, '[]') : data.adminResponseImages,
}
const [review] = await db
.update(productReviews)
.set(normalized)
.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 => {
const normalized = {
...update.data,
images: update.data.images ? toJsonString(update.data.images, '[]') : update.data.images,
}
return db
.update(productInfo)
.set(normalized)
.where(eq(productInfo.id, update.productId))
})
await Promise.all(promises)
}
}
export const productDbService: IProductDbService = new ProductDbService()

View file

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

View file

@ -1,60 +0,0 @@
import { db } from '@/src/db/db_index_sqlite'
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'
import { toJsonString } from '@/src/db/sqlite-casts'
export class ScheduleDbService implements IScheduleDbService {
async createSchedule(data: NewSchedule): Promise<Schedule> {
const normalized = {
...data,
productIds: data.productIds ? toJsonString(data.productIds, '[]') : data.productIds,
groupIds: data.groupIds ? toJsonString(data.groupIds, '[]') : data.groupIds,
}
const [schedule] = await db.insert(productAvailabilitySchedules).values(normalized).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 normalized = {
...data,
productIds: data.productIds ? toJsonString(data.productIds, '[]') : data.productIds,
groupIds: data.groupIds ? toJsonString(data.groupIds, '[]') : data.groupIds,
lastUpdated: new Date(),
}
const [schedule] = await db
.update(productAvailabilitySchedules)
.set(normalized)
.where(eq(productAvailabilitySchedules.id, id))
.returning()
return schedule
}
async deleteSchedule(id: number): Promise<Schedule> {
const [schedule] = await db
.delete(productAvailabilitySchedules)
.where(eq(productAvailabilitySchedules.id, id))
.returning()
return schedule
}
}
export const scheduleDbService: IScheduleDbService = new ScheduleDbService()

View file

@ -1,161 +0,0 @@
import { db } from '@/src/db/db_index_sqlite'
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'
import { parseNumberArray, toJsonString } from '@/src/db/sqlite-casts'
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 normalized = {
...data,
deliverySequence: data.deliverySequence ? toJsonString(data.deliverySequence, '{}') : data.deliverySequence,
groupIds: data.groupIds ? toJsonString(data.groupIds, '[]') : data.groupIds,
}
const [slot] = await db.insert(deliverySlotInfo).values(normalized).returning()
return slot
}
async updateSlot(id: number, data: Partial<NewSlot>): Promise<Slot> {
const normalized = {
...data,
deliverySequence: data.deliverySequence ? toJsonString(data.deliverySequence, '{}') : data.deliverySequence,
groupIds: data.groupIds ? toJsonString(data.groupIds, '[]') : data.groupIds,
}
const [slot] = await db
.update(deliverySlotInfo)
.set(normalized)
.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 }>> {
const snippets = await db.query.vendorSnippets.findMany({
where: eq(vendorSnippets.slotId, slotId),
})
return snippets.map((snippet) => ({
...snippet,
productIds: parseNumberArray(snippet.productIds),
})) as Array<{ id: number; snippetCode: string; slotId: number | null; productIds: number[]; validTill: Date | null; createdAt: Date; isPermanent: boolean | null }>
}
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: toJsonString(data.productIds, '[]'),
validTill: data.validTill || null,
}).returning()
return {
...snippet,
productIds: parseNumberArray(snippet.productIds),
}
}
async checkSnippetCodeExists(code: string): Promise<boolean> {
const existing = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.snippetCode, code),
})
return !!existing
}
async validateProductsExist(productIds: number[]): Promise<boolean> {
const products = await db.query.productInfo.findMany({
where: inArray(productInfo.id, productIds),
})
return products.length === productIds.length
}
async getProductsByIds(productIds: number[]): Promise<typeof productInfo.$inferSelect[]> {
return db.query.productInfo.findMany({
where: inArray(productInfo.id, productIds),
})
}
async getGroupsByIds(groupIds: number[]): Promise<Array<{ id: number; groupName: string; description: string | null; createdAt: Date }>> {
return db.query.productGroupInfo.findMany({
where: inArray(productGroupInfo.id, groupIds),
})
}
async withTransaction<T>(fn: (tx: any) => Promise<T>): Promise<T> {
return db.transaction(fn)
}
}
export const slotDbService: ISlotDbService = new SlotDbService()

View file

@ -1,104 +0,0 @@
import { db } from '@/src/db/db_index_sqlite'
import { staffUsers, staffRoles, users, userDetails, orders } from '@/src/db/schema'
import { eq, or, like, and, lt, desc } from 'drizzle-orm'
import { IStaffUserDbService, StaffUser, NewStaffUser, StaffRole, StaffUserWithRole } 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<StaffUserWithRole[]> {
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<Array<{ id: number; roleName: string }>> {
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(
like(users.name, `%${search}%`),
like(users.email, `%${search}%`),
like(users.mobile, `%${search}%`)
)
}
if (cursor) {
const cursorCondition = lt(users.id, cursor)
whereCondition = whereCondition ? and(whereCondition, cursorCondition) : cursorCondition
}
return db.query.users.findMany({
where: whereCondition,
with: { userDetails: true },
orderBy: desc(users.id),
limit: limit + 1,
})
}
async getUserById(id: number): Promise<typeof users.$inferSelect | undefined> {
return db.query.users.findFirst({
where: eq(users.id, id),
with: {
userDetails: true,
orders: {
orderBy: desc(orders.createdAt),
limit: 1,
},
},
})
}
async upsertUserDetails(data: Partial<typeof userDetails.$inferInsert> & { userId: number }): Promise<void> {
await db
.insert(userDetails)
.values(data)
.onConflictDoUpdate({
target: userDetails.userId,
set: data,
})
}
async getLastOrderByUserId(userId: number): Promise<typeof orders.$inferSelect | undefined> {
const userOrders = await db.query.orders.findMany({
where: eq(orders.userId, userId),
orderBy: desc(orders.createdAt),
limit: 1,
})
return userOrders[0]
}
}
export const staffUserDbService: IStaffUserDbService = new StaffUserDbService()

View file

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

View file

@ -1,51 +0,0 @@
import { db } from '@/src/db/db_index_sqlite'
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'
import { toJsonString } from '@/src/db/sqlite-casts'
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 normalized = {
...data,
relatedStores: data.relatedStores ? toJsonString(data.relatedStores, '[]') : data.relatedStores,
}
const [tag] = await db.insert(productTagInfo).values(normalized).returning()
return tag
}
async updateTag(id: number, data: Partial<NewTag>): Promise<Tag> {
const normalized = {
...data,
relatedStores: data.relatedStores ? toJsonString(data.relatedStores, '[]') : data.relatedStores,
}
const [tag] = await db
.update(productTagInfo)
.set(normalized)
.where(eq(productTagInfo.id, id))
.returning()
return tag
}
async deleteTag(id: number): Promise<void> {
await db.delete(productTagInfo).where(eq(productTagInfo.id, id))
}
}
export const tagDbService: ITagDbService = new TagDbService()

View file

@ -1,172 +0,0 @@
import { db } from '@/src/db/db_index_sqlite'
import { users, userDetails, orders, orderItems, orderStatus, complaints, notifCreds, unloggedUserTokens, userIncidents } from '@/src/db/schema'
import { eq, desc, asc, count, max, inArray, and, like, gt } 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 conditions = []
if (search && search.trim()) {
conditions.push(like(users.mobile, `%${search.trim()}%`))
}
if (cursor) {
conditions.push(gt(users.id, cursor))
}
const whereCondition = conditions.length > 0 ? and(...conditions) : undefined
return db
.select()
.from(users)
.where(whereCondition)
.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(inArray(orders.userId, userIds))
.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(inArray(orders.userId, userIds))
.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(inArray(orderStatus.orderId, orderIds))
}
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(inArray(orderItems.orderId, orderIds))
.groupBy(orderItems.orderId)
}
async getUnresolvedComplaintCount(): Promise<number> {
return db.$count(complaints, eq(complaints.isResolved, false))
}
async getAllNotifTokens(): Promise<string[]> {
const tokens = await db.select({ token: notifCreds.token }).from(notifCreds)
return tokens.map(t => t.token)
}
async getNotifTokensByUserIds(userIds: number[]): Promise<string[]> {
const tokens = await db
.select({ token: notifCreds.token })
.from(notifCreds)
.where(inArray(notifCreds.userId, userIds))
return tokens.map(t => t.token)
}
async getUnloggedTokens(): Promise<string[]> {
const tokens = await db.select({ token: unloggedUserTokens.token }).from(unloggedUserTokens)
return tokens.map(t => t.token)
}
async getUserIncidentsByUserId(userId: number): Promise<Array<typeof userIncidents.$inferSelect & { order?: { orderStatus: Array<{ isCancelled: boolean }> } | null; addedBy?: { name: string | null } | null }>> {
return db.query.userIncidents.findMany({
where: eq(userIncidents.userId, userId),
with: {
order: {
with: {
orderStatus: true,
},
},
addedBy: true,
},
orderBy: desc(userIncidents.dateAdded),
})
}
async createUserIncident(data: { userId: number; orderId?: number | null; adminComment?: string | null; addedBy: number; negativityScore?: number | null }): Promise<typeof userIncidents.$inferSelect> {
const [incident] = await db.insert(userIncidents).values(data).returning()
return incident
}
}
export const userDbService: IUserDbService = new UserDbService()

View file

@ -1,141 +0,0 @@
import { db } from '@/src/db/db_index_sqlite'
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'
import { toJsonString } from '@/src/db/sqlite-casts'
export class VendorSnippetDbService implements IVendorSnippetDbService {
async createSnippet(data: NewVendorSnippet): Promise<VendorSnippet> {
const normalized = {
...data,
productIds: data.productIds ? toJsonString(data.productIds, '[]') : data.productIds,
}
const [snippet] = await db.insert(vendorSnippets).values(normalized).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 normalized = {
...data,
productIds: data.productIds ? toJsonString(data.productIds, '[]') : data.productIds,
}
const [snippet] = await db
.update(vendorSnippets)
.set(normalized)
.where(eq(vendorSnippets.id, id))
.returning()
return snippet
}
async deleteSnippet(id: number): Promise<VendorSnippet> {
const [snippet] = await db
.delete(vendorSnippets)
.where(eq(vendorSnippets.id, id))
.returning()
return snippet
}
async checkSnippetCodeExists(code: string): Promise<boolean> {
const existing = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.snippetCode, code),
})
return !!existing
}
async getSlotById(id: number): Promise<typeof deliverySlotInfo.$inferSelect | undefined> {
return db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, id),
})
}
async getUpcomingSlots(since: Date): Promise<typeof deliverySlotInfo.$inferSelect[]> {
return db.query.deliverySlotInfo.findMany({
where: and(
eq(deliverySlotInfo.isActive, true),
gt(deliverySlotInfo.deliveryTime, since)
),
orderBy: asc(deliverySlotInfo.deliveryTime),
})
}
async getProductsByIds(ids: number[]): Promise<Array<{ id: number; name: string }>> {
return db.query.productInfo.findMany({
where: inArray(productInfo.id, ids),
columns: { id: true, name: true },
})
}
async validateProductsExist(ids: number[]): Promise<boolean> {
const products = await db.query.productInfo.findMany({
where: inArray(productInfo.id, ids),
})
return products.length === ids.length
}
async getOrdersBySlotId(slotId: number): Promise<typeof orders.$inferSelect[]> {
return db.query.orders.findMany({
where: eq(orders.slotId, slotId),
with: {
orderItems: {
with: {
product: {
with: { unit: true },
},
},
},
orderStatus: true,
user: true,
slot: true,
},
orderBy: desc(orders.createdAt),
})
}
async getOrderItemsByOrderIds(orderIds: number[]): Promise<typeof orderItems.$inferSelect[]> {
return db.query.orderItems.findMany({
where: inArray(orderItems.orderId, orderIds),
})
}
async getOrderItemById(id: number): Promise<typeof orderItems.$inferSelect | undefined> {
return db.query.orderItems.findFirst({
where: eq(orderItems.id, id),
})
}
async updateOrderItemPackaging(id: number, is_packaged: boolean): Promise<void> {
await db
.update(orderItems)
.set({ is_packaged })
.where(eq(orderItems.id, id))
}
async hasSnippetForSlot(slotId: number): Promise<boolean> {
const snippet = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.slotId, slotId),
})
return !!snippet
}
}
export const vendorSnippetDbService: IVendorSnippetDbService = new VendorSnippetDbService()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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