Compare commits

..

28 commits

Author SHA1 Message Date
shafi54
9137b5e1e6 enh 2026-03-26 00:49:47 +05:30
shafi54
fe05769343 enh 2026-03-26 00:34:31 +05:30
shafi54
4414f9f64b enh 2026-03-25 19:58:40 +05:30
shafi54
3c836e274d enh 2026-03-25 19:30:01 +05:30
shafi54
306244e8df enh 2026-03-25 18:11:46 +05:30
shafi54
038733c14a enh 2026-03-25 09:39:53 +05:30
shafi54
d9652405ca enh 2026-03-25 01:43:02 +05:30
shafi54
97812fa4c5 enh 2026-03-24 20:50:14 +05:30
shafi54
44e53d2978 enh 2026-03-16 22:15:47 +05:30
shafi54
a5bde12f19 enh 2026-03-16 21:18:14 +05:30
shafi54
31029cc3a7 enh 2026-03-16 21:15:07 +05:30
shafi54
a4758ea9cd enh 2026-03-16 21:14:23 +05:30
shafi54
0c84808637 enh 2026-03-16 19:55:06 +05:30
shafi54
f2763b0597 enh 2026-03-16 18:20:40 +05:30
shafi54
8f48ec39c2 enh 2026-03-16 18:10:28 +05:30
shafi54
5d598b0752 enh 2026-03-15 23:23:44 +05:30
shafi54
4aab508286 enh 2026-03-15 23:23:33 +05:30
shafi54
ad2447d14e enh 2026-03-15 22:10:52 +05:30
shafi54
b4caa383b5 enh 2026-03-15 21:26:00 +05:30
shafi54
a7350914e0 enh 2026-03-15 21:11:54 +05:30
76c43d869d Merge pull request 'enh' (#3) from main into api_cache
Reviewed-on: #3
2026-03-14 12:31:48 +00:00
shafi54
2d37726c62 enh 2026-03-14 17:25:41 +05:30
shafi54
5df040de9a enh 2026-03-12 19:26:21 +05:30
shafi54
ca9eb8a7d2 enh 2026-03-11 16:31:23 +05:30
shafi54
aa900db3e1 enh 2026-03-10 14:20:21 +05:30
shafi54
f7c55ea492 enh 2026-03-10 14:20:14 +05:30
shafi54
c14e32522a enh 2026-03-10 13:05:33 +05:30
shafi54
a4218ee1ad enh 2026-03-10 10:03:49 +05:30
331 changed files with 214679 additions and 33271 deletions

8
.dockerignore Normal file
View file

@ -0,0 +1,8 @@
**/node_modules
**/dist
apps/users-ui/app
apps/users-ui/src
apps/admin-ui/app
apps/users-ui/src
**/package-lock.json

4
APIS_TO_REMOVE.md Normal file
View file

@ -0,0 +1,4 @@
- trpc.user.tags.getTagsByStore — apps/backend/src/trpc/apis/user-apis/apis/tags.ts
- trpc.common.product.getAllProductsSummary — apps/backend/src/trpc/apis/common-apis/common.ts
- remove slots from products cache
- remove redundant product details like name, description etc from the slots api

View file

@ -1,32 +1,36 @@
# Optimized Dockerfile for backend and fallback-ui services (project root) # Optimized Dockerfile for backend and fallback-ui services (project root)
# 1. ---- Base Node image # 1. ---- Base Bun image
FROM node:20-slim AS base FROM oven/bun:1.3.10 AS base
WORKDIR /app WORKDIR /app
# 2. ---- Pruner ---- # 2. ---- Pruner ----
FROM base AS pruner FROM base AS pruner
WORKDIR /app WORKDIR /app
# Copy config files first for better caching # Copy config files first for better caching
COPY package.json package-lock.json turbo.json ./ COPY package.json turbo.json ./
COPY apps/backend/package.json ./apps/backend/ COPY apps/backend/package.json ./apps/backend/
COPY apps/fallback-ui/package.json ./apps/fallback-ui/ COPY apps/fallback-ui/package.json ./apps/fallback-ui/
COPY packages/shared/ ./packages/shared
COPY packages/ui/package.json ./packages/ui/ COPY packages/ui/package.json ./packages/ui/
RUN npm install -g turbo RUN bun install -g turbo
COPY . . COPY . .
RUN turbo prune --scope=backend --scope=fallback-ui --scope=common-ui --docker RUN turbo prune --scope=backend --scope=fallback-ui --scope=@packages/shared --docker
# RUN find . -path "./node_modules" -prune -o -print
# 3. ---- Builder ---- # 3. ---- Builder ----
FROM base AS builder FROM base AS builder
WORKDIR /app WORKDIR /app
# Copy package files first to cache npm install # Copy package files first to cache bun install
COPY --from=pruner /app/out/json/ . COPY --from=pruner /app/out/json/ .
COPY --from=pruner /app/out/package-lock.json ./package-lock.json #COPY --from=pruner /app/out/bun.lock ./bun.lock
#RUN cat ./bun.lock
COPY --from=pruner /app/turbo.json . COPY --from=pruner /app/turbo.json .
RUN npm ci RUN bun install
# Copy source code after dependencies are installed # Copy source code after dependencies are installed
COPY --from=pruner /app/out/full/ . COPY --from=pruner /app/out/full/ .
RUN npx turbo run build --filter=fallback-ui... --filter=backend... RUN bunx turbo run build --filter=fallback-ui... --filter=backend...
RUN find . -path "./node_modules" -prune -o -print
# 4. ---- Runner ---- # 4. ---- Runner ----
FROM base AS runner FROM base AS runner
@ -34,12 +38,15 @@ WORKDIR /app
ENV NODE_ENV=production ENV NODE_ENV=production
# Copy package files and install production deps # Copy package files and install production deps
COPY --from=pruner /app/out/json/ . COPY --from=pruner /app/out/json/ .
COPY --from=pruner /app/out/package-lock.json ./package-lock.json #COPY --from=pruner /app/out/bun.lock ./bun.lock
RUN npm ci --production --omit=dev RUN bun install --production
# Copy built applications # Copy built applications
COPY --from=builder /app/apps/backend/dist ./apps/backend/dist COPY --from=builder /app/apps/backend/dist ./apps/backend/dist
COPY --from=builder /app/apps/fallback-ui/dist ./apps/fallback-ui/dist COPY --from=builder /app/apps/fallback-ui/dist ./apps/fallback-ui/dist
COPY --from=builder /app/packages/shared ./packages/shared
# RUN ls -R
RUN find . -path "./node_modules" -prune -o -print
EXPOSE 4000 EXPOSE 4000
RUN npm i -g bun CMD ["bun", "apps/backend/dist/apps/backend/index.js"]
CMD ["bun", "apps/backend/dist/index.js"]
# CMD ["node", "apps/backend/dist/index.js"]

File diff suppressed because one or more lines are too long

View file

@ -226,9 +226,8 @@ export default function Layout() {
<Drawer.Screen name="coupons" options={{ title: "Coupons" }} /> <Drawer.Screen name="coupons" options={{ title: "Coupons" }} />
<Drawer.Screen name="slots" options={{ title: "Slots" }} /> <Drawer.Screen name="slots" options={{ title: "Slots" }} />
<Drawer.Screen name="vendor-snippets" options={{ title: "Vendor Snippets" }} /> <Drawer.Screen name="vendor-snippets" options={{ title: "Vendor Snippets" }} />
<Drawer.Screen name="stores" options={{ title: "Stores" }} /> <Drawer.Screen name="stores" options={{ title: "Stores" }} />
<Drawer.Screen name="address-management" options={{ title: "Address Management" }} /> <Drawer.Screen name="product-tags" options={{ title: "Product Tags" }} />
<Drawer.Screen name="product-tags" options={{ title: "Product Tags" }} />
<Drawer.Screen name="rebalance-orders" options={{ title: "Rebalance Orders" }} /> <Drawer.Screen name="rebalance-orders" options={{ title: "Rebalance Orders" }} />
<Drawer.Screen name="user-management" options={{ title: "User Management" }} /> <Drawer.Screen name="user-management" options={{ title: "User Management" }} />
<Drawer.Screen name="send-notifications" options={{ title: "Send Notifications" }} /> <Drawer.Screen name="send-notifications" options={{ title: "Send Notifications" }} />

View file

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

View file

@ -74,7 +74,7 @@ export default function Dashboard() {
const menuItems: MenuItem[] = [ const menuItems: MenuItem[] = [
{ {
title: 'Manage Orders', title: 'Manage Orderss',
icon: 'shopping-bag', icon: 'shopping-bag',
description: 'View and manage customer orders', description: 'View and manage customer orders',
route: '/(drawer)/manage-orders', route: '/(drawer)/manage-orders',
@ -176,15 +176,6 @@ export default function Dashboard() {
iconColor: '#F97316', iconColor: '#F97316',
iconBg: '#FFEDD5', iconBg: '#FFEDD5',
}, },
{
title: 'Address Management',
icon: 'location-on',
description: 'Manage service areas',
route: '/(drawer)/address-management',
category: 'settings',
iconColor: '#EAB308',
iconBg: '#FEF9C3',
},
{ {
title: 'App Constants', title: 'App Constants',
icon: 'settings-applications', icon: 'settings-applications',

View file

@ -63,7 +63,8 @@ export default function OrderDetails() {
onSuccess: (result) => { onSuccess: (result) => {
Alert.alert( Alert.alert(
"Success", "Success",
`Refund initiated successfully!\n\nAmount: ₹${result.amount}\nStatus: ${result.status}` `Refund initiated successfully!\n\nAmount: `
// `Refund initiated successfully!\n\nAmount: ₹${result.amount}\nStatus: ${result.status}`
); );
setInitiateRefundDialogOpen(false); setInitiateRefundDialogOpen(false);
}, },

View file

@ -1,197 +0,0 @@
import React from 'react';
import { View, ScrollView, Dimensions } from 'react-native';
import { Image } from 'expo-image';
import { MyText, tw } from 'common-ui';
import { trpc } from '../src/trpc-client';
interface FullOrderViewProps {
orderId: number;
}
export const FullOrderView: React.FC<FullOrderViewProps> = ({ orderId }) => {
const { data: order, isLoading, error } = trpc.admin.order.getFullOrder.useQuery({ orderId });
if (isLoading) {
return (
<View style={tw`p-6`}>
<MyText style={tw`text-center text-gray-600`}>Loading order details...</MyText>
</View>
);
}
if (error || !order) {
return (
<View style={tw`p-6`}>
<MyText style={tw`text-center text-red-600`}>Failed to load order details</MyText>
</View>
);
}
const totalAmount = order.items.reduce((sum, item) => sum + item.amount, 0);
return (
<ScrollView
style={[tw`flex-1`, { maxHeight: Dimensions.get('window').height * 0.8 }]}
showsVerticalScrollIndicator={false}
>
<View style={tw`p-6`}>
<MyText style={tw`text-2xl font-bold text-gray-800 mb-6`}>Order #{order.readableId}</MyText>
{/* Customer Information */}
<View style={tw`bg-white rounded-xl p-4 mb-4 shadow-sm`}>
<MyText style={tw`text-lg font-semibold text-gray-800 mb-3`}>Customer Details</MyText>
<View style={tw`space-y-2`}>
<View style={tw`flex-row justify-between`}>
<MyText style={tw`text-gray-600`}>Name:</MyText>
<MyText style={tw`font-medium`}>{order.customerName}</MyText>
</View>
{order.customerEmail && (
<View style={tw`flex-row justify-between`}>
<MyText style={tw`text-gray-600`}>Email:</MyText>
<MyText style={tw`font-medium`}>{order.customerEmail}</MyText>
</View>
)}
<View style={tw`flex-row justify-between`}>
<MyText style={tw`text-gray-600`}>Mobile:</MyText>
<MyText style={tw`font-medium`}>{order.customerMobile}</MyText>
</View>
</View>
</View>
{/* Delivery Address */}
<View style={tw`bg-white rounded-xl p-4 mb-4 shadow-sm`}>
<MyText style={tw`text-lg font-semibold text-gray-800 mb-3`}>Delivery Address</MyText>
<View style={tw`space-y-1`}>
<MyText style={tw`text-gray-800`}>{order.address.line1}</MyText>
{order.address.line2 && <MyText style={tw`text-gray-800`}>{order.address.line2}</MyText>}
<MyText style={tw`text-gray-800`}>
{order.address.city}, {order.address.state} - {order.address.pincode}
</MyText>
<MyText style={tw`text-gray-800`}>Phone: {order.address.phone}</MyText>
</View>
</View>
{/* Order Details */}
<View style={tw`bg-white rounded-xl p-4 mb-4 shadow-sm`}>
<MyText style={tw`text-lg font-semibold text-gray-800 mb-3`}>Order Details</MyText>
<View style={tw`space-y-2`}>
<View style={tw`flex-row justify-between`}>
<MyText style={tw`text-gray-600`}>Order Date:</MyText>
<MyText style={tw`font-medium`}>
{new Date(order.createdAt).toLocaleDateString('en-IN', {
day: 'numeric',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
</MyText>
</View>
<View style={tw`flex-row justify-between`}>
<MyText style={tw`text-gray-600`}>Payment Method:</MyText>
<MyText style={tw`font-medium`}>
{order.isCod ? 'Cash on Delivery' : 'Online Payment'}
</MyText>
</View>
{order.slotInfo && (
<View style={tw`flex-row justify-between`}>
<MyText style={tw`text-gray-600`}>Delivery Slot:</MyText>
<MyText style={tw`font-medium`}>
{new Date(order.slotInfo.time).toLocaleDateString('en-IN', {
day: 'numeric',
month: 'short',
hour: '2-digit',
minute: '2-digit'
})}
</MyText>
</View>
)}
</View>
</View>
{/* Items */}
<View style={tw`bg-white rounded-xl p-4 mb-4 shadow-sm`}>
<MyText style={tw`text-lg font-semibold text-gray-800 mb-3`}>Items ({order.items.length})</MyText>
{order.items.map((item, index) => (
<View key={item.id} style={tw`flex-row items-center py-3 ${index !== order.items.length - 1 ? 'border-b border-gray-100' : ''}`}>
<View style={tw`flex-1`}>
<MyText style={tw`font-medium text-gray-800`} numberOfLines={2}>
{item.productName}
</MyText>
<MyText style={tw`text-sm text-gray-600`}>
Qty: {item.quantity} {item.unit} × {parseFloat(item.price.toString()).toFixed(2)}
</MyText>
</View>
<MyText style={tw`font-semibold text-gray-800`}>{item.amount.toFixed(2)}</MyText>
</View>
))}
</View>
{/* Payment Information */}
{(order.payment || order.paymentInfo) && (
<View style={tw`bg-white rounded-xl p-4 mb-4 shadow-sm`}>
<MyText style={tw`text-lg font-semibold text-gray-800 mb-3`}>Payment Information</MyText>
{order.payment && (
<View style={tw`space-y-2 mb-3`}>
<MyText style={tw`text-sm font-medium text-gray-700`}>Payment Details:</MyText>
<View style={tw`flex-row justify-between`}>
<MyText style={tw`text-gray-600`}>Status:</MyText>
<MyText style={tw`font-medium capitalize`}>{order.payment.status}</MyText>
</View>
<View style={tw`flex-row justify-between`}>
<MyText style={tw`text-gray-600`}>Gateway:</MyText>
<MyText style={tw`font-medium`}>{order.payment.gateway}</MyText>
</View>
<View style={tw`flex-row justify-between`}>
<MyText style={tw`text-gray-600`}>Order ID:</MyText>
<MyText style={tw`font-medium`}>{order.payment.merchantOrderId}</MyText>
</View>
</View>
)}
{order.paymentInfo && (
<View style={tw`space-y-2`}>
<MyText style={tw`text-sm font-medium text-gray-700`}>Payment Info:</MyText>
<View style={tw`flex-row justify-between`}>
<MyText style={tw`text-gray-600`}>Status:</MyText>
<MyText style={tw`font-medium capitalize`}>{order.paymentInfo.status}</MyText>
</View>
<View style={tw`flex-row justify-between`}>
<MyText style={tw`text-gray-600`}>Gateway:</MyText>
<MyText style={tw`font-medium`}>{order.paymentInfo.gateway}</MyText>
</View>
<View style={tw`flex-row justify-between`}>
<MyText style={tw`text-gray-600`}>Order ID:</MyText>
<MyText style={tw`font-medium`}>{order.paymentInfo.merchantOrderId}</MyText>
</View>
</View>
)}
</View>
)}
{/* User Notes */}
{order.userNotes && (
<View style={tw`bg-blue-50 rounded-xl p-4 mb-4`}>
<MyText style={tw`text-lg font-semibold text-gray-800 mb-2`}>Customer Notes</MyText>
<MyText style={tw`text-gray-700`}>{order.userNotes}</MyText>
</View>
)}
{/* Admin Notes */}
{order.adminNotes && (
<View style={tw`bg-yellow-50 rounded-xl p-4 mb-4`}>
<MyText style={tw`text-lg font-semibold text-gray-800 mb-2`}>Admin Notes</MyText>
<MyText style={tw`text-gray-700`}>{order.adminNotes}</MyText>
</View>
)}
{/* Total */}
<View style={tw`bg-blue-50 rounded-xl p-4`}>
<View style={tw`flex-row justify-between items-center`}>
<MyText style={tw`text-xl font-bold text-gray-800`}>Total Amount</MyText>
<MyText style={tw`text-2xl font-bold text-blue-600`}>{parseFloat(order.totalAmount.toString()).toFixed(2)}</MyText>
</View>
</View>
</View>
</ScrollView>
);
};

Binary file not shown.

View file

@ -1,6 +1,6 @@
ENV_MODE=PROD ENV_MODE=PROD
# DATABASE_URL=postgresql://postgres:meatfarmer_master_password@57.128.212.174:7447/meatfarmer #technocracy DATABASE_URL=postgresql://postgres:meatfarmer_master_password@57.128.212.174:7447/meatfarmer #technocracy
DATABASE_URL=postgres://postgres:meatfarmer_master_password@5.223.55.14:7447/meatfarmer #hetzner # DATABASE_URL=postgres://postgres:meatfarmer_master_password@5.223.55.14:7447/meatfarmer #hetzner
PHONE_PE_BASE_URL=https://api-preprod.phonepe.com/ PHONE_PE_BASE_URL=https://api-preprod.phonepe.com/
PHONE_PE_CLIENT_ID=TEST-M23F2IGP34ZAR_25090 PHONE_PE_CLIENT_ID=TEST-M23F2IGP34ZAR_25090
PHONE_PE_CLIENT_VERSION=1 PHONE_PE_CLIENT_VERSION=1
@ -17,10 +17,14 @@ S3_REGION=apac
S3_ACCESS_KEY_ID=8fab47503efb9547b50e4fb317e35cc7 S3_ACCESS_KEY_ID=8fab47503efb9547b50e4fb317e35cc7
S3_SECRET_ACCESS_KEY=47c2eb5636843cf568dda7ad0959a3e42071303f26dbdff94bd45a3c33dcd950 S3_SECRET_ACCESS_KEY=47c2eb5636843cf568dda7ad0959a3e42071303f26dbdff94bd45a3c33dcd950
S3_URL=https://da9b1aa7c1951c23e2c0c3246ba68a58.r2.cloudflarestorage.com S3_URL=https://da9b1aa7c1951c23e2c0c3246ba68a58.r2.cloudflarestorage.com
S3_BUCKET_NAME=meatfarmer S3_BUCKET_NAME=meatfarmer-dev
EXPO_ACCESS_TOKEN=Asvpy8cByRh6T4ksnWScO6PLcio2n35-BwES5zK- EXPO_ACCESS_TOKEN=Asvpy8cByRh6T4ksnWScO6PLcio2n35-BwES5zK-
JWT_SECRET=my_meatfarmer_jwt_secret_key JWT_SECRET=my_meatfarmer_jwt_secret_key
ASSETS_DOMAIN=https://assets.freshyo.in/ ASSETS_DOMAIN=https://assets2.freshyo.in/
API_CACHE_KEY=api-cache-dev
# CLOUDFLARE_API_TOKEN=I8Vp4E9TX58E8qEDeH0nTFDS2d2zXNYiXvbs4Ckj
CLOUDFLARE_API_TOKEN=N7jAg5X-RUj_fVfMW6zbfJ8qIYc81TSIKKlbZ6oh
CLOUDFLARE_ZONE_ID=edefbf750bfc3ff26ccd11e8e28dc8d7
# REDIS_URL=redis://default:redis_shafi_password@5.223.55.14:6379 # REDIS_URL=redis://default:redis_shafi_password@5.223.55.14:6379
REDIS_URL=redis://default:redis_shafi_password@57.128.212.174:6379 REDIS_URL=redis://default:redis_shafi_password@57.128.212.174:6379
APP_URL=http://localhost:4000 APP_URL=http://localhost:4000

File diff suppressed because one or more lines are too long

View file

@ -180,6 +180,6 @@ app.use((err: any, req: express.Request, res: express.Response, next: express.Ne
res.status(status).json({ message }); res.status(status).json({ message });
}); });
app.listen(4000, '::', () => { app.listen(4000, () => {
console.log("Server is running on http://localhost:4000/api/mobile/"); console.log("Server is running on http://localhost:4000/api/mobile/");
}); });

View file

@ -5,7 +5,8 @@ import { eq } from "drizzle-orm";
import { ApiError } from "@/src/lib/api-error"; import { ApiError } from "@/src/lib/api-error";
import { imageUploadS3, generateSignedUrlFromS3Url } from "@/src/lib/s3-client"; import { imageUploadS3, generateSignedUrlFromS3Url } from "@/src/lib/s3-client";
import { deleteS3Image } from "@/src/lib/delete-image"; import { deleteS3Image } from "@/src/lib/delete-image";
import { initializeAllStores } from '@/src/stores/store-initializer'; import { scheduleStoreInitialization } from '@/src/stores/store-initializer';
/** /**
* Create a new product tag * Create a new product tag
@ -58,9 +59,10 @@ export const createTag = async (req: Request, res: Response) => {
.returning(); .returning();
// Reinitialize stores to reflect changes in cache // Reinitialize stores to reflect changes in cache
await initializeAllStores(); scheduleStoreInitialization()
return res.status(201).json({ // Send response first
res.status(201).json({
tag: newTag, tag: newTag,
message: "Tag created successfully", message: "Tag created successfully",
}); });
@ -93,7 +95,7 @@ export const getAllTags = async (req: Request, res: Response) => {
* Get a single product tag by ID * Get a single product tag by ID
*/ */
export const getTagById = async (req: Request, res: Response) => { export const getTagById = async (req: Request, res: Response) => {
const { id } = req.params; const id = req.params.id as string
const tag = await db.query.productTagInfo.findFirst({ const tag = await db.query.productTagInfo.findFirst({
where: eq(productTagInfo.id, parseInt(id)), where: eq(productTagInfo.id, parseInt(id)),
@ -119,7 +121,7 @@ export const getTagById = async (req: Request, res: Response) => {
* Update a product tag * Update a product tag
*/ */
export const updateTag = async (req: Request, res: Response) => { export const updateTag = async (req: Request, res: Response) => {
const { id } = req.params; const id = req.params.id as string
const { tagName, tagDescription, isDashboardTag, relatedStores } = req.body; const { tagName, tagDescription, isDashboardTag, relatedStores } = req.body;
// Get the current tag to check for existing image // Get the current tag to check for existing image
@ -177,9 +179,10 @@ export const updateTag = async (req: Request, res: Response) => {
.returning(); .returning();
// Reinitialize stores to reflect changes in cache // Reinitialize stores to reflect changes in cache
await initializeAllStores(); scheduleStoreInitialization()
return res.status(200).json({ // Send response first
res.status(200).json({
tag: updatedTag, tag: updatedTag,
message: "Tag updated successfully", message: "Tag updated successfully",
}); });
@ -189,7 +192,7 @@ export const updateTag = async (req: Request, res: Response) => {
* Delete a product tag * Delete a product tag
*/ */
export const deleteTag = async (req: Request, res: Response) => { export const deleteTag = async (req: Request, res: Response) => {
const { id } = req.params; const id = req.params.id as string
// Check if tag exists // Check if tag exists
const tag = await db.query.productTagInfo.findFirst({ const tag = await db.query.productTagInfo.findFirst({
@ -214,9 +217,10 @@ export const deleteTag = async (req: Request, res: Response) => {
await db.delete(productTagInfo).where(eq(productTagInfo.id, parseInt(id))); await db.delete(productTagInfo).where(eq(productTagInfo.id, parseInt(id)));
// Reinitialize stores to reflect changes in cache // Reinitialize stores to reflect changes in cache
await initializeAllStores(); scheduleStoreInitialization()
return res.status(200).json({ // Send response first
res.status(200).json({
message: "Tag deleted successfully", message: "Tag deleted successfully",
}); });
}; };

View file

@ -6,7 +6,8 @@ import { ApiError } from "@/src/lib/api-error";
import { imageUploadS3, getOriginalUrlFromSignedUrl } from "@/src/lib/s3-client"; import { imageUploadS3, getOriginalUrlFromSignedUrl } from "@/src/lib/s3-client";
import { deleteS3Image } from "@/src/lib/delete-image"; import { deleteS3Image } from "@/src/lib/delete-image";
import type { SpecialDeal } from "@/src/db/types"; import type { SpecialDeal } from "@/src/db/types";
import { initializeAllStores } from '@/src/stores/store-initializer'; import { scheduleStoreInitialization } from '@/src/stores/store-initializer';
type CreateDeal = { type CreateDeal = {
quantity: number; quantity: number;
@ -108,9 +109,10 @@ export const createProduct = async (req: Request, res: Response) => {
} }
// Reinitialize stores to reflect changes // Reinitialize stores to reflect changes
await initializeAllStores(); scheduleStoreInitialization()
return res.status(201).json({ // Send response first
res.status(201).json({
product: newProduct, product: newProduct,
deals: createdDeals, deals: createdDeals,
message: "Product created successfully", message: "Product created successfully",
@ -121,7 +123,7 @@ export const createProduct = async (req: Request, res: Response) => {
* Update a product * Update a product
*/ */
export const updateProduct = async (req: Request, res: Response) => { export const updateProduct = async (req: Request, res: Response) => {
const { id } = req.params; const id = req.params.id as string
const { name, shortDescription, longDescription, unitId, storeId, price, marketPrice, incrementStep, productQuantity, isSuspended, isFlashAvailable, flashPrice, deals:dealsRaw, imagesToDelete:imagesToDeleteRaw, tagIds } = req.body; const { name, shortDescription, longDescription, unitId, storeId, price, marketPrice, incrementStep, productQuantity, isSuspended, isFlashAvailable, flashPrice, deals:dealsRaw, imagesToDelete:imagesToDeleteRaw, tagIds } = req.body;
@ -294,9 +296,10 @@ export const updateProduct = async (req: Request, res: Response) => {
} }
// Reinitialize stores to reflect changes // Reinitialize stores to reflect changes
await initializeAllStores(); scheduleStoreInitialization()
return res.status(200).json({ // Send response first
res.status(200).json({
product: updatedProduct, product: updatedProduct,
message: "Product updated successfully", message: "Product updated successfully",
}); });

View file

@ -0,0 +1,170 @@
// Database Service - Central export for all database-related imports
// This file re-exports everything from postgresImporter to provide a clean abstraction layer
import type { AdminOrderDetails } from '@packages/shared'
import { getOrderDetails } from '@/src/postgresImporter'
// Re-export everything from postgresImporter
export * from '@/src/postgresImporter'
// Re-export getOrderDetails with the correct signature
export async function getOrderDetailsWrapper(orderId: number): Promise<AdminOrderDetails | null> {
return getOrderDetails(orderId)
}
// Re-export all types from shared package
export type {
// Admin types
Banner,
Complaint,
ComplaintWithUser,
Constant,
ConstantUpdateResult,
Coupon,
CouponValidationResult,
UserMiniInfo,
Store,
StaffUser,
StaffRole,
AdminOrderRow,
AdminOrderDetails,
AdminOrderUpdateResult,
AdminOrderItemPackagingResult,
AdminOrderMessageResult,
AdminOrderBasicResult,
AdminGetSlotOrdersResult,
AdminGetAllOrdersResult,
AdminGetAllOrdersResultWithUserId,
AdminRebalanceSlotsResult,
AdminCancelOrderResult,
AdminUnit,
AdminProduct,
AdminProductWithRelations,
AdminProductWithDetails,
AdminProductTagInfo,
AdminProductTagWithProducts,
AdminProductListResponse,
AdminProductResponse,
AdminDeleteProductResult,
AdminToggleOutOfStockResult,
AdminUpdateSlotProductsResult,
AdminSlotProductIdsResult,
AdminSlotsProductIdsResult,
AdminProductReview,
AdminProductReviewWithSignedUrls,
AdminProductReviewsResult,
AdminProductReviewResponse,
AdminProductGroup,
AdminProductGroupsResult,
AdminProductGroupResponse,
AdminProductGroupInfo,
AdminUpdateProductPricesResult,
AdminDeliverySlot,
AdminSlotProductSummary,
AdminSlotWithProducts,
AdminSlotWithProductsAndSnippets,
AdminSlotWithProductsAndSnippetsBase,
AdminSlotsResult,
AdminSlotsListResult,
AdminSlotResult,
AdminSlotCreateResult,
AdminSlotUpdateResult,
AdminSlotDeleteResult,
AdminDeliverySequence,
AdminDeliverySequenceResult,
AdminUpdateDeliverySequenceResult,
AdminUpdateSlotCapacityResult,
AdminVendorSnippet,
AdminVendorSnippetWithAccess,
AdminVendorSnippetWithSlot,
AdminVendorSnippetProduct,
AdminVendorSnippetWithProducts,
AdminVendorSnippetCreateInput,
AdminVendorSnippetUpdateInput,
AdminVendorSnippetDeleteResult,
AdminVendorSnippetOrderProduct,
AdminVendorSnippetOrderSummary,
AdminVendorSnippetOrdersResult,
AdminVendorSnippetOrdersWithSlotResult,
AdminVendorOrderSummary,
AdminUpcomingSlotsResult,
AdminVendorUpdatePackagingResult,
UserAddress,
UserAddressResponse,
UserAddressesResponse,
UserAddressDeleteResponse,
UserBanner,
UserBannersResponse,
UserCartProduct,
UserCartItem,
UserCartResponse,
UserComplaint,
UserComplaintsResponse,
UserRaiseComplaintResponse,
UserStoreSummary,
UserStoreSummaryData,
UserStoresResponse,
UserStoreSampleProduct,
UserStoreSampleProductData,
UserStoreDetail,
UserStoreDetailData,
UserStoreProduct,
UserStoreProductData,
UserTagSummary,
UserProductDetail,
UserProductDetailData,
UserProductReview,
UserProductReviewWithSignedUrls,
UserProductReviewsResponse,
UserCreateReviewResponse,
UserSlotProduct,
UserSlotWithProducts,
UserSlotData,
UserSlotAvailability,
UserDeliverySlot,
UserSlotsResponse,
UserSlotsWithProductsResponse,
UserSlotsListResponse,
UserPaymentOrderResponse,
UserPaymentVerifyResponse,
UserPaymentFailResponse,
UserAuthProfile,
UserAuthResponse,
UserAuthResult,
UserOtpVerifyResponse,
UserPasswordUpdateResponse,
UserProfileResponse,
UserDeleteAccountResponse,
UserCouponUsage,
UserCouponApplicableUser,
UserCouponApplicableProduct,
UserCoupon,
UserCouponWithRelations,
UserEligibleCouponsResponse,
UserCouponDisplay,
UserMyCouponsResponse,
UserRedeemCouponResponse,
UserSelfDataResponse,
UserProfileCompleteResponse,
UserSavePushTokenResponse,
UserOrderItemSummary,
UserOrderSummary,
UserOrdersResponse,
UserOrderDetail,
UserCancelOrderResponse,
UserUpdateNotesResponse,
UserRecentProduct,
UserRecentProductsResponse,
} from '@packages/shared';
export type {
// User types
User,
UserDetails,
Address,
Product,
CartItem,
Order,
OrderItem,
Payment,
} from '@packages/shared';

View file

@ -2,7 +2,6 @@ import * as cron from 'node-cron';
import { db } from '@/src/db/db_index' import { db } from '@/src/db/db_index'
import { payments, orders, deliverySlotInfo, refunds } from '@/src/db/schema' import { payments, orders, deliverySlotInfo, refunds } from '@/src/db/schema'
import { eq, and, gt, isNotNull } from 'drizzle-orm'; import { eq, and, gt, isNotNull } from 'drizzle-orm';
import { RazorpayPaymentService } from '@/src/lib/payments-utils'
interface PendingPaymentRecord { interface PendingPaymentRecord {
payment: typeof payments.$inferSelect; payment: typeof payments.$inferSelect;
@ -20,34 +19,34 @@ export const createPaymentNotification = (record: PendingPaymentRecord) => {
export const checkRefundStatuses = async () => { export const checkRefundStatuses = async () => {
try { try {
const initiatedRefunds = await db // const initiatedRefunds = await db
.select() // .select()
.from(refunds) // .from(refunds)
.where(and( // .where(and(
eq(refunds.refundStatus, 'initiated'), // eq(refunds.refundStatus, 'initiated'),
isNotNull(refunds.merchantRefundId) // isNotNull(refunds.merchantRefundId)
)); // ));
//
// Process refunds concurrently using Promise.allSettled // // Process refunds concurrently using Promise.allSettled
const promises = initiatedRefunds.map(async (refund) => { // const promises = initiatedRefunds.map(async (refund) => {
if (!refund.merchantRefundId) return; // if (!refund.merchantRefundId) return;
//
try { // try {
const razorpayRefund = await RazorpayPaymentService.fetchRefund(refund.merchantRefundId); // const razorpayRefund = await RazorpayPaymentService.fetchRefund(refund.merchantRefundId);
//
if (razorpayRefund.status === 'processed') { // if (razorpayRefund.status === 'processed') {
await db // await db
.update(refunds) // .update(refunds)
.set({ refundStatus: 'success', refundProcessedAt: new Date() }) // .set({ refundStatus: 'success', refundProcessedAt: new Date() })
.where(eq(refunds.id, refund.id)); // .where(eq(refunds.id, refund.id));
} // }
} catch (error) { // } catch (error) {
console.error(`Error checking refund ${refund.id}:`, error); // console.error(`Error checking refund ${refund.id}:`, error);
} // }
}); // });
//
// Wait for all promises to complete // // Wait for all promises to complete
await Promise.allSettled(promises); // await Promise.allSettled(promises);
} catch (error) { } catch (error) {
console.error('Error in checkRefundStatuses:', error); console.error('Error in checkRefundStatuses:', error);
} }

View file

@ -0,0 +1,376 @@
import axios from 'axios'
import { scaffoldProducts } from '@/src/trpc/apis/common-apis/common'
import { scaffoldEssentialConsts } from '@/src/trpc/apis/common-apis/common-trpc-index'
import { scaffoldStores } from '@/src/trpc/apis/user-apis/apis/stores'
import { scaffoldSlotsWithProducts } from '@/src/trpc/apis/user-apis/apis/slots'
import { scaffoldBanners } from '@/src/trpc/apis/user-apis/apis/banners'
import { scaffoldStoreWithProducts } from '@/src/trpc/apis/user-apis/apis/stores'
import { storeInfo } from '@/src/db/schema'
import { db } from '@/src/db/db_index'
import { imageUploadS3 } from '@/src/lib/s3-client'
import { apiCacheKey, cloudflareApiToken, cloudflareZoneId, assetsDomain } from '@/src/lib/env-exporter'
import { CACHE_FILENAMES } from '@packages/shared'
import { retryWithExponentialBackoff } from '@/src/lib/retry'
function constructCacheUrl(path: string): string {
return `${assetsDomain}${apiCacheKey}/${path}`
}
export async function createProductsFile(): Promise<string> {
// Get products data from the API method
const productsData = await scaffoldProducts()
// Convert to JSON string with pretty formatting
const jsonContent = JSON.stringify(productsData, null, 2)
// Convert to Buffer for S3 upload
const buffer = Buffer.from(jsonContent, 'utf-8')
// Upload to S3 at the specified path using apiCacheKey
const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.products}`)
// Purge cache with retry
const url = constructCacheUrl(CACHE_FILENAMES.products)
try {
await retryWithExponentialBackoff(() => clearUrlCache([url]))
console.log(`Cache purged for ${url}`)
} catch (error) {
console.error(`Failed to purge cache for ${url} after 3 retries:`, error)
}
return s3Key
}
export async function createEssentialConstsFile(): Promise<string> {
// Get essential consts data from the API method
const essentialConstsData = await scaffoldEssentialConsts()
// Convert to JSON string with pretty formatting
const jsonContent = JSON.stringify(essentialConstsData, null, 2)
// Convert to Buffer for S3 upload
const buffer = Buffer.from(jsonContent, 'utf-8')
// Upload to S3 at the specified path using apiCacheKey
const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.essentialConsts}`)
// Purge cache with retry
const url = constructCacheUrl(CACHE_FILENAMES.essentialConsts)
try {
await retryWithExponentialBackoff(() => clearUrlCache([url]))
console.log(`Cache purged for ${url}`)
} catch (error) {
console.error(`Failed to purge cache for ${url} after 3 retries:`, error)
}
return s3Key
}
export async function createStoresFile(): Promise<string> {
// Get stores data from the API method
const storesData = await scaffoldStores()
// Convert to JSON string with pretty formatting
const jsonContent = JSON.stringify(storesData, null, 2)
// Convert to Buffer for S3 upload
const buffer = Buffer.from(jsonContent, 'utf-8')
// Upload to S3 at the specified path using apiCacheKey
const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.stores}`)
// Purge cache with retry
const url = constructCacheUrl(CACHE_FILENAMES.stores)
try {
await retryWithExponentialBackoff(() => clearUrlCache([url]))
console.log(`Cache purged for ${url}`)
} catch (error) {
console.error(`Failed to purge cache for ${url} after 3 retries:`, error)
}
return s3Key
}
export async function createSlotsFile(): Promise<string> {
// Get slots data from the API method
const slotsData = await scaffoldSlotsWithProducts()
// Convert to JSON string with pretty formatting
const jsonContent = JSON.stringify(slotsData, null, 2)
// Convert to Buffer for S3 upload
const buffer = Buffer.from(jsonContent, 'utf-8')
// Upload to S3 at the specified path using apiCacheKey
const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.slots}`)
// Purge cache with retry
const url = constructCacheUrl(CACHE_FILENAMES.slots)
try {
await retryWithExponentialBackoff(() => clearUrlCache([url]))
console.log(`Cache purged for ${url}`)
} catch (error) {
console.error(`Failed to purge cache for ${url} after 3 retries:`, error)
}
return s3Key
}
export async function createBannersFile(): Promise<string> {
// Get banners data from the API method
const bannersData = await scaffoldBanners()
// Convert to JSON string with pretty formatting
const jsonContent = JSON.stringify(bannersData, null, 2)
// Convert to Buffer for S3 upload
const buffer = Buffer.from(jsonContent, 'utf-8')
// Upload to S3 at the specified path using apiCacheKey
const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.banners}`)
// Purge cache with retry
const url = constructCacheUrl(CACHE_FILENAMES.banners)
try {
await retryWithExponentialBackoff(() => clearUrlCache([url]))
console.log(`Cache purged for ${url}`)
} catch (error) {
console.error(`Failed to purge cache for ${url} after 3 retries:`, error)
}
return s3Key
}
export async function createStoreFile(storeId: number): Promise<string> {
// Get store data from the API method
const storeData = await scaffoldStoreWithProducts(storeId)
// Convert to JSON string with pretty formatting
const jsonContent = JSON.stringify(storeData, null, 2)
// Convert to Buffer for S3 upload
const buffer = Buffer.from(jsonContent, 'utf-8')
// Upload to S3 at the specified path using apiCacheKey
const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/stores/${storeId}.json`)
// Purge cache with retry
const url = constructCacheUrl(`stores/${storeId}.json`)
try {
await retryWithExponentialBackoff(() => clearUrlCache([url]))
console.log(`Cache purged for ${url}`)
} catch (error) {
console.error(`Failed to purge cache for ${url} after 3 retries:`, error)
}
return s3Key
}
export async function createAllStoresFiles(): Promise<string[]> {
// Fetch all store IDs from database
const stores = await db.select({ id: storeInfo.id }).from(storeInfo)
// Create cache files for all stores and collect URLs
const results: string[] = []
const urls: string[] = []
for (const store of stores) {
const s3Key = await createStoreFile(store.id)
results.push(s3Key)
urls.push(constructCacheUrl(`stores/${store.id}.json`))
}
console.log(`Created ${results.length} store cache files`)
// Purge all store caches in one batch with retry
try {
await retryWithExponentialBackoff(() => clearUrlCache(urls))
console.log(`Cache purged for ${urls.length} store files`)
} catch (error) {
console.error(`Failed to purge cache for store files after 3 retries. URLs: ${urls.join(', ')}`, error)
}
return results
}
export interface CreateAllCacheFilesResult {
products: string
essentialConsts: string
stores: string
slots: string
banners: string
individualStores: string[]
}
export async function createAllCacheFiles(): Promise<CreateAllCacheFilesResult> {
console.log('Starting creation of all cache files...')
// Create all global cache files in parallel
const [
productsKey,
essentialConstsKey,
storesKey,
slotsKey,
bannersKey,
individualStoreKeys,
] = await Promise.all([
createProductsFileInternal(),
createEssentialConstsFileInternal(),
createStoresFileInternal(),
createSlotsFileInternal(),
createBannersFileInternal(),
createAllStoresFilesInternal(),
])
// Collect all URLs for batch cache purge
const urls = [
constructCacheUrl(CACHE_FILENAMES.products),
constructCacheUrl(CACHE_FILENAMES.essentialConsts),
constructCacheUrl(CACHE_FILENAMES.stores),
constructCacheUrl(CACHE_FILENAMES.slots),
constructCacheUrl(CACHE_FILENAMES.banners),
...individualStoreKeys.map((_, index) => constructCacheUrl(`stores/${index + 1}.json`)),
]
// Purge all caches in one batch with retry
try {
await retryWithExponentialBackoff(() => clearUrlCache(urls))
console.log(`Cache purged for all ${urls.length} files`)
} catch (error) {
console.error(`Failed to purge cache for all files after 3 retries`, error)
}
console.log('All cache files created successfully')
return {
products: productsKey,
essentialConsts: essentialConstsKey,
stores: storesKey,
slots: slotsKey,
banners: bannersKey,
individualStores: individualStoreKeys,
}
}
// Internal versions that skip cache purging (for batch operations)
async function createProductsFileInternal(): Promise<string> {
const productsData = await scaffoldProducts()
const jsonContent = JSON.stringify(productsData, null, 2)
const buffer = Buffer.from(jsonContent, 'utf-8')
return await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.products}`)
}
async function createEssentialConstsFileInternal(): Promise<string> {
const essentialConstsData = await scaffoldEssentialConsts()
const jsonContent = JSON.stringify(essentialConstsData, null, 2)
const buffer = Buffer.from(jsonContent, 'utf-8')
return await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.essentialConsts}`)
}
async function createStoresFileInternal(): Promise<string> {
const storesData = await scaffoldStores()
const jsonContent = JSON.stringify(storesData, null, 2)
const buffer = Buffer.from(jsonContent, 'utf-8')
return await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.stores}`)
}
async function createSlotsFileInternal(): Promise<string> {
const slotsData = await scaffoldSlotsWithProducts()
const jsonContent = JSON.stringify(slotsData, null, 2)
const buffer = Buffer.from(jsonContent, 'utf-8')
return await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.slots}`)
}
async function createBannersFileInternal(): Promise<string> {
const bannersData = await scaffoldBanners()
const jsonContent = JSON.stringify(bannersData, null, 2)
const buffer = Buffer.from(jsonContent, 'utf-8')
return await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.banners}`)
}
async function createAllStoresFilesInternal(): Promise<string[]> {
const stores = await db.select({ id: storeInfo.id }).from(storeInfo)
const results: string[] = []
for (const store of stores) {
const storeData = await scaffoldStoreWithProducts(store.id)
const jsonContent = JSON.stringify(storeData, null, 2)
const buffer = Buffer.from(jsonContent, 'utf-8')
const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/stores/${store.id}.json`)
results.push(s3Key)
}
console.log(`Created ${results.length} store cache files`)
return results
}
export async function clearUrlCache(urls: string[]): Promise<{ success: boolean; errors?: string[] }> {
if (!cloudflareApiToken || !cloudflareZoneId) {
console.warn('Cloudflare credentials not configured, skipping cache clear')
return { success: false, errors: ['Cloudflare credentials not configured'] }
}
try {
const response = await axios.post(
`https://api.cloudflare.com/client/v4/zones/${cloudflareZoneId}/purge_cache`,
{ files: urls },
{
headers: {
'Authorization': `Bearer ${cloudflareApiToken}`,
'Content-Type': 'application/json',
},
}
)
const result = response.data as { success: boolean; errors?: { message: string }[] }
if (!result.success) {
const errorMessages = result.errors?.map(e => e.message) || ['Unknown error']
console.error(`Cloudflare cache purge failed for URLs: ${urls.join(', ')}`, errorMessages)
return { success: false, errors: errorMessages }
}
console.log(`Successfully purged ${urls.length} URLs from Cloudflare cache: ${urls.join(', ')}`)
return { success: true }
} catch (error) {
console.log(error)
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
console.error(`Error clearing Cloudflare cache for URLs: ${urls.join(', ')}`, errorMessage)
return { success: false, errors: [errorMessage] }
}
}
export async function clearAllCache(): Promise<{ success: boolean; errors?: string[] }> {
if (!cloudflareApiToken || !cloudflareZoneId) {
console.warn('Cloudflare credentials not configured, skipping cache clear')
return { success: false, errors: ['Cloudflare credentials not configured'] }
}
try {
const response = await axios.post(
`https://api.cloudflare.com/client/v4/zones/${cloudflareZoneId}/purge_cache`,
{ purge_everything: true },
{
headers: {
'Authorization': `Bearer ${cloudflareApiToken}`,
'Content-Type': 'application/json',
},
}
)
const result = response.data as { success: boolean; errors?: { message: string }[] }
if (!result.success) {
const errorMessages = result.errors?.map(e => e.message) || ['Unknown error']
console.error('Cloudflare cache purge failed:', errorMessages)
return { success: false, errors: errorMessages }
}
console.log('Successfully purged all cache from Cloudflare')
return { success: true }
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
console.error('Error clearing Cloudflare cache:', errorMessage)
return { success: false, errors: [errorMessage] }
}
}

View file

@ -17,6 +17,12 @@ export const s3Region = process.env.S3_REGION as string
export const assetsDomain = process.env.ASSETS_DOMAIN as string; export const assetsDomain = process.env.ASSETS_DOMAIN as string;
export const apiCacheKey = process.env.API_CACHE_KEY as string;
export const cloudflareApiToken = process.env.CLOUDFLARE_API_TOKEN as string;
export const cloudflareZoneId = process.env.CLOUDFLARE_ZONE_ID as string;
export const s3Url = process.env.S3_URL as string export const s3Url = process.env.S3_URL as string
export const redisUrl = process.env.REDIS_URL as string export const redisUrl = process.env.REDIS_URL as string

View file

@ -3,6 +3,7 @@ import { initializeAllStores } from '@/src/stores/store-initializer'
import { initializeUserNegativityStore } from '@/src/stores/user-negativity-store' import { initializeUserNegativityStore } from '@/src/stores/user-negativity-store'
import { startOrderHandler, startCancellationHandler, publishOrder } from '@/src/lib/post-order-handler' import { startOrderHandler, startCancellationHandler, publishOrder } from '@/src/lib/post-order-handler'
import { deleteOrders } from '@/src/lib/delete-orders' import { deleteOrders } from '@/src/lib/delete-orders'
import { createAllCacheFiles } from '@/src/lib/cloud_cache'
/** /**
* Initialize all application services * Initialize all application services
@ -25,6 +26,10 @@ export const initFunc = async (): Promise<void> => {
startCancellationHandler(), startCancellationHandler(),
]); ]);
// Create all cache files after stores are initialized
await createAllCacheFiles();
console.log('Cache files created successfully');
console.log('Application initialization completed successfully'); console.log('Application initialization completed successfully');
} catch (error) { } catch (error) {
console.error('Application initialization failed:', error); console.error('Application initialization failed:', error);

View file

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

View file

@ -35,10 +35,7 @@ const formatOrderMessageWithFullData = (ordersData: any[]): string => {
message += '📦 <b>Items:</b>\n'; message += '📦 <b>Items:</b>\n';
order.orderItems?.forEach((item: any) => { order.orderItems?.forEach((item: any) => {
const productQuantity = item.product?.productQuantity ?? 1 message += `${item.product?.name || 'Unknown'} x${item.quantity}\n`;
const unitNotation = item.product?.unit?.shortNotation || ''
const quantityWithUnit = unitNotation ? `${productQuantity}${unitNotation}` : `${productQuantity}`
message += `${item.product?.name || 'Unknown'} ${quantityWithUnit} x${item.quantity}\n`;
}); });
message += `\n💰 <b>Total:</b> ₹${order.totalAmount}\n`; message += `\n💰 <b>Total:</b> ₹${order.totalAmount}\n`;
@ -75,12 +72,7 @@ const formatCancellationMessage = (orderData: any, cancellationData: Cancellatio
📞 <b>Phone:</b> ${orderData.address?.phone || 'N/A'} 📞 <b>Phone:</b> ${orderData.address?.phone || 'N/A'}
📦 <b>Items:</b> 📦 <b>Items:</b>
${orderData.orderItems?.map((item: any) => { ${orderData.orderItems?.map((item: any) => `${item.product?.name || 'Unknown'} x${item.quantity}`).join('\n') || ' N/A'}
const productQuantity = item.product?.productQuantity ?? 1
const unitNotation = item.product?.unit?.shortNotation || ''
const quantityWithUnit = unitNotation ? `${productQuantity}${unitNotation}` : `${productQuantity}`
return `${item.product?.name || 'Unknown'} ${quantityWithUnit} x${item.quantity}`
}).join('\n') || ' N/A'}
💰 <b>Total:</b> ${orderData.totalAmount} 💰 <b>Total:</b> ${orderData.totalAmount}
💳 <b>Refund:</b> ${orderData.refundStatus === 'na' ? 'N/A (COD)' : orderData.refundStatus || 'Pending'} 💳 <b>Refund:</b> ${orderData.refundStatus === 'na' ? 'N/A (COD)' : orderData.refundStatus || 'Pending'}
@ -110,7 +102,7 @@ export const startOrderHandler = async (): Promise<void> => {
where: inArray(orders.id, orderIds), where: inArray(orders.id, orderIds),
with: { with: {
address: true, address: true,
orderItems: { with: { product: { with: { unit: true } } } }, orderItems: { with: { product: true } },
slot: true, slot: true,
}, },
}); });
@ -155,7 +147,7 @@ export const startCancellationHandler = async (): Promise<void> => {
where: eq(orders.id, cancellationData.orderId), where: eq(orders.id, cancellationData.orderId),
with: { with: {
address: true, address: true,
orderItems: { with: { product: { with: { unit: true } } } }, orderItems: { with: { product: true } },
refunds: true, refunds: true,
}, },
}); });

View file

@ -0,0 +1,23 @@
export async function retryWithExponentialBackoff<T>(
fn: () => Promise<T>,
maxRetries: number = 3,
delayMs: number = 1000
): Promise<T> {
let lastError: Error | undefined
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn()
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error))
if (attempt < maxRetries) {
console.log(`Attempt ${attempt} failed, retrying in ${delayMs}ms...`)
await new Promise(resolve => setTimeout(resolve, delayMs))
delayMs *= 2
}
}
}
throw lastError
}

View file

@ -0,0 +1,221 @@
// Postgres Importer - Intermediate layer to avoid direct postgresService imports in dbService
// This file re-exports everything from postgresService
// Re-export database connection
export { db } from 'postgresService'
// Re-export all schema exports
export * from 'postgresService'
// Re-export all helper methods from postgresService
export {
// Admin - Banner
getBanners,
getBannerById,
createBanner,
updateBanner,
deleteBanner,
// Admin - Complaint
getComplaints,
resolveComplaint,
// Admin - Constants
getAllConstants,
upsertConstants,
// Admin - Coupon
getAllCoupons,
getCouponById,
invalidateCoupon,
validateCoupon,
getReservedCoupons,
getUsersForCoupon,
createCouponWithRelations,
updateCouponWithRelations,
generateCancellationCoupon,
createReservedCouponWithProducts,
createCouponForUser,
checkUsersExist,
checkCouponExists,
checkReservedCouponExists,
getOrderWithUser,
// Admin - Order
updateOrderNotes,
getOrderDetails,
updateOrderPackaged,
updateOrderDelivered,
updateOrderItemPackaging,
removeDeliveryCharge,
getSlotOrders,
updateAddressCoords,
getAllOrders,
rebalanceSlots,
cancelOrder,
deleteOrderById,
// Admin - Product
getAllProducts,
getProductById,
deleteProduct,
createProduct,
updateProduct,
toggleProductOutOfStock,
updateSlotProducts,
getSlotProductIds,
getSlotsProductIds,
getAllUnits,
getAllProductTags,
getProductReviews,
respondToReview,
getAllProductGroups,
createProductGroup,
updateProductGroup,
deleteProductGroup,
addProductToGroup,
removeProductFromGroup,
updateProductPrices,
// Admin - Slots
getActiveSlotsWithProducts,
getActiveSlots,
getSlotsAfterDate,
getSlotByIdWithRelations,
createSlotWithRelations,
updateSlotWithRelations,
deleteSlotById,
updateSlotCapacity,
getSlotDeliverySequence,
updateSlotDeliverySequence,
// Admin - Staff User
getStaffUserByName,
getAllStaff,
getAllUsers,
getUserWithDetails,
updateUserSuspensionStatus,
checkStaffUserExists,
checkStaffRoleExists,
createStaffUser,
getAllRoles,
// Admin - Store
getAllStores,
getStoreById,
createStore,
updateStore,
deleteStore,
// Admin - User
createUserByMobile,
getUserByMobile,
getUnresolvedComplaintsCount,
getAllUsersWithFilters,
getOrderCountsByUserIds,
getLastOrdersByUserIds,
getSuspensionStatusesByUserIds,
getUserBasicInfo,
getUserSuspensionStatus,
getUserOrders,
getOrderStatusesByOrderIds,
getItemCountsByOrderIds,
upsertUserSuspension,
searchUsers,
getAllNotifCreds,
getAllUnloggedTokens,
getNotifTokensByUserIds,
getUserIncidentsWithRelations,
createUserIncident,
// Admin - Vendor Snippets
checkVendorSnippetExists,
getVendorSnippetById,
getVendorSnippetByCode,
getAllVendorSnippets,
createVendorSnippet,
updateVendorSnippet,
deleteVendorSnippet,
getProductsByIds,
getVendorSlotById,
getVendorOrdersBySlotId,
getOrderItemsByOrderIds,
getOrderStatusByOrderIds,
updateVendorOrderItemPackaging,
getVendorOrders,
// User - Address
getUserDefaultAddress,
getUserAddresses,
getUserAddressById,
clearUserDefaultAddress,
createUserAddress,
updateUserAddress,
deleteUserAddress,
hasOngoingOrdersForAddress,
// User - Banners
getUserActiveBanners,
// User - Cart
getUserCartItemsWithProducts,
getUserProductById,
getUserCartItemByUserProduct,
incrementUserCartItemQuantity,
insertUserCartItem,
updateUserCartItemQuantity,
deleteUserCartItem,
clearUserCart,
// User - Complaint
getUserComplaints,
createUserComplaint,
// User - Stores
getUserStoreSummaries,
getUserStoreDetail,
// User - Product
getUserProductDetailById,
getUserProductReviews,
getUserProductByIdBasic,
createUserProductReview,
// User - Slots
getUserActiveSlotsList,
getUserProductAvailability,
// User - Payments
getUserPaymentOrderById,
getUserPaymentByOrderId,
getUserPaymentByMerchantOrderId,
updateUserPaymentSuccess,
updateUserOrderPaymentStatus,
markUserPaymentFailed,
// User - Auth
getUserAuthByEmail,
getUserAuthByMobile,
getUserAuthById,
getUserAuthCreds,
getUserAuthDetails,
createUserAuthWithCreds,
createUserAuthWithMobile,
upsertUserAuthPassword,
deleteUserAuthAccount,
// User - Coupon
getUserActiveCouponsWithRelations,
getUserAllCouponsWithRelations,
getUserReservedCouponByCode,
redeemUserReservedCoupon,
// User - Profile
getUserProfileById,
getUserProfileDetailById,
getUserWithCreds,
getUserNotifCred,
upsertUserNotifCred,
deleteUserUnloggedToken,
getUserUnloggedToken,
upsertUserUnloggedToken,
// User - Order
validateAndGetUserCoupon,
applyDiscountToUserOrder,
getUserAddressByIdAndUser,
getOrderProductById,
checkUserSuspended,
getUserSlotCapacityStatus,
placeUserOrderTransaction,
deleteUserCartItemsForOrder,
recordUserCouponUsage,
getUserOrdersWithRelations,
getUserOrderCount,
getUserOrderByIdWithRelations,
getUserCouponUsageForOrder,
getUserOrderBasic,
cancelUserOrderTransaction,
updateUserOrderNotes,
getUserRecentlyDeliveredOrderIds,
getUserProductIdsFromOrders,
getUserProductsForRecentOrders,
} from 'postgresService'

View file

@ -4,6 +4,10 @@ import { initializeProducts } from '@/src/stores/product-store'
import { initializeProductTagStore } from '@/src/stores/product-tag-store' import { initializeProductTagStore } from '@/src/stores/product-tag-store'
import { initializeSlotStore } from '@/src/stores/slot-store' import { initializeSlotStore } from '@/src/stores/slot-store'
import { initializeBannerStore } from '@/src/stores/banner-store' import { initializeBannerStore } from '@/src/stores/banner-store'
import { createAllCacheFiles } from '@/src/lib/cloud_cache'
const STORE_INIT_DELAY_MS = 3 * 60 * 1000
let storeInitializationTimeout: NodeJS.Timeout | null = null
/** /**
* Initialize all application stores * Initialize all application stores
@ -29,8 +33,27 @@ export const initializeAllStores = async (): Promise<void> => {
]); ]);
console.log('All application stores initialized successfully'); console.log('All application stores initialized successfully');
// Regenerate all cache files (fire-and-forget)
createAllCacheFiles().catch(error => {
console.error('Failed to regenerate cache files during store initialization:', error)
})
} catch (error) { } catch (error) {
console.error('Application stores initialization failed:', error); console.error('Application stores initialization failed:', error);
throw error; throw error;
} }
}; };
export const scheduleStoreInitialization = (): void => {
if (storeInitializationTimeout) {
clearTimeout(storeInitializationTimeout)
storeInitializationTimeout = null
}
storeInitializationTimeout = setTimeout(() => {
storeInitializationTimeout = null
initializeAllStores().catch(error => {
console.error('Scheduled store initialization failed:', error)
})
}, STORE_INIT_DELAY_MS)
}

View file

@ -1,32 +0,0 @@
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,7 +2,6 @@
import { router } from '@/src/trpc/trpc-index' import { router } from '@/src/trpc/trpc-index'
import { complaintRouter } from '@/src/trpc/apis/admin-apis/apis/complaint' import { complaintRouter } from '@/src/trpc/apis/admin-apis/apis/complaint'
import { couponRouter } from '@/src/trpc/apis/admin-apis/apis/coupon' import { couponRouter } from '@/src/trpc/apis/admin-apis/apis/coupon'
import { cancelledOrdersRouter } from '@/src/trpc/apis/admin-apis/apis/cancelled-orders'
import { orderRouter } from '@/src/trpc/apis/admin-apis/apis/order' import { orderRouter } from '@/src/trpc/apis/admin-apis/apis/order'
import { vendorSnippetsRouter } from '@/src/trpc/apis/admin-apis/apis/vendor-snippets' import { vendorSnippetsRouter } from '@/src/trpc/apis/admin-apis/apis/vendor-snippets'
import { slotsRouter } from '@/src/trpc/apis/admin-apis/apis/slots' import { slotsRouter } from '@/src/trpc/apis/admin-apis/apis/slots'
@ -10,7 +9,6 @@ import { productRouter } from '@/src/trpc/apis/admin-apis/apis/product'
import { staffUserRouter } from '@/src/trpc/apis/admin-apis/apis/staff-user' import { staffUserRouter } from '@/src/trpc/apis/admin-apis/apis/staff-user'
import { storeRouter } from '@/src/trpc/apis/admin-apis/apis/store' import { storeRouter } from '@/src/trpc/apis/admin-apis/apis/store'
import { adminPaymentsRouter } from '@/src/trpc/apis/admin-apis/apis/payments' import { adminPaymentsRouter } from '@/src/trpc/apis/admin-apis/apis/payments'
import addressRouter from '@/src/trpc/apis/admin-apis/apis/address'
import { bannerRouter } from '@/src/trpc/apis/admin-apis/apis/banner' import { bannerRouter } from '@/src/trpc/apis/admin-apis/apis/banner'
import { userRouter } from '@/src/trpc/apis/admin-apis/apis/user' import { userRouter } from '@/src/trpc/apis/admin-apis/apis/user'
import { constRouter } from '@/src/trpc/apis/admin-apis/apis/const' import { constRouter } from '@/src/trpc/apis/admin-apis/apis/const'
@ -18,7 +16,6 @@ import { constRouter } from '@/src/trpc/apis/admin-apis/apis/const'
export const adminRouter = router({ export const adminRouter = router({
complaint: complaintRouter, complaint: complaintRouter,
coupon: couponRouter, coupon: couponRouter,
cancelledOrders: cancelledOrdersRouter,
order: orderRouter, order: orderRouter,
vendorSnippets: vendorSnippetsRouter, vendorSnippets: vendorSnippetsRouter,
slots: slotsRouter, slots: slotsRouter,
@ -26,7 +23,6 @@ export const adminRouter = router({
staffUser: staffUserRouter, staffUser: staffUserRouter,
store: storeRouter, store: storeRouter,
payments: adminPaymentsRouter, payments: adminPaymentsRouter,
address: addressRouter,
banner: bannerRouter, banner: bannerRouter,
user: userRouter, user: userRouter,
const: constRouter, const: constRouter,

View file

@ -1,22 +1,34 @@
import { z } from 'zod'; import { z } from 'zod';
import { db } from '@/src/db/db_index'
import { homeBanners } from '@/src/db/schema'
import { eq, and, desc, sql } from 'drizzle-orm';
import { protectedProcedure, router } from '@/src/trpc/trpc-index' import { protectedProcedure, router } from '@/src/trpc/trpc-index'
import { extractKeyFromPresignedUrl, generateSignedUrlFromS3Url } from '@/src/lib/s3-client' import { extractKeyFromPresignedUrl, generateSignedUrlFromS3Url } from '@/src/lib/s3-client'
import { ApiError } from '@/src/lib/api-error'; import { ApiError } from '@/src/lib/api-error';
import { initializeAllStores } from '@/src/stores/store-initializer' import { scheduleStoreInitialization } from '@/src/stores/store-initializer'
import {
getBanners as getBannersFromDb,
getBannerById as getBannerByIdFromDb,
createBanner as createBannerInDb,
updateBanner as updateBannerInDb,
deleteBanner as deleteBannerFromDb,
} from '@/src/dbService'
import type { Banner } from '@packages/shared'
export const bannerRouter = router({ export const bannerRouter = router({
// Get all banners // Get all banners
getBanners: protectedProcedure getBanners: protectedProcedure
.query(async () => { .query(async (): Promise<{ banners: Banner[] }> => {
try { try {
const banners = await db.query.homeBanners.findMany({ // Using dbService helper (new implementation)
orderBy: desc(homeBanners.createdAt), // Order by creation date instead const banners = await getBannersFromDb();
// Old implementation - direct DB query:
// 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 // Removed product relationship since we now use productIds array
}); // });
// Convert S3 keys to signed URLs for client // Convert S3 keys to signed URLs for client
const bannersWithSignedUrls = await Promise.all( const bannersWithSignedUrls = await Promise.all(
@ -54,11 +66,17 @@ export const bannerRouter = router({
// Get single banner by ID // Get single banner by ID
getBanner: protectedProcedure getBanner: protectedProcedure
.input(z.object({ id: z.number() })) .input(z.object({ id: z.number() }))
.query(async ({ input }) => { .query(async ({ input }): Promise<Banner | null> => {
// Using dbService helper (new implementation)
const banner = await getBannerByIdFromDb(input.id);
/*
// Old implementation - direct DB query:
const banner = await db.query.homeBanners.findFirst({ const banner = await db.query.homeBanners.findFirst({
where: eq(homeBanners.id, input.id), where: eq(homeBanners.id, input.id),
// Removed product relationship since we now use productIds array // Removed product relationship since we now use productIds array
}); });
*/
if (banner) { if (banner) {
try { try {
@ -90,8 +108,22 @@ export const bannerRouter = router({
redirectUrl: z.string().url().optional(), redirectUrl: z.string().url().optional(),
// serialNum removed completely // serialNum removed completely
})) }))
.mutation(async ({ input }) => { .mutation(async ({ input }): Promise<Banner> => {
try { try {
// Using dbService helper (new implementation)
const imageUrl = extractKeyFromPresignedUrl(input.imageUrl)
const banner = await createBannerInDb({
name: input.name,
imageUrl: imageUrl,
description: input.description ?? null,
productIds: input.productIds || [],
redirectUrl: input.redirectUrl ?? null,
serialNum: 999, // Default value, not used
isActive: false, // Default to inactive
});
/*
// Old implementation - direct DB query:
const imageUrl = extractKeyFromPresignedUrl(input.imageUrl) const imageUrl = extractKeyFromPresignedUrl(input.imageUrl)
const [banner] = await db.insert(homeBanners).values({ const [banner] = await db.insert(homeBanners).values({
name: input.name, name: input.name,
@ -102,9 +134,10 @@ export const bannerRouter = router({
serialNum: 999, // Default value, not used serialNum: 999, // Default value, not used
isActive: false, // Default to inactive isActive: false, // Default to inactive
}).returning(); }).returning();
*/
// Reinitialize stores to reflect changes // Reinitialize stores to reflect changes
await initializeAllStores(); scheduleStoreInitialization()
return banner; return banner;
} catch (error) { } catch (error) {
@ -125,9 +158,28 @@ export const bannerRouter = router({
serialNum: z.number().nullable().optional(), serialNum: z.number().nullable().optional(),
isActive: z.boolean().optional(), isActive: z.boolean().optional(),
})) }))
.mutation(async ({ input }) => { .mutation(async ({ input }): Promise<Banner> => {
try { try {
// Using dbService helper (new implementation)
const { id, ...updateData } = input;
// Extract S3 key from presigned URL if imageUrl is provided
const processedData = {
...updateData,
...(updateData.imageUrl && {
imageUrl: extractKeyFromPresignedUrl(updateData.imageUrl)
}),
};
// Handle serialNum null case
if ('serialNum' in processedData && processedData.serialNum === null) {
processedData.serialNum = null;
}
const banner = await updateBannerInDb(id, processedData);
/*
// Old implementation - direct DB query:
const { id, ...updateData } = input; const { id, ...updateData } = input;
const incomingProductIds = input.productIds; const incomingProductIds = input.productIds;
// Extract S3 key from presigned URL if imageUrl is provided // Extract S3 key from presigned URL if imageUrl is provided
@ -149,9 +201,10 @@ export const bannerRouter = router({
.set({ ...finalData, lastUpdated: new Date(), }) .set({ ...finalData, lastUpdated: new Date(), })
.where(eq(homeBanners.id, id)) .where(eq(homeBanners.id, id))
.returning(); .returning();
*/
// Reinitialize stores to reflect changes // Reinitialize stores to reflect changes
await initializeAllStores(); scheduleStoreInitialization()
return banner; return banner;
} catch (error) { } catch (error) {
@ -163,11 +216,17 @@ export const bannerRouter = router({
// Delete banner // Delete banner
deleteBanner: protectedProcedure deleteBanner: protectedProcedure
.input(z.object({ id: z.number() })) .input(z.object({ id: z.number() }))
.mutation(async ({ input }) => { .mutation(async ({ input }): Promise<{ success: true }> => {
// Using dbService helper (new implementation)
await deleteBannerFromDb(input.id);
/*
// Old implementation - direct DB query:
await db.delete(homeBanners).where(eq(homeBanners.id, input.id)); await db.delete(homeBanners).where(eq(homeBanners.id, input.id));
*/
// Reinitialize stores to reflect changes // Reinitialize stores to reflect changes
await initializeAllStores(); scheduleStoreInitialization()
return { success: true }; return { success: true };
}), }),

View file

@ -1,179 +0,0 @@
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,9 +1,8 @@
import { router, protectedProcedure } from '@/src/trpc/trpc-index' import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'; import { z } from 'zod';
import { db } from '@/src/db/db_index'
import { complaints, users } from '@/src/db/schema'
import { eq, desc, lt, and } from 'drizzle-orm';
import { generateSignedUrlsFromS3Urls } from '@/src/lib/s3-client' import { generateSignedUrlsFromS3Urls } from '@/src/lib/s3-client'
import { getComplaints as getComplaintsFromDb, resolveComplaint as resolveComplaintInDb } from '@/src/dbService'
import type { ComplaintWithUser } from '@packages/shared'
export const complaintRouter = router({ export const complaintRouter = router({
getAll: protectedProcedure getAll: protectedProcedure
@ -11,7 +10,27 @@ export const complaintRouter = router({
cursor: z.number().optional(), cursor: z.number().optional(),
limit: z.number().default(20), limit: z.number().default(20),
})) }))
.query(async ({ input }) => { .query(async ({ input }): Promise<{
complaints: Array<{
id: number;
text: string;
userId: number;
userName: string | null;
userMobile: string | null;
orderId: number | null;
status: string;
createdAt: Date;
images: string[];
}>;
nextCursor?: number;
}> => {
const { cursor, limit } = input;
// Using dbService helper (new implementation)
const { complaints: complaintsData, hasMore } = await getComplaintsFromDb(cursor, limit);
/*
// Old implementation - direct DB query:
const { cursor, limit } = input; const { cursor, limit } = input;
let whereCondition = cursor let whereCondition = cursor
@ -37,10 +56,13 @@ export const complaintRouter = router({
.limit(limit + 1); .limit(limit + 1);
const hasMore = complaintsData.length > limit; const hasMore = complaintsData.length > limit;
const complaintsToReturn = hasMore ? complaintsData.slice(0, limit) : complaintsData;
*/
const complaintsToReturn = hasMore ? complaintsData.slice(0, limit) : complaintsData; const complaintsToReturn = hasMore ? complaintsData.slice(0, limit) : complaintsData;
const complaintsWithSignedImages = await Promise.all( const complaintsWithSignedImages = await Promise.all(
complaintsToReturn.map(async (c) => { complaintsToReturn.map(async (c: ComplaintWithUser) => {
const signedImages = c.images const signedImages = c.images
? await generateSignedUrlsFromS3Urls(c.images as string[]) ? await generateSignedUrlsFromS3Urls(c.images as string[])
: []; : [];
@ -69,11 +91,17 @@ export const complaintRouter = router({
resolve: protectedProcedure resolve: protectedProcedure
.input(z.object({ id: z.string(), response: z.string().optional() })) .input(z.object({ id: z.string(), response: z.string().optional() }))
.mutation(async ({ input }) => { .mutation(async ({ input }): Promise<{ message: string }> => {
// Using dbService helper (new implementation)
await resolveComplaintInDb(parseInt(input.id), input.response);
/*
// Old implementation - direct DB query:
await db await db
.update(complaints) .update(complaints)
.set({ isResolved: true, response: input.response }) .set({ isResolved: true, response: input.response })
.where(eq(complaints.id, parseInt(input.id))); .where(eq(complaints.id, parseInt(input.id)));
*/
return { message: 'Complaint resolved successfully' }; return { message: 'Complaint resolved successfully' };
}), }),

View file

@ -1,22 +1,27 @@
import { router, protectedProcedure } from '@/src/trpc/trpc-index' import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'; import { z } from 'zod';
import { db } from '@/src/db/db_index'
import { keyValStore } from '@/src/db/schema'
import { computeConstants } from '@/src/lib/const-store' import { computeConstants } from '@/src/lib/const-store'
import { CONST_KEYS } from '@/src/lib/const-keys' import { CONST_KEYS } from '@/src/lib/const-keys'
import { getAllConstants as getAllConstantsFromDb, upsertConstants as upsertConstantsInDb } from '@/src/dbService'
import type { Constant, ConstantUpdateResult } from '@packages/shared'
export const constRouter = router({ export const constRouter = router({
getConstants: protectedProcedure getConstants: protectedProcedure
.query(async () => { .query(async (): Promise<Constant[]> => {
// Using dbService helper (new implementation)
const constants = await getAllConstantsFromDb();
/*
// Old implementation - direct DB query:
const constants = await db.select().from(keyValStore); const constants = await db.select().from(keyValStore);
const resp = constants.map(c => ({ const resp = constants.map(c => ({
key: c.key, key: c.key,
value: c.value, value: c.value,
})); }));
*/
return resp; return constants;
}), }),
updateConstants: protectedProcedure updateConstants: protectedProcedure
@ -26,7 +31,7 @@ export const constRouter = router({
value: z.any(), value: z.any(),
})), })),
})) }))
.mutation(async ({ input }) => { .mutation(async ({ input }): Promise<ConstantUpdateResult> => {
const { constants } = input; const { constants } = input;
const validKeys = Object.values(CONST_KEYS) as string[]; const validKeys = Object.values(CONST_KEYS) as string[];
@ -38,6 +43,11 @@ export const constRouter = router({
throw new Error(`Invalid constant keys: ${invalidKeys.join(', ')}`); throw new Error(`Invalid constant keys: ${invalidKeys.join(', ')}`);
} }
// Using dbService helper (new implementation)
await upsertConstantsInDb(constants);
/*
// Old implementation - direct DB query:
await db.transaction(async (tx) => { await db.transaction(async (tx) => {
for (const { key, value } of constants) { for (const { key, value } of constants) {
await tx.insert(keyValStore) await tx.insert(keyValStore)
@ -48,6 +58,7 @@ export const constRouter = router({
}); });
} }
}); });
*/
// Refresh all constants in Redis after database update // Refresh all constants in Redis after database update
await computeConstants(); await computeConstants();

View file

@ -1,9 +1,26 @@
import { router, protectedProcedure } from '@/src/trpc/trpc-index' import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'; import { z } from 'zod';
import { db } from '@/src/db/db_index'
import { coupons, users, staffUsers, orders, couponApplicableUsers, couponApplicableProducts, orderStatus, reservedCoupons } from '@/src/db/schema'
import { eq, and, like, or, inArray, lt } from 'drizzle-orm';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import {
// Batch 1 - Non-transaction methods
getAllCoupons as getAllCouponsFromDb,
getCouponById as getCouponByIdFromDb,
invalidateCoupon as invalidateCouponInDb,
validateCoupon as validateCouponInDb,
getReservedCoupons as getReservedCouponsFromDb,
getUsersForCoupon as getUsersForCouponFromDb,
// Batch 2 - Transaction methods
createCouponWithRelations,
updateCouponWithRelations,
generateCancellationCoupon,
createReservedCouponWithProducts,
createCouponForUser,
checkUsersExist,
checkCouponExists,
checkReservedCouponExists,
getOrderWithUser,
} from '@/src/dbService'
import type { Coupon, CouponValidationResult, UserMiniInfo } from '@packages/shared'
const createCouponBodySchema = z.object({ const createCouponBodySchema = z.object({
couponCode: z.string().optional(), couponCode: z.string().optional(),
@ -31,7 +48,7 @@ const validateCouponBodySchema = z.object({
export const couponRouter = router({ export const couponRouter = router({
create: protectedProcedure create: protectedProcedure
.input(createCouponBodySchema) .input(createCouponBodySchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<Coupon> => {
const { couponCode, isUserBased, discountPercent, flatDiscount, minOrder, productIds, applicableUsers, applicableProducts, maxValue, isApplyForAll, 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 // Validation: ensure at least one discount type is provided
@ -49,17 +66,6 @@ export const couponRouter = router({
throw new Error("Cannot be both user-based and apply for all users"); throw new Error("Cannot be both user-based and apply for all users");
} }
// If applicableUsers is provided, verify users exist
if (applicableUsers && applicableUsers.length > 0) {
const existingUsers = await db.query.users.findMany({
where: inArray(users.id, applicableUsers),
columns: { id: true },
});
if (existingUsers.length !== applicableUsers.length) {
throw new Error("Some applicable users not found");
}
}
// Get staff user ID from auth middleware // Get staff user ID from auth middleware
const staffUserId = ctx.staffUser?.id; const staffUserId = ctx.staffUser?.id;
if (!staffUserId) { if (!staffUserId) {
@ -69,22 +75,27 @@ export const couponRouter = router({
// Generate coupon code if not provided // Generate coupon code if not provided
let finalCouponCode = couponCode; let finalCouponCode = couponCode;
if (!finalCouponCode) { if (!finalCouponCode) {
// Generate a unique coupon code
const timestamp = Date.now().toString().slice(-6); const timestamp = Date.now().toString().slice(-6);
const random = Math.random().toString(36).substring(2, 8).toUpperCase(); const random = Math.random().toString(36).substring(2, 8).toUpperCase();
finalCouponCode = `MF${timestamp}${random}`; finalCouponCode = `MF${timestamp}${random}`;
} }
// Check if coupon code already exists // Using dbService helper (new implementation)
const existingCoupon = await db.query.coupons.findFirst({ const codeExists = await checkCouponExists(finalCouponCode);
where: eq(coupons.couponCode, finalCouponCode), if (codeExists) {
});
if (existingCoupon) {
throw new Error("Coupon code already exists"); throw new Error("Coupon code already exists");
} }
const result = await db.insert(coupons).values({ // If applicableUsers is provided, verify users exist
if (applicableUsers && applicableUsers.length > 0) {
const usersExist = await checkUsersExist(applicableUsers);
if (!usersExist) {
throw new Error("Some applicable users not found");
}
}
const coupon = await createCouponWithRelations(
{
couponCode: finalCouponCode, couponCode: finalCouponCode,
isUserBased: isUserBased || false, isUserBased: isUserBased || false,
discountPercent: discountPercent?.toString(), discountPercent: discountPercent?.toString(),
@ -95,9 +106,29 @@ export const couponRouter = router({
maxValue: maxValue?.toString(), maxValue: maxValue?.toString(),
isApplyForAll: isApplyForAll || false, isApplyForAll: isApplyForAll || false,
validTill: validTill ? dayjs(validTill).toDate() : undefined, validTill: validTill ? dayjs(validTill).toDate() : undefined,
maxLimitForUser: maxLimitForUser, maxLimitForUser,
exclusiveApply: exclusiveApply || false, exclusiveApply: exclusiveApply || false,
}).returning(); },
applicableUsers,
applicableProducts
);
/*
// Old implementation - direct DB query with transaction:
const result = await db.insert(coupons).values({
couponCode: finalCouponCode,
isUserBased: isUserBased || false,
discountPercent: discountPercent?.toString(),
flatDiscount: flatDiscount?.toString(),
minOrder: minOrder?.toString(),
productIds: productIds || null,
createdBy: staffUserId,
maxValue: maxValue?.toString(),
isApplyForAll: isApplyForAll || false,
validTill: validTill ? dayjs(validTill).toDate() : undefined,
maxLimitForUser,
exclusiveApply: exclusiveApply || false,
}).returning();
const coupon = result[0]; const coupon = result[0];
@ -120,6 +151,7 @@ export const couponRouter = router({
})) }))
); );
} }
*/
return coupon; return coupon;
}), }),
@ -130,71 +162,22 @@ export const couponRouter = router({
limit: z.number().default(50), limit: z.number().default(50),
search: z.string().optional(), search: z.string().optional(),
})) }))
.query(async ({ input }) => { .query(async ({ input }): Promise<{ coupons: any[]; nextCursor?: number }> => {
const { cursor, limit, search } = input; const { cursor, limit, search } = input;
let whereCondition = undefined; const { coupons: couponsList, hasMore } = await getAllCouponsFromDb(cursor, limit, search);
const conditions = [];
if (cursor) { const nextCursor = hasMore ? couponsList[couponsList.length - 1].id : undefined;
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;
const nextCursor = hasMore ? result[result.length - 1].id : undefined;
return { coupons: couponsList, nextCursor }; return { coupons: couponsList, nextCursor };
}), }),
getById: protectedProcedure getById: protectedProcedure
.input(z.object({ id: z.number() })) .input(z.object({ id: z.number() }))
.query(async ({ input }) => { .query(async ({ input }): Promise<any> => {
const couponId = input.id; const couponId = input.id;
const result = await db.query.coupons.findFirst({ const result = await getCouponByIdFromDb(couponId);
where: eq(coupons.id, couponId),
with: {
creator: true,
applicableUsers: {
with: {
user: true,
},
},
applicableProducts: {
with: {
product: true,
},
},
},
});
if (!result) { if (!result) {
throw new Error("Coupon not found"); throw new Error("Coupon not found");
@ -203,8 +186,8 @@ export const couponRouter = router({
return { return {
...result, ...result,
productIds: (result.productIds as number[]) || undefined, productIds: (result.productIds as number[]) || undefined,
applicableUsers: result.applicableUsers.map(au => au.user), applicableUsers: result.applicableUsers.map((au: any) => au.user),
applicableProducts: result.applicableProducts.map(ap => ap.product), applicableProducts: result.applicableProducts.map((ap: any) => ap.product),
}; };
}), }),
@ -215,7 +198,7 @@ export const couponRouter = router({
isInvalidated: z.boolean().optional(), isInvalidated: z.boolean().optional(),
}), }),
})) }))
.mutation(async ({ input }) => { .mutation(async ({ input }): Promise<Coupon> => {
const { id, updates } = input; const { id, updates } = input;
// Validation: ensure discount types are valid // Validation: ensure discount types are valid
@ -225,43 +208,31 @@ export const couponRouter = router({
} }
} }
// If updating to user-based, applicableUsers is required // Prepare update data
if (updates.isUserBased && (!updates.applicableUsers || updates.applicableUsers.length === 0)) { const updateData: any = {};
const existingCount = await db.$count(couponApplicableUsers, eq(couponApplicableUsers.couponId, id)); if (updates.couponCode !== undefined) updateData.couponCode = updates.couponCode;
if (existingCount === 0) { if (updates.isUserBased !== undefined) updateData.isUserBased = updates.isUserBased;
throw new Error("applicableUsers is required for user-based coupons"); if (updates.discountPercent !== undefined) updateData.discountPercent = updates.discountPercent?.toString();
} if (updates.flatDiscount !== undefined) updateData.flatDiscount = updates.flatDiscount?.toString();
} if (updates.minOrder !== undefined) updateData.minOrder = updates.minOrder?.toString();
if (updates.maxValue !== undefined) updateData.maxValue = updates.maxValue?.toString();
if (updates.isApplyForAll !== undefined) updateData.isApplyForAll = updates.isApplyForAll;
if (updates.validTill !== undefined) updateData.validTill = updates.validTill ? dayjs(updates.validTill).toDate() : null;
if (updates.maxLimitForUser !== undefined) updateData.maxLimitForUser = updates.maxLimitForUser;
if (updates.exclusiveApply !== undefined) updateData.exclusiveApply = updates.exclusiveApply;
if (updates.isInvalidated !== undefined) updateData.isInvalidated = updates.isInvalidated;
if (updates.productIds !== undefined) updateData.productIds = updates.productIds;
// If applicableUsers is provided, verify users exist // Using dbService helper (new implementation)
if (updates.applicableUsers && updates.applicableUsers.length > 0) { const coupon = await updateCouponWithRelations(
const existingUsers = await db.query.users.findMany({ id,
where: inArray(users.id, updates.applicableUsers), updateData,
columns: { id: true }, updates.applicableUsers,
}); updates.applicableProducts
if (existingUsers.length !== updates.applicableUsers.length) { );
throw new Error("Some applicable users not found");
}
}
const updateData: any = { ...updates };
delete updateData.applicableUsers; // Remove since we use couponApplicableUsers table
if (updates.discountPercent !== undefined) {
updateData.discountPercent = updates.discountPercent?.toString();
}
if (updates.flatDiscount !== undefined) {
updateData.flatDiscount = updates.flatDiscount?.toString();
}
if (updates.minOrder !== undefined) {
updateData.minOrder = updates.minOrder?.toString();
}
if (updates.maxValue !== undefined) {
updateData.maxValue = updates.maxValue?.toString();
}
if (updates.validTill !== undefined) {
updateData.validTill = updates.validTill ? dayjs(updates.validTill).toDate() : null;
}
/*
// Old implementation - direct DB query:
const result = await db.update(coupons) const result = await db.update(coupons)
.set(updateData) .set(updateData)
.where(eq(coupons.id, id)) .where(eq(coupons.id, id))
@ -271,8 +242,6 @@ export const couponRouter = router({
throw new Error("Coupon not found"); throw new Error("Coupon not found");
} }
console.log('updated coupon successfully')
// Update applicable users: delete existing and insert new // Update applicable users: delete existing and insert new
if (updates.applicableUsers !== undefined) { if (updates.applicableUsers !== undefined) {
await db.delete(couponApplicableUsers).where(eq(couponApplicableUsers.couponId, id)); await db.delete(couponApplicableUsers).where(eq(couponApplicableUsers.couponId, id));
@ -298,246 +267,43 @@ export const couponRouter = router({
); );
} }
} }
*/
return result[0]; return coupon;
}), }),
delete: protectedProcedure delete: protectedProcedure
.input(z.object({ id: z.number() })) .input(z.object({ id: z.number() }))
.mutation(async ({ input }) => { .mutation(async ({ input }): Promise<{ message: string }> => {
const { id } = input; const { id } = input;
const result = await db.update(coupons) await invalidateCouponInDb(id);
.set({ isInvalidated: true })
.where(eq(coupons.id, id))
.returning();
if (result.length === 0) {
throw new Error("Coupon not found");
}
return { message: "Coupon invalidated successfully" }; return { message: "Coupon invalidated successfully" };
}), }),
validate: protectedProcedure validate: protectedProcedure
.input(validateCouponBodySchema) .input(validateCouponBodySchema)
.query(async ({ input }) => { .query(async ({ input }): Promise<CouponValidationResult> => {
const { code, userId, orderAmount } = input; const { code, userId, orderAmount } = input;
if (!code || typeof code !== 'string') { if (!code || typeof code !== 'string') {
return { valid: false, message: "Invalid coupon code" }; return { valid: false, message: "Invalid coupon code" };
} }
const coupon = await db.query.coupons.findFirst({ const result = await validateCouponInDb(code, userId, orderAmount);
where: and(
eq(coupons.couponCode, code.toUpperCase()),
eq(coupons.isInvalidated, false)
),
});
if (!coupon) { return result;
return { valid: false, message: "Coupon not found or invalidated" };
}
// Check expiry date
if (coupon.validTill && new Date(coupon.validTill) < new Date()) {
return { valid: false, message: "Coupon has expired" };
}
// Check if coupon applies to all users or specific user
if (!coupon.isApplyForAll && !coupon.isUserBased) {
return { valid: false, message: "Coupon is not available for use" };
}
// Check minimum order amount
const minOrderValue = coupon.minOrder ? parseFloat(coupon.minOrder) : 0;
if (minOrderValue > 0 && orderAmount < minOrderValue) {
return { valid: false, message: `Minimum order amount is ${minOrderValue}` };
}
// Calculate discount
let discountAmount = 0;
if (coupon.discountPercent) {
const percent = parseFloat(coupon.discountPercent);
discountAmount = (orderAmount * percent) / 100;
} else if (coupon.flatDiscount) {
discountAmount = parseFloat(coupon.flatDiscount);
}
// Apply max value limit
const maxValueLimit = coupon.maxValue ? parseFloat(coupon.maxValue) : 0;
if (maxValueLimit > 0 && discountAmount > maxValueLimit) {
discountAmount = maxValueLimit;
}
return {
valid: true,
discountAmount,
coupon: {
id: coupon.id,
discountPercent: coupon.discountPercent,
flatDiscount: coupon.flatDiscount,
maxValue: coupon.maxValue,
}
};
}),
generateCancellationCoupon: protectedProcedure
.input(
z.object({
orderId: z.number(),
})
)
.mutation(async ({ input, ctx }) => {
const { orderId } = input;
// Get staff user ID from auth middleware
const staffUserId = ctx.staffUser?.id;
if (!staffUserId) {
throw new Error("Unauthorized");
}
// Find the order with user and order status information
const order = await db.query.orders.findFirst({
where: eq(orders.id, orderId),
with: {
user: true,
orderStatus: true,
},
});
if (!order) {
throw new Error("Order not found");
}
// Check if order is cancelled (check if any status entry has isCancelled: true)
// const isOrderCancelled = order.orderStatus?.some(status => status.isCancelled) || false;
// if (!isOrderCancelled) {
// throw new Error("Order is not cancelled");
// }
// // Check if payment method is COD
// if (order.isCod) {
// throw new Error("Can't generate refund coupon for CoD Order");
// }
// Verify user exists
if (!order.user) {
throw new Error("User not found for this order");
}
// Generate coupon code: first 3 letters of user name or mobile + orderId
const userNamePrefix = (order.user.name || order.user.mobile || 'USR').substring(0, 3).toUpperCase();
const couponCode = `${userNamePrefix}${orderId}`;
// Check if coupon code already exists
const existingCoupon = await db.query.coupons.findFirst({
where: eq(coupons.couponCode, couponCode),
});
if (existingCoupon) {
throw new Error("Coupon code already exists");
}
// Get order total amount
const orderAmount = parseFloat(order.totalAmount);
// Calculate expiry date (30 days from now)
const expiryDate = new Date();
expiryDate.setDate(expiryDate.getDate() + 30);
// Create the coupon and update order status in a transaction
const coupon = await db.transaction(async (tx) => {
// Create the coupon
const result = await tx.insert(coupons).values({
couponCode,
isUserBased: true,
flatDiscount: orderAmount.toString(),
minOrder: orderAmount.toString(),
maxValue: orderAmount.toString(),
validTill: expiryDate,
maxLimitForUser: 1,
createdBy: staffUserId,
isApplyForAll: false,
}).returning();
const coupon = result[0];
// Insert applicable users
await tx.insert(couponApplicableUsers).values({
couponId: coupon.id,
userId: order.userId,
});
// Update order_status with refund coupon ID
await tx.update(orderStatus)
.set({ refundCouponId: coupon.id })
.where(eq(orderStatus.orderId, orderId));
return coupon;
});
return coupon;
}),
getReservedCoupons: protectedProcedure
.input(z.object({
cursor: z.number().optional(),
limit: z.number().default(50),
search: z.string().optional(),
}))
.query(async ({ input }) => {
const { cursor, limit, search } = input;
let whereCondition = undefined;
const conditions = [];
if (cursor) {
conditions.push(lt(reservedCoupons.id, cursor));
}
if (search && search.trim()) {
conditions.push(or(
like(reservedCoupons.secretCode, `%${search}%`),
like(reservedCoupons.couponCode, `%${search}%`)
));
}
if (conditions.length > 0) {
whereCondition = and(...conditions);
}
const result = await db.query.reservedCoupons.findMany({
where: whereCondition,
with: {
redeemedUser: true,
creator: true,
},
orderBy: (reservedCoupons, { desc }) => [desc(reservedCoupons.createdAt)],
limit: limit + 1, // Fetch one extra to check if there's more
});
const hasMore = result.length > limit;
const coupons = hasMore ? result.slice(0, limit) : result;
const nextCursor = hasMore ? result[result.length - 1].id : undefined;
return {
coupons,
nextCursor,
};
}), }),
createReservedCoupon: protectedProcedure generateCancellationCoupon: protectedProcedure
.input(createCouponBodySchema) .input(
.mutation(async ({ input, ctx }) => { z.object({
const { couponCode, isUserBased, discountPercent, flatDiscount, minOrder, productIds, applicableUsers, applicableProducts, maxValue, isApplyForAll, validTill, maxLimitForUser, exclusiveApply } = input; orderId: z.number(),
})
// Validation: ensure at least one discount type is provided )
if ((!discountPercent && !flatDiscount) || (discountPercent && flatDiscount)) { .mutation(async ({ input, ctx }): Promise<Coupon> => {
throw new Error("Either discountPercent or flatDiscount must be provided (but not both)"); const { orderId } = input;
}
// For reserved coupons, applicableUsers is not used, as it's redeemed by one user
// Get staff user ID from auth middleware // Get staff user ID from auth middleware
const staffUserId = ctx.staffUser?.id; const staffUserId = ctx.staffUser?.id;
@ -545,21 +311,144 @@ export const couponRouter = router({
throw new Error("Unauthorized"); throw new Error("Unauthorized");
} }
// Generate secret code if not provided (use couponCode as base) // Using dbService helper (new implementation)
const order = await getOrderWithUser(orderId);
if (!order) {
throw new Error("Order not found");
}
if (!order.user) {
throw new Error("User not found for this order");
}
// Generate coupon code: first 3 letters of user name or mobile + orderId
const userNamePrefix = (order.user.name || order.user.mobile || 'USR').substring(0, 3).toUpperCase();
const couponCode = `${userNamePrefix}${orderId}`;
// Check if coupon code already exists
const codeExists = await checkCouponExists(couponCode);
if (codeExists) {
throw new Error("Coupon code already exists");
}
// Get order total amount
const orderAmount = parseFloat(order.totalAmount);
const coupon = await generateCancellationCoupon(
orderId,
staffUserId,
order.userId,
orderAmount,
couponCode
);
/*
// Old implementation - direct DB query with transaction:
const coupon = await db.transaction(async (tx) => {
// Calculate expiry date (30 days from now)
const expiryDate = new Date();
expiryDate.setDate(expiryDate.getDate() + 30);
// Create the coupon
const result = await tx.insert(coupons).values({
couponCode,
isUserBased: true,
flatDiscount: orderAmount.toString(),
minOrder: orderAmount.toString(),
maxValue: orderAmount.toString(),
validTill: expiryDate,
maxLimitForUser: 1,
createdBy: staffUserId,
isApplyForAll: false,
}).returning();
const coupon = result[0];
// Insert applicable users
await tx.insert(couponApplicableUsers).values({
couponId: coupon.id,
userId: order.userId,
});
// Update order_status with refund coupon ID
await tx.update(orderStatus)
.set({ refundCouponId: coupon.id })
.where(eq(orderStatus.orderId, orderId));
return coupon;
});
*/
return coupon;
}),
getReservedCoupons: protectedProcedure
.input(z.object({
cursor: z.number().optional(),
limit: z.number().default(50),
search: z.string().optional(),
}))
.query(async ({ input }): Promise<{ coupons: any[]; nextCursor?: number }> => {
const { cursor, limit, search } = input;
const { coupons: result, hasMore } = await getReservedCouponsFromDb(cursor, limit, search);
const nextCursor = hasMore ? result[result.length - 1].id : undefined;
return {
coupons: result,
nextCursor,
};
}),
createReservedCoupon: protectedProcedure
.input(createCouponBodySchema)
.mutation(async ({ input, ctx }): Promise<any> => {
const { couponCode, isUserBased, discountPercent, flatDiscount, minOrder, productIds, applicableProducts, maxValue, validTill, maxLimitForUser, exclusiveApply } = input;
// Validation: ensure at least one discount type is provided
if ((!discountPercent && !flatDiscount) || (discountPercent && flatDiscount)) {
throw new Error("Either discountPercent or flatDiscount must be provided (but not both)");
}
// Get staff user ID from auth middleware
const staffUserId = ctx.staffUser?.id;
if (!staffUserId) {
throw new Error("Unauthorized");
}
// Generate secret code if not provided
let secretCode = couponCode || `SECRET${Date.now().toString().slice(-6)}${Math.random().toString(36).substring(2, 8).toUpperCase()}`; let secretCode = couponCode || `SECRET${Date.now().toString().slice(-6)}${Math.random().toString(36).substring(2, 8).toUpperCase()}`;
// Check if secret code already exists // Using dbService helper (new implementation)
const existing = await db.query.reservedCoupons.findFirst({ const codeExists = await checkReservedCouponExists(secretCode);
where: eq(reservedCoupons.secretCode, secretCode), if (codeExists) {
});
if (existing) {
throw new Error("Secret code already exists"); throw new Error("Secret code already exists");
} }
const coupon = await createReservedCouponWithProducts(
{
secretCode,
couponCode: couponCode || `RESERVED${Date.now().toString().slice(-6)}`,
discountPercent: discountPercent?.toString(),
flatDiscount: flatDiscount?.toString(),
minOrder: minOrder?.toString(),
productIds,
maxValue: maxValue?.toString(),
validTill: validTill ? dayjs(validTill).toDate() : undefined,
maxLimitForUser,
exclusiveApply: exclusiveApply || false,
createdBy: staffUserId,
},
applicableProducts
);
/*
// Old implementation - direct DB query:
const result = await db.insert(reservedCoupons).values({ const result = await db.insert(reservedCoupons).values({
secretCode, secretCode,
couponCode: couponCode || `RESERVED${Date.now().toString().slice(-6)}`, couponCode: couponCode || RESERVED${Date.now().toString().slice(-6)},
discountPercent: discountPercent?.toString(), discountPercent: discountPercent?.toString(),
flatDiscount: flatDiscount?.toString(), flatDiscount: flatDiscount?.toString(),
minOrder: minOrder?.toString(), minOrder: minOrder?.toString(),
@ -582,6 +471,7 @@ export const couponRouter = router({
})) }))
); );
} }
*/
return coupon; return coupon;
}), }),
@ -592,120 +482,97 @@ export const couponRouter = router({
limit: z.number().min(1).max(50).default(20), limit: z.number().min(1).max(50).default(20),
offset: z.number().min(0).default(0), offset: z.number().min(0).default(0),
})) }))
.query(async ({ input }) => { .query(async ({ input }): Promise<{ users: UserMiniInfo[] }> => {
const { search, limit } = input; const { search, limit, offset } = input;
let whereCondition = undefined; const result = await getUsersForCouponFromDb(search, limit, offset);
if (search && search.trim()) {
whereCondition = or(
like(users.name, `%${search}%`),
like(users.mobile, `%${search}%`)
);
}
const userList = await db.query.users.findMany({ return result;
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 => ({
id: user.id,
name: user.name || 'Unknown',
mobile: user.mobile,
}))
};
}), }),
createCoupon: protectedProcedure createCoupon: protectedProcedure
.input(z.object({ .input(z.object({
mobile: z.string().min(1, 'Mobile number is required'), mobile: z.string().min(1, 'Mobile number is required'),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<{ success: boolean; coupon: any }> => {
const { mobile } = input; const { mobile } = input;
// Get staff user ID from auth middleware // Get staff user ID from auth middleware
const staffUserId = ctx.staffUser?.id; const staffUserId = ctx.staffUser?.id;
if (!staffUserId) { if (!staffUserId) {
throw new Error("Unauthorized"); throw new Error("Unauthorized");
} }
// Clean mobile number (remove non-digits) // Clean mobile number (remove non-digits)
const cleanMobile = mobile.replace(/\D/g, ''); const cleanMobile = mobile.replace(/\D/g, '');
// Validate: exactly 10 digits // Validate: exactly 10 digits
if (cleanMobile.length !== 10) { if (cleanMobile.length !== 10) {
throw new Error("Mobile number must be exactly 10 digits"); throw new Error("Mobile number must be exactly 10 digits");
} }
// Check if user exists, create if not // Generate unique coupon code
let user = await db.query.users.findFirst({ const timestamp = Date.now().toString().slice(-6);
where: eq(users.mobile, cleanMobile), const random = Math.random().toString(36).substring(2, 6).toUpperCase();
}); const couponCode = `MF${cleanMobile.slice(-4)}${timestamp}${random}`;
if (!user) { // Using dbService helper (new implementation)
// Create new user const codeExists = await checkCouponExists(couponCode);
const [newUser] = await db.insert(users).values({ if (codeExists) {
name: null, throw new Error("Generated coupon code already exists - please try again");
email: null, }
mobile: cleanMobile,
}).returning();
user = newUser;
}
// Generate unique coupon code const { coupon, user } = await createCouponForUser(cleanMobile, couponCode, staffUserId);
const timestamp = Date.now().toString().slice(-6);
const random = Math.random().toString(36).substring(2, 6).toUpperCase();
const couponCode = `MF${cleanMobile.slice(-4)}${timestamp}${random}`;
// Check if coupon code already exists (very unlikely but safe) /*
const existingCode = await db.query.coupons.findFirst({ // Old implementation - direct DB query with transaction:
where: eq(coupons.couponCode, couponCode), // Check if user exists, create if not
}); let user = await db.query.users.findFirst({
where: eq(users.mobile, cleanMobile),
});
if (existingCode) { if (!user) {
throw new Error("Generated coupon code already exists - please try again"); const [newUser] = await db.insert(users).values({
} name: null,
email: null,
// Create the coupon mobile: cleanMobile,
const [coupon] = await db.insert(coupons).values({
couponCode,
isUserBased: true,
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(), // 90 days from now
}).returning(); }).returning();
user = newUser;
}
// Associate coupon with user // Create the coupon
await db.insert(couponApplicableUsers).values({ const [coupon] = await db.insert(coupons).values({
couponId: coupon.id, couponCode,
isUserBased: true,
discountPercent: "20",
minOrder: "1000",
maxValue: "500",
maxLimitForUser: 1,
isApplyForAll: false,
exclusiveApply: false,
createdBy: staffUserId,
validTill: dayjs().add(90, 'days').toDate(),
}).returning();
// Associate coupon with user
await db.insert(couponApplicableUsers).values({
couponId: coupon.id,
userId: user.id,
});
*/
return {
success: true,
coupon: {
id: coupon.id,
couponCode: coupon.couponCode,
userId: user.id, userId: user.id,
}); userMobile: user.mobile,
discountPercent: 20,
return { minOrder: 1000,
success: true, maxValue: 500,
coupon: { maxLimitForUser: 1,
id: coupon.id, },
couponCode: coupon.couponCode, };
userId: user.id, }),
userMobile: user.mobile,
discountPercent: 20,
minOrder: 1000,
maxValue: 500,
maxLimitForUser: 1,
},
};
}),
}); });

View file

@ -1,21 +1,5 @@
import { router, protectedProcedure } from "@/src/trpc/trpc-index" import { router, protectedProcedure } from "@/src/trpc/trpc-index"
import { z } from "zod"; import { z } from "zod";
import { db } from "@/src/db/db_index"
import {
orders,
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 { ApiError } from "@/src/lib/api-error"
import { import {
sendOrderPackagedNotification, sendOrderPackagedNotification,
@ -23,16 +7,38 @@ import {
} from "@/src/lib/notif-job"; } from "@/src/lib/notif-job";
import { publishCancellation } from "@/src/lib/post-order-handler" import { publishCancellation } from "@/src/lib/post-order-handler"
import { getMultipleUserNegativityScores } from "@/src/stores/user-negativity-store" import { getMultipleUserNegativityScores } from "@/src/stores/user-negativity-store"
import {
updateOrderNotes as updateOrderNotesInDb,
getOrderDetails as getOrderDetailsInDb,
updateOrderPackaged as updateOrderPackagedInDb,
updateOrderDelivered as updateOrderDeliveredInDb,
updateOrderItemPackaging as updateOrderItemPackagingInDb,
removeDeliveryCharge as removeDeliveryChargeInDb,
getSlotOrders as getSlotOrdersInDb,
updateAddressCoords as updateAddressCoordsInDb,
getAllOrders as getAllOrdersInDb,
rebalanceSlots as rebalanceSlotsInDb,
cancelOrder as cancelOrderInDb,
deleteOrderById as deleteOrderByIdInDb,
} from '@/src/dbService'
import type {
AdminCancelOrderResult,
AdminGetAllOrdersResult,
AdminGetSlotOrdersResult,
AdminOrderBasicResult,
AdminOrderDetails,
AdminOrderItemPackagingResult,
AdminOrderMessageResult,
AdminOrderRow,
AdminOrderUpdateResult,
AdminRebalanceSlotsResult,
} from "@packages/shared"
const updateOrderNotesSchema = z.object({ const updateOrderNotesSchema = z.object({
orderId: z.number(), orderId: z.number(),
adminNotes: z.string(), adminNotes: z.string(),
}); });
const getFullOrderSchema = z.object({
orderId: z.number(),
});
const getOrderDetailsSchema = z.object({ const getOrderDetailsSchema = z.object({
orderId: z.number(), orderId: z.number(),
}); });
@ -57,10 +63,6 @@ const getSlotOrdersSchema = z.object({
slotId: z.string(), slotId: z.string(),
}); });
const getTodaysOrdersSchema = z.object({
slotId: z.string().optional(),
});
const getAllOrdersSchema = z.object({ const getAllOrdersSchema = z.object({
cursor: z.number().optional(), cursor: z.number().optional(),
limit: z.number().default(20), limit: z.number().default(20),
@ -86,9 +88,13 @@ const getAllOrdersSchema = z.object({
export const orderRouter = router({ export const orderRouter = router({
updateNotes: protectedProcedure updateNotes: protectedProcedure
.input(updateOrderNotesSchema) .input(updateOrderNotesSchema)
.mutation(async ({ input }) => { .mutation(async ({ input }): Promise<AdminOrderRow> => {
const { orderId, adminNotes } = input; const { orderId, adminNotes } = input;
const result = await updateOrderNotesInDb(orderId, adminNotes || null)
/*
// Old implementation - direct DB query:
const result = await db const result = await db
.update(orders) .update(orders)
.set({ .set({
@ -100,125 +106,24 @@ export const orderRouter = router({
if (result.length === 0) { if (result.length === 0) {
throw new Error("Order not found"); throw new Error("Order not found");
} }
*/
return result[0]; if (!result) {
}), throw new Error("Order not found")
getFullOrder: protectedProcedure
.input(getFullOrderSchema)
.query(async ({ input }) => {
const { orderId } = input;
const orderData = await db.query.orders.findFirst({
where: eq(orders.id, orderId),
with: {
user: true,
address: true,
slot: true,
orderItems: {
with: {
product: {
with: {
unit: true,
},
},
},
},
payment: true,
paymentInfo: true,
},
});
if (!orderData) {
throw new Error("Order not found");
} }
// Get order status separately return result as AdminOrderRow;
const statusRecord = await db.query.orderStatus.findFirst({
where: eq(orderStatus.orderId, orderId),
});
let status: "pending" | "delivered" | "cancelled" = "pending";
if (statusRecord?.isCancelled) {
status = "cancelled";
} else if (statusRecord?.isDelivered) {
status = "delivered";
}
// Get refund details if order is cancelled
let refund = null;
if (status === "cancelled") {
refund = await db.query.refunds.findFirst({
where: eq(refunds.orderId, orderId),
});
}
return {
id: orderData.id,
readableId: orderData.id,
customerName: `${orderData.user.name}`,
customerEmail: orderData.user.email,
customerMobile: orderData.user.mobile,
address: {
line1: orderData.address.addressLine1,
line2: orderData.address.addressLine2,
city: orderData.address.city,
state: orderData.address.state,
pincode: orderData.address.pincode,
phone: orderData.address.phone,
},
slotInfo: orderData.slot
? {
time: orderData.slot.deliveryTime.toISOString(),
sequence: orderData.slot.deliverySequence,
}
: null,
isCod: orderData.isCod,
isOnlinePayment: orderData.isOnlinePayment,
totalAmount: orderData.totalAmount,
adminNotes: orderData.adminNotes,
userNotes: orderData.userNotes,
createdAt: orderData.createdAt,
status,
isPackaged:
orderData.orderItems.every((item) => item.is_packaged) || false,
isDelivered: statusRecord?.isDelivered || false,
items: orderData.orderItems.map((item) => ({
id: item.id,
name: item.product.name,
quantity: item.quantity,
price: item.price,
unit: item.product.unit?.shortNotation,
amount:
parseFloat(item.price.toString()) *
parseFloat(item.quantity || "0"),
})),
payment: orderData.payment
? {
status: orderData.payment.status,
gateway: orderData.payment.gateway,
merchantOrderId: orderData.payment.merchantOrderId,
}
: null,
paymentInfo: orderData.paymentInfo
? {
status: orderData.paymentInfo.status,
gateway: orderData.paymentInfo.gateway,
merchantOrderId: orderData.paymentInfo.merchantOrderId,
}
: null,
// Cancellation details (only present for cancelled orders)
cancelReason: statusRecord?.cancelReason || null,
cancellationReviewed: statusRecord?.cancellationReviewed || false,
isRefundDone: refund?.refundStatus === "processed" || false,
};
}), }),
getOrderDetails: protectedProcedure getOrderDetails: protectedProcedure
.input(getOrderDetailsSchema) .input(getOrderDetailsSchema)
.query(async ({ input }) => { .query(async ({ input }): Promise<AdminOrderDetails> => {
const { orderId } = input; const { orderId } = input;
const orderDetails = await getOrderDetailsInDb(orderId)
/*
// Old implementation - direct DB queries:
// Single optimized query with all relations // Single optimized query with all relations
const orderData = await db.query.orders.findFirst({ const orderData = await db.query.orders.findFirst({
where: eq(orders.id, orderId), where: eq(orders.id, orderId),
@ -237,8 +142,8 @@ export const orderRouter = router({
}, },
payment: true, payment: true,
paymentInfo: true, paymentInfo: true,
orderStatus: true, // Include in main query orderStatus: true,
refunds: true, // Include in main query refunds: true,
}, },
}); });
@ -248,7 +153,7 @@ export const orderRouter = router({
// Get coupon usage for this specific order using new orderId field // Get coupon usage for this specific order using new orderId field
const couponUsageData = await db.query.couponUsage.findMany({ const couponUsageData = await db.query.couponUsage.findMany({
where: eq(couponUsage.orderId, orderData.id), // Use new orderId field where: eq(couponUsage.orderId, orderData.id),
with: { with: {
coupon: true, coupon: true,
}, },
@ -380,13 +285,24 @@ export const orderRouter = router({
refundRecord: refund, refundRecord: refund,
isFlashDelivery: orderData.isFlashDelivery, isFlashDelivery: orderData.isFlashDelivery,
}; };
*/
if (!orderDetails) {
throw new Error('Order not found')
}
return orderDetails
}), }),
updatePackaged: protectedProcedure updatePackaged: protectedProcedure
.input(updatePackagedSchema) .input(updatePackagedSchema)
.mutation(async ({ input }) => { .mutation(async ({ input }): Promise<AdminOrderUpdateResult> => {
const { orderId, isPackaged } = input; const { orderId, isPackaged } = input;
const result = await updateOrderPackagedInDb(orderId, isPackaged)
/*
// Old implementation - direct DB queries:
// Update all order items to the specified packaged state // Update all order items to the specified packaged state
await db await db
.update(orderItems) .update(orderItems)
@ -412,13 +328,22 @@ export const orderRouter = router({
if (order) await sendOrderPackagedNotification(order.userId, orderId); if (order) await sendOrderPackagedNotification(order.userId, orderId);
return { success: true }; return { success: true };
*/
if (result.userId) await sendOrderPackagedNotification(result.userId, orderId)
return { success: true, userId: result.userId }
}), }),
updateDelivered: protectedProcedure updateDelivered: protectedProcedure
.input(updateDeliveredSchema) .input(updateDeliveredSchema)
.mutation(async ({ input }) => { .mutation(async ({ input }): Promise<AdminOrderUpdateResult> => {
const { orderId, isDelivered } = input; const { orderId, isDelivered } = input;
const result = await updateOrderDeliveredInDb(orderId, isDelivered)
/*
// Old implementation - direct DB queries:
await db await db
.update(orderStatus) .update(orderStatus)
.set({ isDelivered }) .set({ isDelivered })
@ -430,13 +355,22 @@ export const orderRouter = router({
if (order) await sendOrderDeliveredNotification(order.userId, orderId); if (order) await sendOrderDeliveredNotification(order.userId, orderId);
return { success: true }; return { success: true };
*/
if (result.userId) await sendOrderDeliveredNotification(result.userId, orderId)
return { success: true, userId: result.userId }
}), }),
updateOrderItemPackaging: protectedProcedure updateOrderItemPackaging: protectedProcedure
.input(updateOrderItemPackagingSchema) .input(updateOrderItemPackagingSchema)
.mutation(async ({ input }) => { .mutation(async ({ input }): Promise<AdminOrderItemPackagingResult> => {
const { orderItemId, isPackaged, isPackageVerified } = input; const { orderItemId, isPackaged, isPackageVerified } = input;
const result = await updateOrderItemPackagingInDb(orderItemId, isPackaged, isPackageVerified)
/*
// Old implementation - direct DB queries:
// Validate that orderItem exists // Validate that orderItem exists
const orderItem = await db.query.orderItems.findFirst({ const orderItem = await db.query.orderItems.findFirst({
where: eq(orderItems.id, orderItemId), where: eq(orderItems.id, orderItemId),
@ -462,13 +396,24 @@ export const orderRouter = router({
.where(eq(orderItems.id, orderItemId)); .where(eq(orderItems.id, orderItemId));
return { success: true }; return { success: true };
*/
if (!result.updated) {
throw new ApiError('Order item not found', 404)
}
return result
}), }),
removeDeliveryCharge: protectedProcedure removeDeliveryCharge: protectedProcedure
.input(z.object({ orderId: z.number() })) .input(z.object({ orderId: z.number() }))
.mutation(async ({ input }) => { .mutation(async ({ input }): Promise<AdminOrderMessageResult> => {
const { orderId } = input; const { orderId } = input;
const result = await removeDeliveryChargeInDb(orderId)
/*
// Old implementation - direct DB queries:
const order = await db.query.orders.findFirst({ const order = await db.query.orders.findFirst({
where: eq(orders.id, orderId), where: eq(orders.id, orderId),
}); });
@ -490,13 +435,24 @@ export const orderRouter = router({
.where(eq(orders.id, orderId)); .where(eq(orders.id, orderId));
return { success: true, message: 'Delivery charge removed' }; return { success: true, message: 'Delivery charge removed' };
*/
if (!result) {
throw new Error('Order not found')
}
return result
}), }),
getSlotOrders: protectedProcedure getSlotOrders: protectedProcedure
.input(getSlotOrdersSchema) .input(getSlotOrdersSchema)
.query(async ({ input }) => { .query(async ({ input }): Promise<AdminGetSlotOrdersResult> => {
const { slotId } = input; const { slotId } = input;
const result = await getSlotOrdersInDb(slotId)
/*
// Old implementation - direct DB queries:
const slotOrders = await db.query.orders.findMany({ const slotOrders = await db.query.orders.findMany({
where: eq(orders.slotId, parseInt(slotId)), where: eq(orders.slotId, parseInt(slotId)),
with: { with: {
@ -573,97 +529,9 @@ export const orderRouter = router({
}); });
return { success: true, data: formattedOrders }; return { success: true, data: formattedOrders };
}), */
getTodaysOrders: protectedProcedure return result
.input(getTodaysOrdersSchema)
.query(async ({ input }) => {
const { slotId } = input;
const start = dayjs().startOf("day").toDate();
const end = dayjs().endOf("day").toDate();
let whereCondition = and(
gte(orders.createdAt, start),
lt(orders.createdAt, end)
);
if (slotId) {
whereCondition = and(
whereCondition,
eq(orders.slotId, parseInt(slotId))
);
}
const todaysOrders = await db.query.orders.findMany({
where: whereCondition,
with: {
user: true,
address: true,
slot: true,
orderItems: {
with: {
product: {
with: {
unit: true,
},
},
},
},
orderStatus: true,
},
});
const filteredOrders = todaysOrders.filter((order) => {
const statusRecord = order.orderStatus[0];
return (
order.isCod ||
(statusRecord && statusRecord.paymentStatus === "success")
);
});
const formattedOrders = filteredOrders.map((order) => {
const statusRecord = order.orderStatus[0]; // assuming one status per order
let status: "pending" | "delivered" | "cancelled" = "pending";
if (statusRecord?.isCancelled) {
status = "cancelled";
} else if (statusRecord?.isDelivered) {
status = "delivered";
}
const items = order.orderItems.map((item) => ({
name: item.product.name,
quantity: parseFloat(item.quantity),
price: parseFloat(item.price.toString()),
amount: parseFloat(item.quantity) * parseFloat(item.price.toString()),
unit: item.product.unit?.shortNotation || "",
}));
return {
orderId: order.id.toString(),
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: parseFloat(order.totalAmount),
items,
deliveryTime: order.slot?.deliveryTime.toISOString() || null,
status,
isPackaged:
order.orderItems.every((item) => item.is_packaged) || false,
isDelivered: statusRecord?.isDelivered || false,
isCod: order.isCod,
paymentMode: order.isCod ? "COD" : "Online",
paymentStatus: statusRecord?.paymentStatus || "pending",
slotId: order.slotId,
adminNotes: order.adminNotes,
userNotes: order.userNotes,
};
});
return { success: true, data: formattedOrders };
}), }),
updateAddressCoords: protectedProcedure updateAddressCoords: protectedProcedure
@ -674,9 +542,13 @@ export const orderRouter = router({
longitude: z.number(), longitude: z.number(),
}) })
) )
.mutation(async ({ input }) => { .mutation(async ({ input }): Promise<AdminOrderBasicResult> => {
const { addressId, latitude, longitude } = input; const { addressId, latitude, longitude } = input;
const result = await updateAddressCoordsInDb(addressId, latitude, longitude)
/*
// Old implementation - direct DB queries:
const result = await db const result = await db
.update(addresses) .update(addresses)
.set({ .set({
@ -691,12 +563,33 @@ export const orderRouter = router({
} }
return { success: true }; return { success: true };
*/
if (!result.success) {
throw new ApiError('Address not found', 404)
}
return result
}), }),
getAll: protectedProcedure getAll: protectedProcedure
.input(getAllOrdersSchema) .input(getAllOrdersSchema)
.query(async ({ input }) => { .query(async ({ input }): Promise<AdminGetAllOrdersResult | undefined> => {
try { try {
const result = await getAllOrdersInDb(input)
const userIds = [...new Set(result.orders.map((order) => order.userId))]
const negativityScores = await getMultipleUserNegativityScores(userIds)
const orders = result.orders.map((order) => {
const { userId, userNegativityScore, ...rest } = order
return {
...rest,
userNegativityScore: negativityScores[userId] || 0,
}
})
/*
// Old implementation - direct DB queries:
const { const {
cursor, cursor,
limit, limit,
@ -858,6 +751,12 @@ export const orderRouter = router({
? ordersToReturn[ordersToReturn.length - 1].id ? ordersToReturn[ordersToReturn.length - 1].id
: undefined, : undefined,
}; };
*/
return {
orders,
nextCursor: result.nextCursor,
}
} catch (e) { } catch (e) {
console.log({ e }); console.log({ e });
} }
@ -865,9 +764,13 @@ export const orderRouter = router({
rebalanceSlots: protectedProcedure rebalanceSlots: protectedProcedure
.input(z.object({ slotIds: z.array(z.number()).min(1).max(50) })) .input(z.object({ slotIds: z.array(z.number()).min(1).max(50) }))
.mutation(async ({ input }) => { .mutation(async ({ input }): Promise<AdminRebalanceSlotsResult> => {
const slotIds = input.slotIds; const slotIds = input.slotIds;
const result = await rebalanceSlotsInDb(slotIds)
/*
// Old implementation - direct DB queries:
const ordersList = await db.query.orders.findMany({ const ordersList = await db.query.orders.findMany({
where: inArray(orders.slotId, slotIds), where: inArray(orders.slotId, slotIds),
with: { with: {
@ -936,6 +839,9 @@ export const orderRouter = router({
}); });
return { success: true, updatedOrders: updatedOrderIds, message: `Rebalanced ${updatedOrderIds.length} orders.` }; return { success: true, updatedOrders: updatedOrderIds, message: `Rebalanced ${updatedOrderIds.length} orders.` };
*/
return result
}), }),
cancelOrder: protectedProcedure cancelOrder: protectedProcedure
@ -943,9 +849,13 @@ export const orderRouter = router({
orderId: z.number(), orderId: z.number(),
reason: z.string().min(1, "Cancellation reason is required"), reason: z.string().min(1, "Cancellation reason is required"),
})) }))
.mutation(async ({ input }) => { .mutation(async ({ input }): Promise<AdminCancelOrderResult> => {
const { orderId, reason } = input; const { orderId, reason } = input;
const result = await cancelOrderInDb(orderId, reason)
/*
// Old implementation - direct DB queries:
const order = await db.query.orders.findFirst({ const order = await db.query.orders.findFirst({
where: eq(orders.id, orderId), where: eq(orders.id, orderId),
with: { with: {
@ -997,14 +907,40 @@ export const orderRouter = router({
await publishCancellation(result.orderId, 'admin', reason); await publishCancellation(result.orderId, 'admin', reason);
return { success: true, message: "Order cancelled successfully" }; return { success: true, message: "Order cancelled successfully" };
*/
if (!result.success) {
if (result.error === 'order_not_found') {
throw new ApiError(result.message, 404)
}
if (result.error === 'status_not_found') {
throw new ApiError(result.message, 400)
}
if (result.error === 'already_cancelled') {
throw new ApiError(result.message, 400)
}
if (result.error === 'already_delivered') {
throw new ApiError(result.message, 400)
}
throw new ApiError(result.message, 400)
}
if (result.orderId) {
await publishCancellation(result.orderId, 'admin', reason)
}
return { success: true, message: result.message }
}), }),
}); });
// {"id": "order_Rhh00qJNdjUp8o", "notes": {"retry": "true", "customerOrderId": "14"}, "amount": 21000, "entity": "order", "status": "created", "receipt": "order_14_retry", "attempts": 0, "currency": "INR", "offer_id": null, "signature": "6df20655021f1d6841340f2a2ef2ef9378cb3d43495ab09e85f08aea1a851583", "amount_due": 21000, "created_at": 1763575791, "payment_id": "pay_Rhh15cLL28YM7j", "amount_paid": 0} // {"id": "order_Rhh00qJNdjUp8o", "notes": {"retry": "true", "customerOrderId": "14"}, "amount": 21000, "entity": "order", "status": "created", "receipt": "order_14_retry", "attempts": 0, "currency": "INR", "offer_id": null, "signature": "6df20655021f1d6841340f2a2ef2ef9378cb3d43495ab09e85f08aea1a851583", "amount_due": 21000, "created_at": 1763575791, "payment_id": "pay_Rhh15cLL28YM7j", "amount_paid": 0}
type RefundStatus = "success" | "pending" | "failed" | "none" | "na";
export async function deleteOrderById(orderId: number): Promise<void> { export async function deleteOrderById(orderId: number): Promise<void> {
await deleteOrderByIdInDb(orderId)
/*
// Old implementation - direct DB queries:
await db.transaction(async (tx) => { await db.transaction(async (tx) => {
await tx.delete(orderItems).where(eq(orderItems.orderId, orderId)); await tx.delete(orderItems).where(eq(orderItems.orderId, orderId));
await tx.delete(orderStatus).where(eq(orderStatus.orderId, orderId)); await tx.delete(orderStatus).where(eq(orderStatus.orderId, orderId));
@ -1014,5 +950,5 @@ export async function deleteOrderById(orderId: number): Promise<void> {
await tx.delete(complaints).where(eq(complaints.orderId, orderId)); await tx.delete(complaints).where(eq(complaints.orderId, orderId));
await tx.delete(orders).where(eq(orders.id, orderId)); await tx.delete(orders).where(eq(orders.id, orderId));
}); });
*/
} }

View file

@ -1,15 +1,5 @@
import { router, protectedProcedure } from "@/src/trpc/trpc-index" import { router, protectedProcedure } from "@/src/trpc/trpc-index"
import { z } from "zod"; import { z } from "zod";
import { db } from "@/src/db/db_index"
import {
orders,
orderStatus,
payments,
refunds,
} from "@/src/db/schema";
import { and, eq } from "drizzle-orm";
import { ApiError } from "@/src/lib/api-error"
import { RazorpayPaymentService } from "@/src/lib/payments-utils"
const initiateRefundSchema = z const initiateRefundSchema = z
.object({ .object({
@ -33,114 +23,6 @@ export const adminPaymentsRouter = router({
initiateRefund: protectedProcedure initiateRefund: protectedProcedure
.input(initiateRefundSchema) .input(initiateRefundSchema)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
try { return {}
const { orderId, refundPercent, refundAmount } = input;
// Validate order exists
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 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")
}
if (
!orderStatusRecord ||
(orderStatusRecord.paymentStatus !== "success" &&
!(order.isCod && orderStatusRecord.isDelivered))
) {
throw new ApiError("Order payment not verified or not eligible for refund", 400);
}
// Calculate refund amount
let calculatedRefundAmount: number;
if (refundPercent !== undefined) {
calculatedRefundAmount =
(parseFloat(order.totalAmount) * refundPercent) / 100;
} else if (refundAmount !== undefined) {
calculatedRefundAmount = refundAmount;
if (calculatedRefundAmount > parseFloat(order.totalAmount)) {
throw new ApiError("Refund amount cannot exceed order total", 400);
}
} else {
throw new ApiError("Invalid refund parameters", 400);
}
let razorpayRefund = null;
let merchantRefundId = null;
// Get payment record for online payments
const payment = await db.query.payments.findFirst({
where: and(
eq(payments.orderId, orderId),
eq(payments.status, "success")
),
});
if (!payment || payment.status !== "success") {
throw new ApiError("Payment not found or not successful", 404);
}
const payload = payment.payload as any;
// Initiate Razorpay refund
razorpayRefund = await RazorpayPaymentService.initiateRefund(
payload.payment_id,
Math.round(calculatedRefundAmount * 100) // Convert to paisa
);
merchantRefundId = razorpayRefund.id;
// Check if refund already exists for this order
const existingRefund = await db.query.refunds.findFirst({
where: eq(refunds.orderId, orderId),
});
const refundStatus = "initiated";
if (existingRefund) {
// Update existing refund
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 db
.insert(refunds)
.values({
orderId,
refundAmount: calculatedRefundAmount.toString(),
refundStatus,
merchantRefundId,
});
}
return {
refundId: merchantRefundId || `cod_${orderId}`,
amount: calculatedRefundAmount,
status: refundStatus,
message: order.isCod ? "COD refund processed successfully" : "Refund initiated successfully",
};
}
catch(e) {
console.log(e);
throw new ApiError("Failed to initiate refund")
}
}), }),
}); });

View file

@ -1,23 +1,47 @@
import { router, protectedProcedure } from '@/src/trpc/trpc-index' import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'; import { z } from 'zod'
import { db } from '@/src/db/db_index'
import { productInfo, units, specialDeals, productSlots, productTags, productReviews, users, productGroupInfo, productGroupMembership } from '@/src/db/schema'
import { eq, and, inArray, desc, sql } from 'drizzle-orm';
import { ApiError } from '@/src/lib/api-error' import { ApiError } from '@/src/lib/api-error'
import { imageUploadS3, generateSignedUrlsFromS3Urls, getOriginalUrlFromSignedUrl, claimUploadUrl } from '@/src/lib/s3-client' import { generateSignedUrlsFromS3Urls, claimUploadUrl } from '@/src/lib/s3-client'
import { deleteS3Image } from '@/src/lib/delete-image' import { scheduleStoreInitialization } from '@/src/stores/store-initializer'
import type { SpecialDeal } from '@/src/db/types' import {
import { initializeAllStores } from '@/src/stores/store-initializer' getAllProducts as getAllProductsInDb,
getProductById as getProductByIdInDb,
deleteProduct as deleteProductInDb,
toggleProductOutOfStock as toggleProductOutOfStockInDb,
updateSlotProducts as updateSlotProductsInDb,
getSlotProductIds as getSlotProductIdsInDb,
getSlotsProductIds as getSlotsProductIdsInDb,
getProductReviews as getProductReviewsInDb,
respondToReview as respondToReviewInDb,
getAllProductGroups as getAllProductGroupsInDb,
createProductGroup as createProductGroupInDb,
updateProductGroup as updateProductGroupInDb,
deleteProductGroup as deleteProductGroupInDb,
updateProductPrices as updateProductPricesInDb,
} from '@/src/dbService'
import type {
AdminProductGroupsResult,
AdminProductGroupResponse,
AdminProductReviewsResult,
AdminProductReviewResponse,
AdminProductListResponse,
AdminProductResponse,
AdminDeleteProductResult,
AdminToggleOutOfStockResult,
AdminUpdateSlotProductsResult,
AdminSlotProductIdsResult,
AdminSlotsProductIdsResult,
AdminUpdateProductPricesResult,
} from '@packages/shared'
type CreateDeal = {
quantity: number;
price: number;
validTill: string;
};
export const productRouter = router({ export const productRouter = router({
getProducts: protectedProcedure getProducts: protectedProcedure
.query(async ({ ctx }) => { .query(async (): Promise<AdminProductListResponse> => {
const products = await getAllProductsInDb()
/*
// Old implementation - direct DB query:
const products = await db.query.productInfo.findMany({ const products = await db.query.productInfo.findMany({
orderBy: productInfo.name, orderBy: productInfo.name,
with: { with: {
@ -25,28 +49,32 @@ export const productRouter = router({
store: true, store: true,
}, },
}); });
*/
// Generate signed URLs for all product images
const productsWithSignedUrls = await Promise.all( const productsWithSignedUrls = await Promise.all(
products.map(async (product) => ({ products.map(async (product) => ({
...product, ...product,
images: await generateSignedUrlsFromS3Urls((product.images as string[]) || []), images: await generateSignedUrlsFromS3Urls((product.images as string[]) || []),
})) }))
); )
return { return {
products: productsWithSignedUrls, products: productsWithSignedUrls,
count: productsWithSignedUrls.length, count: productsWithSignedUrls.length,
}; }
}), }),
getProductById: protectedProcedure getProductById: protectedProcedure
.input(z.object({ .input(z.object({
id: z.number(), id: z.number(),
})) }))
.query(async ({ input, ctx }) => { .query(async ({ input }): Promise<AdminProductResponse> => {
const { id } = input; const { id } = input;
const product = await getProductByIdInDb(id)
/*
// Old implementation - direct DB queries:
const product = await db.query.productInfo.findFirst({ const product = await db.query.productInfo.findFirst({
where: eq(productInfo.id, id), where: eq(productInfo.id, id),
with: { with: {
@ -83,15 +111,33 @@ export const productRouter = router({
return { return {
product: productWithSignedUrls, product: productWithSignedUrls,
}; };
*/
if (!product) {
throw new ApiError('Product not found', 404)
}
const productWithSignedUrls = {
...product,
images: await generateSignedUrlsFromS3Urls((product.images as string[]) || []),
}
return {
product: productWithSignedUrls,
}
}), }),
deleteProduct: protectedProcedure deleteProduct: protectedProcedure
.input(z.object({ .input(z.object({
id: z.number(), id: z.number(),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input }): Promise<AdminDeleteProductResult> => {
const { id } = input; const { id } = input;
const deletedProduct = await deleteProductInDb(id)
/*
// Old implementation - direct DB query:
const [deletedProduct] = await db const [deletedProduct] = await db
.delete(productInfo) .delete(productInfo)
.where(eq(productInfo.id, id)) .where(eq(productInfo.id, id))
@ -100,22 +146,31 @@ export const productRouter = router({
if (!deletedProduct) { if (!deletedProduct) {
throw new ApiError("Product not found", 404); throw new ApiError("Product not found", 404);
} }
*/
if (!deletedProduct) {
throw new ApiError('Product not found', 404)
}
// Reinitialize stores to reflect changes // Reinitialize stores to reflect changes
await initializeAllStores(); scheduleStoreInitialization()
return { return {
message: "Product deleted successfully", message: 'Product deleted successfully',
}; }
}), }),
toggleOutOfStock: protectedProcedure toggleOutOfStock: protectedProcedure
.input(z.object({ .input(z.object({
id: z.number(), id: z.number(),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input }): Promise<AdminToggleOutOfStockResult> => {
const { id } = input; const { id } = input;
const updatedProduct = await toggleProductOutOfStockInDb(id)
/*
// Old implementation - direct DB queries:
const product = await db.query.productInfo.findFirst({ const product = await db.query.productInfo.findFirst({
where: eq(productInfo.id, id), where: eq(productInfo.id, id),
}); });
@ -131,14 +186,18 @@ export const productRouter = router({
}) })
.where(eq(productInfo.id, id)) .where(eq(productInfo.id, id))
.returning(); .returning();
*/
// Reinitialize stores to reflect changes if (!updatedProduct) {
await initializeAllStores(); throw new ApiError('Product not found', 404)
}
scheduleStoreInitialization()
return { return {
product: updatedProduct, product: updatedProduct,
message: `Product marked as ${updatedProduct.isOutOfStock ? 'out of stock' : 'in stock'}`, message: `Product marked as ${updatedProduct.isOutOfStock ? 'out of stock' : 'in stock'}`,
}; }
}), }),
updateSlotProducts: protectedProcedure updateSlotProducts: protectedProcedure
@ -146,13 +205,17 @@ export const productRouter = router({
slotId: z.string(), slotId: z.string(),
productIds: z.array(z.string()), productIds: z.array(z.string()),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input }): Promise<AdminUpdateSlotProductsResult> => {
const { slotId, productIds } = input; const { slotId, productIds } = input;
if (!Array.isArray(productIds)) { if (!Array.isArray(productIds)) {
throw new ApiError("productIds must be an array", 400); throw new ApiError("productIds must be an array", 400);
} }
const result = await updateSlotProductsInDb(slotId, productIds)
/*
// Old implementation - direct DB queries:
// Get current associations // Get current associations
const currentAssociations = await db.query.productSlots.findMany({ const currentAssociations = await db.query.productSlots.findMany({
where: eq(productSlots.slotId, parseInt(slotId)), where: eq(productSlots.slotId, parseInt(slotId)),
@ -189,22 +252,35 @@ export const productRouter = router({
} }
// Reinitialize stores to reflect changes // Reinitialize stores to reflect changes
await initializeAllStores(); scheduleStoreInitialization()
return { return {
message: "Slot products updated successfully", message: "Slot products updated successfully",
added: productsToAdd.length, added: productsToAdd.length,
removed: productsToRemove.length, removed: productsToRemove.length,
}; };
*/
scheduleStoreInitialization()
return {
message: 'Slot products updated successfully',
added: result.added,
removed: result.removed,
}
}), }),
getSlotProductIds: protectedProcedure getSlotProductIds: protectedProcedure
.input(z.object({ .input(z.object({
slotId: z.string(), slotId: z.string(),
})) }))
.query(async ({ input, ctx }) => { .query(async ({ input }): Promise<AdminSlotProductIdsResult> => {
const { slotId } = input; const { slotId } = input;
const productIds = await getSlotProductIdsInDb(slotId)
/*
// Old implementation - direct DB queries:
const associations = await db.query.productSlots.findMany({ const associations = await db.query.productSlots.findMany({
where: eq(productSlots.slotId, parseInt(slotId)), where: eq(productSlots.slotId, parseInt(slotId)),
columns: { columns: {
@ -217,19 +293,28 @@ export const productRouter = router({
return { return {
productIds, productIds,
}; };
*/
return {
productIds,
}
}), }),
getSlotsProductIds: protectedProcedure getSlotsProductIds: protectedProcedure
.input(z.object({ .input(z.object({
slotIds: z.array(z.number()), slotIds: z.array(z.number()),
})) }))
.query(async ({ input, ctx }) => { .query(async ({ input }): Promise<AdminSlotsProductIdsResult> => {
const { slotIds } = input; const { slotIds } = input;
if (!Array.isArray(slotIds)) { if (!Array.isArray(slotIds)) {
throw new ApiError("slotIds must be an array", 400); throw new ApiError("slotIds must be an array", 400);
} }
const result = await getSlotsProductIdsInDb(slotIds)
/*
// Old implementation - direct DB queries:
if (slotIds.length === 0) { if (slotIds.length === 0) {
return {}; return {};
} }
@ -260,6 +345,9 @@ export const productRouter = router({
}); });
return result; return result;
*/
return result
}), }),
getProductReviews: protectedProcedure getProductReviews: protectedProcedure
@ -268,9 +356,13 @@ export const productRouter = router({
limit: z.number().int().min(1).max(50).optional().default(10), limit: z.number().int().min(1).max(50).optional().default(10),
offset: z.number().int().min(0).optional().default(0), offset: z.number().int().min(0).optional().default(0),
})) }))
.query(async ({ input }) => { .query(async ({ input }): Promise<AdminProductReviewsResult> => {
const { productId, limit, offset } = input; const { productId, limit, offset } = input;
const { reviews, totalCount } = await getProductReviewsInDb(productId, limit, offset)
/*
// Old implementation - direct DB queries:
const reviews = await db const reviews = await db
.select({ .select({
id: productReviews.id, id: productReviews.id,
@ -308,6 +400,19 @@ export const productRouter = router({
const hasMore = offset + limit < totalCount; const hasMore = offset + limit < totalCount;
return { reviews: reviewsWithSignedUrls, hasMore }; return { reviews: reviewsWithSignedUrls, hasMore };
*/
const reviewsWithSignedUrls = await Promise.all(
reviews.map(async (review) => ({
...review,
signedImageUrls: await generateSignedUrlsFromS3Urls((review.imageUrls as string[]) || []),
signedAdminImageUrls: await generateSignedUrlsFromS3Urls((review.adminResponseImages as string[]) || []),
}))
)
const hasMore = offset + limit < totalCount
return { reviews: reviewsWithSignedUrls, hasMore }
}), }),
respondToReview: protectedProcedure respondToReview: protectedProcedure
@ -317,9 +422,13 @@ export const productRouter = router({
adminResponseImages: z.array(z.string()).optional().default([]), adminResponseImages: z.array(z.string()).optional().default([]),
uploadUrls: z.array(z.string()).optional().default([]), uploadUrls: z.array(z.string()).optional().default([]),
})) }))
.mutation(async ({ input }) => { .mutation(async ({ input }): Promise<AdminProductReviewResponse> => {
const { reviewId, adminResponse, adminResponseImages, uploadUrls } = input; const { reviewId, adminResponse, adminResponseImages, uploadUrls } = input;
const updatedReview = await respondToReviewInDb(reviewId, adminResponse, adminResponseImages)
/*
// Old implementation - direct DB queries:
const [updatedReview] = await db const [updatedReview] = await db
.update(productReviews) .update(productReviews)
.set({ .set({
@ -340,10 +449,25 @@ export const productRouter = router({
} }
return { success: true, review: updatedReview }; return { success: true, review: updatedReview };
*/
if (!updatedReview) {
throw new ApiError('Review not found', 404)
}
if (uploadUrls && uploadUrls.length > 0) {
await Promise.all(uploadUrls.map(url => claimUploadUrl(url)))
}
return { success: true, review: updatedReview }
}), }),
getGroups: protectedProcedure getGroups: protectedProcedure
.query(async ({ ctx }) => { .query(async (): Promise<AdminProductGroupsResult> => {
const groups = await getAllProductGroupsInDb()
/*
// Old implementation - direct DB queries:
const groups = await db.query.productGroupInfo.findMany({ const groups = await db.query.productGroupInfo.findMany({
with: { with: {
memberships: { memberships: {
@ -354,14 +478,18 @@ export const productRouter = router({
}, },
orderBy: desc(productGroupInfo.createdAt), orderBy: desc(productGroupInfo.createdAt),
}); });
*/
return { return {
groups: groups.map(group => ({ groups: groups.map(group => ({
...group, ...group,
products: group.memberships.map(m => m.product), products: group.memberships.map(m => ({
...m.product,
images: (m.product.images as string[]) || null,
})),
productCount: group.memberships.length, productCount: group.memberships.length,
})), })),
}; }
}), }),
createGroup: protectedProcedure createGroup: protectedProcedure
@ -370,9 +498,13 @@ export const productRouter = router({
description: z.string().optional(), description: z.string().optional(),
product_ids: z.array(z.number()).default([]), product_ids: z.array(z.number()).default([]),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input }): Promise<AdminProductGroupResponse> => {
const { group_name, description, product_ids } = input; const { group_name, description, product_ids } = input;
const newGroup = await createProductGroupInDb(group_name, description, product_ids)
/*
// Old implementation - direct DB queries:
const [newGroup] = await db const [newGroup] = await db
.insert(productGroupInfo) .insert(productGroupInfo)
.values({ .values({
@ -391,12 +523,20 @@ export const productRouter = router({
} }
// Reinitialize stores to reflect changes // Reinitialize stores to reflect changes
await initializeAllStores(); scheduleStoreInitialization()
return { return {
group: newGroup, group: newGroup,
message: 'Group created successfully', message: 'Group created successfully',
}; };
*/
scheduleStoreInitialization()
return {
group: newGroup,
message: 'Group created successfully',
}
}), }),
updateGroup: protectedProcedure updateGroup: protectedProcedure
@ -406,9 +546,13 @@ export const productRouter = router({
description: z.string().optional(), description: z.string().optional(),
product_ids: z.array(z.number()).optional(), product_ids: z.array(z.number()).optional(),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input }): Promise<AdminProductGroupResponse> => {
const { id, group_name, description, product_ids } = input; const { id, group_name, description, product_ids } = input;
const updatedGroup = await updateProductGroupInDb(id, group_name, description, product_ids)
/*
// Old implementation - direct DB queries:
const updateData: any = {}; const updateData: any = {};
if (group_name !== undefined) updateData.groupName = group_name; if (group_name !== undefined) updateData.groupName = group_name;
if (description !== undefined) updateData.description = description; if (description !== undefined) updateData.description = description;
@ -439,21 +583,37 @@ export const productRouter = router({
} }
// Reinitialize stores to reflect changes // Reinitialize stores to reflect changes
await initializeAllStores(); scheduleStoreInitialization()
return { return {
group: updatedGroup, group: updatedGroup,
message: 'Group updated successfully', message: 'Group updated successfully',
}; };
*/
if (!updatedGroup) {
throw new ApiError('Group not found', 404)
}
scheduleStoreInitialization()
return {
group: updatedGroup,
message: 'Group updated successfully',
}
}), }),
deleteGroup: protectedProcedure deleteGroup: protectedProcedure
.input(z.object({ .input(z.object({
id: z.number(), id: z.number(),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input }): Promise<AdminDeleteProductResult> => {
const { id } = input; const { id } = input;
const deletedGroup = await deleteProductGroupInDb(id)
/*
// Old implementation - direct DB queries:
// Delete memberships first // Delete memberships first
await db.delete(productGroupMembership).where(eq(productGroupMembership.groupId, id)); await db.delete(productGroupMembership).where(eq(productGroupMembership.groupId, id));
@ -467,15 +627,26 @@ export const productRouter = router({
throw new ApiError('Group not found', 404); throw new ApiError('Group not found', 404);
} }
// Reinitialize stores to reflect changes // Reinitialize stores to reflect changes
await initializeAllStores(); scheduleStoreInitialization()
return { return {
message: 'Group deleted successfully', message: 'Group deleted successfully',
}; };
}), */
updateProductPrices: protectedProcedure if (!deletedGroup) {
throw new ApiError('Group not found', 404)
}
scheduleStoreInitialization()
return {
message: 'Group deleted successfully',
}
}),
updateProductPrices: protectedProcedure
.input(z.object({ .input(z.object({
updates: z.array(z.object({ updates: z.array(z.object({
productId: z.number(), productId: z.number(),
@ -485,9 +656,17 @@ export const productRouter = router({
isFlashAvailable: z.boolean().optional(), isFlashAvailable: z.boolean().optional(),
})), })),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input }): Promise<AdminUpdateProductPricesResult> => {
const { updates } = input; const { updates } = input;
if (updates.length === 0) {
throw new ApiError('No updates provided', 400)
}
const result = await updateProductPricesInDb(updates)
/*
// Old implementation - direct DB queries:
if (updates.length === 0) { if (updates.length === 0) {
throw new ApiError('No updates provided', 400); throw new ApiError('No updates provided', 400);
} }
@ -523,12 +702,24 @@ export const productRouter = router({
await Promise.all(updatePromises); await Promise.all(updatePromises);
// Reinitialize stores to reflect changes // Reinitialize stores to reflect changes
await initializeAllStores(); scheduleStoreInitialization()
return {
message: `Updated prices for ${updates.length} product(s)`,
updatedCount: updates.length,
};
*/
if (result.invalidIds.length > 0) {
throw new ApiError(`Invalid product IDs: ${result.invalidIds.join(', ')}`, 400)
}
scheduleStoreInitialization()
return { return {
message: `Updated prices for ${updates.length} product(s)`, message: `Updated prices for ${result.updatedCount} product(s)`,
updatedCount: updates.length, updatedCount: result.updatedCount,
}; }
}), }),
}); });

View file

@ -1,14 +1,39 @@
import { router, protectedProcedure } from "@/src/trpc/trpc-index" import { router, protectedProcedure } from "@/src/trpc/trpc-index"
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod";
import { db } from "@/src/db/db_index"
import { deliverySlotInfo, productSlots, productInfo, vendorSnippets, productGroupInfo } from "@/src/db/schema"
import { eq, inArray, and, desc } from "drizzle-orm";
import { ApiError } from "@/src/lib/api-error" import { ApiError } from "@/src/lib/api-error"
import { appUrl } from "@/src/lib/env-exporter" import { appUrl } from "@/src/lib/env-exporter"
import redisClient from "@/src/lib/redis-client" import redisClient from "@/src/lib/redis-client"
import { getSlotSequenceKey } from "@/src/lib/redisKeyGetters" import { getSlotSequenceKey } from "@/src/lib/redisKeyGetters"
import { initializeAllStores } from '@/src/stores/store-initializer' import { scheduleStoreInitialization } from '@/src/stores/store-initializer'
import {
getActiveSlotsWithProducts as getActiveSlotsWithProductsInDb,
getActiveSlots as getActiveSlotsInDb,
getSlotsAfterDate as getSlotsAfterDateInDb,
getSlotByIdWithRelations as getSlotByIdWithRelationsInDb,
createSlotWithRelations as createSlotWithRelationsInDb,
updateSlotWithRelations as updateSlotWithRelationsInDb,
deleteSlotById as deleteSlotByIdInDb,
updateSlotCapacity as updateSlotCapacityInDb,
getSlotDeliverySequence as getSlotDeliverySequenceInDb,
updateSlotDeliverySequence as updateSlotDeliverySequenceInDb,
updateSlotProducts as updateSlotProductsInDb,
getSlotsProductIds as getSlotsProductIdsInDb,
} from '@/src/dbService'
import type {
AdminDeliverySequenceResult,
AdminSlotResult,
AdminSlotsResult,
AdminSlotsListResult,
AdminSlotCreateResult,
AdminSlotUpdateResult,
AdminSlotDeleteResult,
AdminUpdateDeliverySequenceResult,
AdminUpdateSlotCapacityResult,
AdminSlotsProductIdsResult,
AdminUpdateSlotProductsResult,
} from '@packages/shared'
interface CachedDeliverySequence { interface CachedDeliverySequence {
[userId: string]: number[]; [userId: string]: number[];
@ -63,11 +88,15 @@ const updateDeliverySequenceSchema = z.object({
export const slotsRouter = router({ export const slotsRouter = router({
// Exact replica of GET /av/slots // Exact replica of GET /av/slots
getAll: protectedProcedure.query(async ({ ctx }) => { getAll: protectedProcedure.query(async ({ ctx }): Promise<AdminSlotsResult> => {
if (!ctx.staffUser?.id) { if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
} }
const slots = await getActiveSlotsWithProductsInDb()
/*
// Old implementation - direct DB queries:
const slots = await db.query.deliverySlotInfo const slots = await db.query.deliverySlotInfo
.findMany({ .findMany({
where: eq(deliverySlotInfo.isActive, true), where: eq(deliverySlotInfo.isActive, true),
@ -93,17 +122,18 @@ export const slotsRouter = router({
products: slot.productSlots.map((ps) => ps.product), products: slot.productSlots.map((ps) => ps.product),
})) }))
); );
*/
return { return {
slots, slots,
count: slots.length, count: slots.length,
}; }
}), }),
// Exact replica of POST /av/products/slots/product-ids // Exact replica of POST /av/products/slots/product-ids
getSlotsProductIds: protectedProcedure getSlotsProductIds: protectedProcedure
.input(z.object({ slotIds: z.array(z.number()) })) .input(z.object({ slotIds: z.array(z.number()) }))
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }): Promise<AdminSlotsProductIdsResult> => {
if (!ctx.staffUser?.id) { if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
} }
@ -117,6 +147,10 @@ export const slotsRouter = router({
}); });
} }
const result = await getSlotsProductIdsInDb(slotIds)
/*
// Old implementation - direct DB queries:
if (slotIds.length === 0) { if (slotIds.length === 0) {
return {}; return {};
} }
@ -147,6 +181,9 @@ export const slotsRouter = router({
}); });
return result; return result;
*/
return result
}), }),
// Exact replica of PUT /av/products/slots/:slotId/products // Exact replica of PUT /av/products/slots/:slotId/products
@ -157,7 +194,7 @@ export const slotsRouter = router({
productIds: z.array(z.number()), productIds: z.array(z.number()),
}) })
) )
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<AdminUpdateSlotProductsResult> => {
if (!ctx.staffUser?.id) { if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
} }
@ -171,6 +208,10 @@ export const slotsRouter = router({
}); });
} }
const result = await updateSlotProductsInDb(String(slotId), productIds.map(String))
/*
// Old implementation - direct DB queries:
// Get current associations // Get current associations
const currentAssociations = await db.query.productSlots.findMany({ const currentAssociations = await db.query.productSlots.findMany({
where: eq(productSlots.slotId, slotId), where: eq(productSlots.slotId, slotId),
@ -215,18 +256,27 @@ export const slotsRouter = router({
} }
// Reinitialize stores to reflect changes // Reinitialize stores to reflect changes
await initializeAllStores(); scheduleStoreInitialization()
return { return {
message: "Slot products updated successfully", message: "Slot products updated successfully",
added: productsToAdd.length, added: productsToAdd.length,
removed: productsToRemove.length, removed: productsToRemove.length,
}; };
*/
scheduleStoreInitialization()
return {
message: result.message,
added: result.added,
removed: result.removed,
}
}), }),
createSlot: protectedProcedure createSlot: protectedProcedure
.input(createSlotSchema) .input(createSlotSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<AdminSlotCreateResult> => {
if (!ctx.staffUser?.id) { if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
} }
@ -238,6 +288,17 @@ export const slotsRouter = router({
throw new ApiError("Delivery time and orders close time are required", 400); throw new ApiError("Delivery time and orders close time are required", 400);
} }
const result = await createSlotWithRelationsInDb({
deliveryTime,
freezeTime,
isActive,
productIds,
vendorSnippets: snippets,
groupIds,
})
/*
// Old implementation - direct DB queries:
const result = await db.transaction(async (tx) => { const result = await db.transaction(async (tx) => {
// Create slot // Create slot
const [newSlot] = await tx const [newSlot] = await tx
@ -296,76 +357,84 @@ export const slotsRouter = router({
message: "Slot created successfully", message: "Slot created successfully",
}; };
}); });
*/
// Reinitialize stores to reflect changes (outside transaction) // Reinitialize stores to reflect changes (outside transaction)
await initializeAllStores(); scheduleStoreInitialization()
return result; return result
}), }),
getSlots: protectedProcedure.query(async ({ ctx }) => { getSlots: protectedProcedure.query(async ({ ctx }): Promise<AdminSlotsListResult> => {
if (!ctx.staffUser?.id) { if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
} }
const slots = await getActiveSlotsInDb()
/*
// Old implementation - direct DB queries:
const slots = await db.query.deliverySlotInfo.findMany({ const slots = await db.query.deliverySlotInfo.findMany({
where: eq(deliverySlotInfo.isActive, true), where: eq(deliverySlotInfo.isActive, true),
}); });
*/
return { return {
slots, slots,
count: slots.length, count: slots.length,
}; }
}), }),
getSlotById: protectedProcedure getSlotById: protectedProcedure
.input(getSlotByIdSchema) .input(getSlotByIdSchema)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }): Promise<AdminSlotResult> => {
if (!ctx.staffUser?.id) { if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
} }
const { id } = input; const { id } = input;
const slot = await db.query.deliverySlotInfo.findFirst({ const slot = await getSlotByIdWithRelationsInDb(id)
where: eq(deliverySlotInfo.id, id),
with: { /*
productSlots: { // Old implementation - direct DB queries:
with: { const slot = await db.query.deliverySlotInfo.findFirst({
product: { where: eq(deliverySlotInfo.id, id),
columns: { with: {
id: true, productSlots: {
name: true, with: {
images: true, product: {
}, columns: {
}, id: true,
}, name: true,
}, images: true,
vendorSnippets: true, },
}, },
}); },
},
vendorSnippets: true,
},
});
*/
if (!slot) { if (!slot) {
throw new ApiError("Slot not found", 404); throw new ApiError('Slot not found', 404)
} }
return { return {
slot: { slot: {
...slot, ...slot,
deliverySequence: slot.deliverySequence as number[], vendorSnippets: slot.vendorSnippets.map(snippet => ({
groupIds: slot.groupIds as number[],
products: slot.productSlots.map((ps) => ps.product),
vendorSnippets: slot.vendorSnippets?.map(snippet => ({
...snippet, ...snippet,
accessUrl: `${appUrl}/vendor-order-list?id=${snippet.snippetCode}` accessUrl: `${appUrl}/vendor-order-list?id=${snippet.snippetCode}`,
})), })),
}, },
}; }
}), }),
updateSlot: protectedProcedure updateSlot: protectedProcedure
.input(updateSlotSchema) .input(updateSlotSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<AdminSlotUpdateResult> => {
if (!ctx.staffUser?.id) { if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
} }
@ -376,6 +445,18 @@ export const slotsRouter = router({
throw new ApiError("Delivery time and orders close time are required", 400); throw new ApiError("Delivery time and orders close time are required", 400);
} }
const result = await updateSlotWithRelationsInDb({
id,
deliveryTime,
freezeTime,
isActive,
productIds,
vendorSnippets: snippets,
groupIds,
})
/*
// Old implementation - direct DB queries:
// Filter groupIds to only include valid (existing) groups // Filter groupIds to only include valid (existing) groups
let validGroupIds = groupIds; let validGroupIds = groupIds;
if (groupIds && groupIds.length > 0) { if (groupIds && groupIds.length > 0) {
@ -455,11 +536,16 @@ export const slotsRouter = router({
message: "Slot updated successfully", message: "Slot updated successfully",
}; };
}); });
*/
if (!result) {
throw new ApiError('Slot not found', 404)
}
// Reinitialize stores to reflect changes (outside transaction) // Reinitialize stores to reflect changes (outside transaction)
await initializeAllStores(); scheduleStoreInitialization()
return result; return result
} }
catch(e) { catch(e) {
console.log(e) console.log(e)
@ -469,13 +555,17 @@ export const slotsRouter = router({
deleteSlot: protectedProcedure deleteSlot: protectedProcedure
.input(deleteSlotSchema) .input(deleteSlotSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<AdminSlotDeleteResult> => {
if (!ctx.staffUser?.id) { if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
} }
const { id } = input; const { id } = input;
const deletedSlot = await deleteSlotByIdInDb(id)
/*
// Old implementation - direct DB queries:
const [deletedSlot] = await db const [deletedSlot] = await db
.update(deliverySlotInfo) .update(deliverySlotInfo)
.set({ isActive: false }) .set({ isActive: false })
@ -485,18 +575,23 @@ export const slotsRouter = router({
if (!deletedSlot) { if (!deletedSlot) {
throw new ApiError("Slot not found", 404); throw new ApiError("Slot not found", 404);
} }
*/
if (!deletedSlot) {
throw new ApiError('Slot not found', 404)
}
// Reinitialize stores to reflect changes // Reinitialize stores to reflect changes
await initializeAllStores(); scheduleStoreInitialization()
return { return {
message: "Slot deleted successfully", message: 'Slot deleted successfully',
}; }
}), }),
getDeliverySequence: protectedProcedure getDeliverySequence: protectedProcedure
.input(getDeliverySequenceSchema) .input(getDeliverySequenceSchema)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }): Promise<AdminDeliverySequenceResult> => {
const { id } = input; const { id } = input;
const slotId = parseInt(id); const slotId = parseInt(id);
@ -506,7 +601,7 @@ export const slotsRouter = router({
const cached = await redisClient.get(cacheKey); const cached = await redisClient.get(cacheKey);
if (cached) { if (cached) {
const parsed = JSON.parse(cached); const parsed = JSON.parse(cached);
const validated = cachedSequenceSchema.parse(parsed) as CachedDeliverySequence; const validated = cachedSequenceSchema.parse(parsed);
console.log('sending cached response') console.log('sending cached response')
return { deliverySequence: validated }; return { deliverySequence: validated };
@ -517,6 +612,10 @@ export const slotsRouter = router({
} }
// Fallback to DB // Fallback to DB
const slot = await getSlotDeliverySequenceInDb(slotId)
/*
// Old implementation - direct DB queries:
const slot = await db.query.deliverySlotInfo.findFirst({ const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, slotId), where: eq(deliverySlotInfo.id, slotId),
}); });
@ -525,6 +624,13 @@ export const slotsRouter = router({
throw new ApiError("Slot not found", 404); throw new ApiError("Slot not found", 404);
} }
const sequence = cachedSequenceSchema.parse(slot.deliverySequence || {});
*/
if (!slot) {
throw new ApiError('Slot not found', 404)
}
const sequence = (slot.deliverySequence || {}) as CachedDeliverySequence; const sequence = (slot.deliverySequence || {}) as CachedDeliverySequence;
// Cache the validated result // Cache the validated result
@ -535,18 +641,22 @@ export const slotsRouter = router({
console.warn('Redis cache write failed:', cacheError); console.warn('Redis cache write failed:', cacheError);
} }
return { deliverySequence: sequence }; return { deliverySequence: sequence }
}), }),
updateDeliverySequence: protectedProcedure updateDeliverySequence: protectedProcedure
.input(updateDeliverySequenceSchema) .input(updateDeliverySequenceSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<AdminUpdateDeliverySequenceResult> => {
if (!ctx.staffUser?.id) { if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
} }
const { id, deliverySequence } = input; const { id, deliverySequence } = input;
const updatedSlot = await updateSlotDeliverySequenceInDb(id, deliverySequence)
/*
// Old implementation - direct DB queries:
const [updatedSlot] = await db const [updatedSlot] = await db
.update(deliverySlotInfo) .update(deliverySlotInfo)
.set({ deliverySequence }) .set({ deliverySequence })
@ -559,6 +669,11 @@ export const slotsRouter = router({
if (!updatedSlot) { if (!updatedSlot) {
throw new ApiError("Slot not found", 404); throw new ApiError("Slot not found", 404);
} }
*/
if (!updatedSlot) {
throw new ApiError('Slot not found', 404)
}
// Cache the updated sequence // Cache the updated sequence
const cacheKey = getSlotSequenceKey(id); const cacheKey = getSlotSequenceKey(id);
@ -571,8 +686,8 @@ export const slotsRouter = router({
return { return {
slot: updatedSlot, slot: updatedSlot,
message: "Delivery sequence updated successfully", message: 'Delivery sequence updated successfully',
}; }
}), }),
updateSlotCapacity: protectedProcedure updateSlotCapacity: protectedProcedure
@ -580,13 +695,17 @@ export const slotsRouter = router({
slotId: z.number(), slotId: z.number(),
isCapacityFull: z.boolean(), isCapacityFull: z.boolean(),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<AdminUpdateSlotCapacityResult> => {
if (!ctx.staffUser?.id) { if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
} }
const { slotId, isCapacityFull } = input; const { slotId, isCapacityFull } = input;
const result = await updateSlotCapacityInDb(slotId, isCapacityFull)
/*
// Old implementation - direct DB queries:
const [updatedSlot] = await db const [updatedSlot] = await db
.update(deliverySlotInfo) .update(deliverySlotInfo)
.set({ isCapacityFull }) .set({ isCapacityFull })
@ -598,12 +717,21 @@ export const slotsRouter = router({
} }
// Reinitialize stores to reflect changes // Reinitialize stores to reflect changes
await initializeAllStores(); scheduleStoreInitialization()
return { return {
success: true, success: true,
slot: updatedSlot, slot: updatedSlot,
message: `Slot ${isCapacityFull ? 'marked as full capacity' : 'capacity reset'}`, message: `Slot ${isCapacityFull ? 'marked as full capacity' : 'capacity reset'}`,
}; };
*/
if (!result) {
throw new ApiError('Slot not found', 404)
}
scheduleStoreInitialization()
return result
}), }),
}); });

View file

@ -1,11 +1,20 @@
import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index' import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'; import { z } from 'zod';
import { db } from '@/src/db/db_index'
import { staffUsers, staffRoles, users, userDetails, orders } from '@/src/db/schema'
import { eq, or, ilike, and, lt, desc } from 'drizzle-orm';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { ApiError } from '@/src/lib/api-error' import { ApiError } from '@/src/lib/api-error'
import {
getStaffUserByName,
getAllStaff,
getAllUsers,
getUserWithDetails,
upsertUserSuspension,
checkStaffUserExists,
checkStaffRoleExists,
createStaffUser,
getAllRoles,
} from '@/src/dbService'
import type { StaffUser, StaffRole } from '@packages/shared'
export const staffUserRouter = router({ export const staffUserRouter = router({
login: publicProcedure login: publicProcedure
@ -20,9 +29,7 @@ export const staffUserRouter = router({
throw new ApiError('Name and password are required', 400); throw new ApiError('Name and password are required', 400);
} }
const staff = await db.query.staffUsers.findFirst({ const staff = await getStaffUserByName(name);
where: eq(staffUsers.name, name),
});
if (!staff) { if (!staff) {
throw new ApiError('Invalid credentials', 401); throw new ApiError('Invalid credentials', 401);
@ -48,23 +55,7 @@ export const staffUserRouter = router({
getStaff: protectedProcedure getStaff: protectedProcedure
.query(async ({ ctx }) => { .query(async ({ ctx }) => {
const staff = await db.query.staffUsers.findMany({ const staff = await getAllStaff();
columns: {
id: true,
name: true,
},
with: {
role: {
with: {
rolePermissions: {
with: {
permission: true,
},
},
},
},
},
});
// Transform the data to include role and permissions in a cleaner format // Transform the data to include role and permissions in a cleaner format
const transformedStaff = staff.map((user) => ({ const transformedStaff = staff.map((user) => ({
@ -74,7 +65,7 @@ export const staffUserRouter = router({
id: user.role.id, id: user.role.id,
name: user.role.roleName, name: user.role.roleName,
} : null, } : null,
permissions: user.role?.rolePermissions.map((rp) => ({ permissions: user.role?.rolePermissions.map((rp: any) => ({
id: rp.permission.id, id: rp.permission.id,
name: rp.permission.permissionName, name: rp.permission.permissionName,
})) || [], })) || [],
@ -94,34 +85,9 @@ export const staffUserRouter = router({
.query(async ({ input }) => { .query(async ({ input }) => {
const { cursor, limit, search } = input; const { cursor, limit, search } = input;
let whereCondition = undefined; const { users: usersToReturn, hasMore } = await getAllUsers(cursor, limit, search);
if (search) { const formattedUsers = usersToReturn.map((user: any) => ({
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;
const formattedUsers = usersToReturn.map(user => ({
id: user.id, id: user.id,
name: user.name, name: user.name,
email: user.email, email: user.email,
@ -140,16 +106,7 @@ export const staffUserRouter = router({
.query(async ({ input }) => { .query(async ({ input }) => {
const { userId } = input; const { userId } = input;
const user = await db.query.users.findFirst({ const user = await getUserWithDetails(userId);
where: eq(users.id, userId),
with: {
userDetails: true,
orders: {
orderBy: desc(orders.createdAt),
limit: 1,
},
},
});
if (!user) { if (!user) {
throw new ApiError("User not found", 404); throw new ApiError("User not found", 404);
@ -173,13 +130,7 @@ export const staffUserRouter = router({
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
const { userId, isSuspended } = input; const { userId, isSuspended } = input;
await db await upsertUserSuspension(userId, isSuspended);
.insert(userDetails)
.values({ userId, isSuspended })
.onConflictDoUpdate({
target: userDetails.userId,
set: { isSuspended },
});
return { success: true }; return { success: true };
}), }),
@ -194,20 +145,16 @@ export const staffUserRouter = router({
const { name, password, roleId } = input; const { name, password, roleId } = input;
// Check if staff user already exists // Check if staff user already exists
const existingUser = await db.query.staffUsers.findFirst({ const existingUser = await checkStaffUserExists(name);
where: eq(staffUsers.name, name),
});
if (existingUser) { if (existingUser) {
throw new ApiError('Staff user with this name already exists', 409); throw new ApiError('Staff user with this name already exists', 409);
} }
// Check if role exists // Check if role exists
const role = await db.query.staffRoles.findFirst({ const roleExists = await checkStaffRoleExists(roleId);
where: eq(staffRoles.id, roleId),
});
if (!role) { if (!roleExists) {
throw new ApiError('Invalid role selected', 400); throw new ApiError('Invalid role selected', 400);
} }
@ -215,26 +162,17 @@ export const staffUserRouter = router({
const hashedPassword = await bcrypt.hash(password, 12); const hashedPassword = await bcrypt.hash(password, 12);
// Create staff user // Create staff user
const [newUser] = await db.insert(staffUsers).values({ const newUser = await createStaffUser(name, hashedPassword, roleId);
name: name.trim(),
password: hashedPassword,
staffRoleId: roleId,
}).returning();
return { success: true, user: { id: newUser.id, name: newUser.name } }; return { success: true, user: { id: newUser.id, name: newUser.name } };
}), }),
getRoles: protectedProcedure getRoles: protectedProcedure
.query(async ({ ctx }) => { .query(async ({ ctx }) => {
const roles = await db.query.staffRoles.findMany({ const roles = await getAllRoles();
columns: {
id: true,
roleName: true,
},
});
return { return {
roles: roles.map(role => ({ roles: roles.map((role: any) => ({
id: role.id, id: role.id,
name: role.roleName, name: role.roleName,
})), })),

View file

@ -1,29 +1,29 @@
import { router, protectedProcedure } from '@/src/trpc/trpc-index' import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'; import { z } from 'zod';
import { db } from '@/src/db/db_index'
import { storeInfo, productInfo } from '@/src/db/schema'
import { eq, inArray } from 'drizzle-orm';
import { ApiError } from '@/src/lib/api-error' import { ApiError } from '@/src/lib/api-error'
import { extractKeyFromPresignedUrl, deleteImageUtil, generateSignedUrlFromS3Url } from '@/src/lib/s3-client' import { extractKeyFromPresignedUrl, deleteImageUtil, generateSignedUrlFromS3Url } from '@/src/lib/s3-client'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import { scheduleStoreInitialization } from '@/src/stores/store-initializer'
import { initializeAllStores } from '@/src/stores/store-initializer' import {
getAllStores as getAllStoresFromDb,
getStoreById as getStoreByIdFromDb,
createStore as createStoreInDb,
updateStore as updateStoreInDb,
deleteStore as deleteStoreFromDb,
} from '@/src/dbService'
import type { Store } from '@packages/shared'
export const storeRouter = router({ export const storeRouter = router({
getStores: protectedProcedure getStores: protectedProcedure
.query(async ({ ctx }) => { .query(async ({ ctx }): Promise<{ stores: any[]; count: number }> => {
const stores = await db.query.storeInfo.findMany({ const stores = await getAllStoresFromDb();
with: {
owner: true,
},
});
Promise.all(stores.map(async store => { await Promise.all(stores.map(async store => {
if(store.imageUrl) if(store.imageUrl)
store.imageUrl = await generateSignedUrlFromS3Url(store.imageUrl) store.imageUrl = await generateSignedUrlFromS3Url(store.imageUrl)
})).catch((e) => { })).catch((e) => {
throw new ApiError("Unable to find store image urls") throw new ApiError("Unable to find store image urls")
} })
)
return { return {
stores, stores,
count: stores.length, count: stores.length,
@ -34,15 +34,10 @@ export const storeRouter = router({
.input(z.object({ .input(z.object({
id: z.number(), id: z.number(),
})) }))
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }): Promise<{ store: any }> => {
const { id } = input; const { id } = input;
const store = await db.query.storeInfo.findFirst({ const store = await getStoreByIdFromDb(id);
where: eq(storeInfo.id, id),
with: {
owner: true,
},
});
if (!store) { if (!store) {
throw new ApiError("Store not found", 404); throw new ApiError("Store not found", 404);
@ -53,19 +48,31 @@ export const storeRouter = router({
}; };
}), }),
createStore: protectedProcedure createStore: protectedProcedure
.input(z.object({ .input(z.object({
name: z.string().min(1, "Name is required"), name: z.string().min(1, "Name is required"),
description: z.string().optional(), description: z.string().optional(),
imageUrl: z.string().optional(), imageUrl: z.string().optional(),
owner: z.number().min(1, "Owner is required"), owner: z.number().min(1, "Owner is required"),
products: z.array(z.number()).optional(), products: z.array(z.number()).optional(),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<{ store: Store; message: string }> => {
const { name, description, imageUrl, owner, products } = input; const { name, description, imageUrl, owner, products } = input;
const imageKey = imageUrl ? extractKeyFromPresignedUrl(imageUrl) : undefined; const imageKey = imageUrl ? extractKeyFromPresignedUrl(imageUrl) : undefined;
const newStore = await createStoreInDb(
{
name,
description,
imageUrl: imageKey,
owner,
},
products
);
/*
// Old implementation - direct DB query:
const [newStore] = await db const [newStore] = await db
.insert(storeInfo) .insert(storeInfo)
.values({ .values({
@ -83,9 +90,10 @@ export const storeRouter = router({
.set({ storeId: newStore.id }) .set({ storeId: newStore.id })
.where(inArray(productInfo.id, products)); .where(inArray(productInfo.id, products));
} }
*/
// Reinitialize stores to reflect changes // Reinitialize stores to reflect changes
await initializeAllStores(); scheduleStoreInitialization()
return { return {
store: newStore, store: newStore,
@ -93,117 +101,134 @@ export const storeRouter = router({
}; };
}), }),
updateStore: protectedProcedure updateStore: protectedProcedure
.input(z.object({ .input(z.object({
id: z.number(), id: z.number(),
name: z.string().min(1, "Name is required"), name: z.string().min(1, "Name is required"),
description: z.string().optional(), description: z.string().optional(),
imageUrl: z.string().optional(), imageUrl: z.string().optional(),
owner: z.number().min(1, "Owner is required"), owner: z.number().min(1, "Owner is required"),
products: z.array(z.number()).optional(), products: z.array(z.number()).optional(),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<{ store: Store; message: string }> => {
const { id, name, description, imageUrl, owner, products } = input; const { id, name, description, imageUrl, owner, products } = input;
const existingStore = await db.query.storeInfo.findFirst({ const existingStore = await getStoreByIdFromDb(id);
where: eq(storeInfo.id, id),
});
if (!existingStore) { if (!existingStore) {
throw new ApiError("Store not found", 404); throw new ApiError("Store not found", 404);
} }
const oldImageKey = existingStore.imageUrl; const oldImageKey = existingStore.imageUrl;
const newImageKey = imageUrl ? extractKeyFromPresignedUrl(imageUrl) : oldImageKey; const newImageKey = imageUrl ? extractKeyFromPresignedUrl(imageUrl) : oldImageKey;
// Delete old image only if: // Delete old image only if:
// 1. New image provided and keys are different, OR // 1. New image provided and keys are different, OR
// 2. No new image but old exists (clearing the image) // 2. No new image but old exists (clearing the image)
if (oldImageKey && ( if (oldImageKey && (
(newImageKey && newImageKey !== oldImageKey) || (newImageKey && newImageKey !== oldImageKey) ||
(!newImageKey) (!newImageKey)
)) { )) {
try { try {
await deleteImageUtil({keys: [oldImageKey]}); await deleteImageUtil({keys: [oldImageKey]});
} catch (error) { } catch (error) {
console.error('Failed to delete old image:', error); console.error('Failed to delete old image:', error);
// Continue with update even if deletion fails // Continue with update even if deletion fails
}
} }
}
const [updatedStore] = await db const updatedStore = await updateStoreInDb(
.update(storeInfo) id,
.set({ {
name, name,
description, description,
imageUrl: newImageKey, imageUrl: newImageKey,
owner, owner,
}) },
.where(eq(storeInfo.id, id)) products
);
/*
// Old implementation - direct DB query:
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, 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 db
.update(productInfo)
.set({ storeId: id })
.where(inArray(productInfo.id, products));
}
}
*/
// Reinitialize stores to reflect changes
scheduleStoreInitialization()
return {
store: updatedStore,
message: "Store updated successfully",
};
}),
deleteStore: protectedProcedure
.input(z.object({
storeId: z.number(),
}))
.mutation(async ({ input, ctx }): Promise<{ message: string }> => {
const { storeId } = input;
const result = await deleteStoreFromDb(storeId);
/*
// Old implementation - direct DB query with transaction:
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
const [deletedStore] = await tx
.delete(storeInfo)
.where(eq(storeInfo.id, storeId))
.returning(); .returning();
if (!updatedStore) { if (!deletedStore) {
throw new ApiError("Store not found", 404); throw new ApiError("Store not found", 404);
} }
// Update products if provided
if (products) {
// First, set storeId to null for products not in the list but currently assigned to this store
await db
.update(productInfo)
.set({ storeId: null })
.where(eq(productInfo.storeId, id));
// Then, assign the selected products to this store
if (products.length > 0) {
await db
.update(productInfo)
.set({ storeId: id })
.where(inArray(productInfo.id, products));
}
}
// Reinitialize stores to reflect changes
await initializeAllStores();
return { return {
store: updatedStore, message: "Store deleted successfully",
message: "Store updated successfully",
}; };
}), });
*/
deleteStore: protectedProcedure // Reinitialize stores to reflect changes (outside transaction)
.input(z.object({ scheduleStoreInitialization()
storeId: z.number(),
}))
.mutation(async ({ input, ctx }) => {
const { storeId } = input;
const result = await db.transaction(async (tx) => { return result;
// 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
const [deletedStore] = await tx
.delete(storeInfo)
.where(eq(storeInfo.id, storeId))
.returning();
if (!deletedStore) {
throw new ApiError("Store not found", 404);
}
return {
message: "Store deleted successfully",
};
});
// Reinitialize stores to reflect changes (outside transaction)
await initializeAllStores();
return result;
}),
}); });

View file

@ -1,44 +1,29 @@
import { protectedProcedure } from '@/src/trpc/trpc-index'; import { protectedProcedure } from '@/src/trpc/trpc-index';
import { z } from 'zod'; import { z } from 'zod';
import { db } from '@/src/db/db_index';
import { users, complaints, orders, orderItems, notifCreds, unloggedUserTokens, userDetails, userIncidents } from '@/src/db/schema';
import { eq, sql, desc, asc, count, max, inArray } from 'drizzle-orm';
import { ApiError } from '@/src/lib/api-error'; import { ApiError } from '@/src/lib/api-error';
import { notificationQueue } from '@/src/lib/notif-job'; import { notificationQueue } from '@/src/lib/notif-job';
import { recomputeUserNegativityScore } from '@/src/stores/user-negativity-store'; import { recomputeUserNegativityScore } from '@/src/stores/user-negativity-store';
import {
async function createUserByMobile(mobile: string): Promise<typeof users.$inferSelect> { createUserByMobile,
// Clean mobile number (remove non-digits) getUserByMobile,
const cleanMobile = mobile.replace(/\D/g, ''); getUnresolvedComplaintsCount,
getAllUsersWithFilters,
// Validate: exactly 10 digits getOrderCountsByUserIds,
if (cleanMobile.length !== 10) { getLastOrdersByUserIds,
throw new ApiError('Mobile number must be exactly 10 digits', 400); getSuspensionStatusesByUserIds,
} getUserBasicInfo,
getUserSuspensionStatus,
// Check if user already exists getUserOrders,
const [existingUser] = await db getOrderStatusesByOrderIds,
.select() getItemCountsByOrderIds,
.from(users) upsertUserSuspension,
.where(eq(users.mobile, cleanMobile)) searchUsers,
.limit(1); getAllNotifCreds,
getAllUnloggedTokens,
if (existingUser) { getNotifTokensByUserIds,
throw new ApiError('User with this mobile number already exists', 409); getUserIncidentsWithRelations,
} createUserIncident,
} from '@/src/dbService';
// Create user
const [newUser] = await db
.insert(users)
.values({
name: null,
email: null,
mobile: cleanMobile,
})
.returning();
return newUser;
}
export const userRouter = { export const userRouter = {
createUserByMobile: protectedProcedure createUserByMobile: protectedProcedure
@ -46,7 +31,22 @@ export const userRouter = {
mobile: z.string().min(1, 'Mobile number is required'), mobile: z.string().min(1, 'Mobile number is required'),
})) }))
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
const newUser = await createUserByMobile(input.mobile); // Clean mobile number (remove non-digits)
const cleanMobile = input.mobile.replace(/\D/g, '');
// Validate: exactly 10 digits
if (cleanMobile.length !== 10) {
throw new ApiError('Mobile number must be exactly 10 digits', 400);
}
// Check if user already exists
const existingUser = await getUserByMobile(cleanMobile);
if (existingUser) {
throw new ApiError('User with this mobile number already exists', 409);
}
const newUser = await createUserByMobile(cleanMobile);
return { return {
success: true, success: true,
@ -56,10 +56,10 @@ export const userRouter = {
getEssentials: protectedProcedure getEssentials: protectedProcedure
.query(async () => { .query(async () => {
const count = await db.$count(complaints, eq(complaints.isResolved, false)); const count = await getUnresolvedComplaintsCount();
return { return {
unresolvedComplaints: count || 0, unresolvedComplaints: count,
}; };
}), }),
@ -72,71 +72,14 @@ export const userRouter = {
.query(async ({ input }) => { .query(async ({ input }) => {
const { limit, cursor, search } = input; const { limit, cursor, search } = input;
// Build where conditions const { users: usersToReturn, hasMore } = await getAllUsersWithFilters(limit, cursor, search);
const whereConditions = [];
if (search && search.trim()) {
whereConditions.push(sql`${users.mobile} ILIKE ${`%${search.trim()}%`}`);
}
if (cursor) {
whereConditions.push(sql`${users.id} > ${cursor}`);
}
// Get users with filters applied
const usersList = await db
.select({
id: users.id,
name: users.name,
mobile: users.mobile,
createdAt: users.createdAt,
})
.from(users)
.where(whereConditions.length > 0 ? sql.join(whereConditions, sql` AND `) : undefined)
.orderBy(asc(users.id))
.limit(limit + 1); // Get one extra to determine if there's more
// Check if there are more results
const hasMore = usersList.length > limit;
const usersToReturn = hasMore ? usersList.slice(0, limit) : usersList;
// Get order stats for each user // Get order stats for each user
const userIds = usersToReturn.map(u => u.id); const userIds = usersToReturn.map((u: any) => u.id);
let orderCounts: { userId: number; totalOrders: number }[] = []; const orderCounts = await getOrderCountsByUserIds(userIds);
let lastOrders: { userId: number; lastOrderDate: Date | null }[] = []; const lastOrders = await getLastOrdersByUserIds(userIds);
let suspensionStatuses: { userId: number; isSuspended: boolean }[] = []; const suspensionStatuses = await getSuspensionStatusesByUserIds(userIds);
if (userIds.length > 0) {
// Get total orders per user
orderCounts = await db
.select({
userId: orders.userId,
totalOrders: count(orders.id),
})
.from(orders)
.where(sql`${orders.userId} IN (${sql.join(userIds, sql`, `)})`)
.groupBy(orders.userId);
// 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 // Create lookup maps
const orderCountMap = new Map(orderCounts.map(o => [o.userId, o.totalOrders])); const orderCountMap = new Map(orderCounts.map(o => [o.userId, o.totalOrders]));
@ -144,7 +87,7 @@ export const userRouter = {
const suspensionMap = new Map(suspensionStatuses.map(s => [s.userId, s.isSuspended])); const suspensionMap = new Map(suspensionStatuses.map(s => [s.userId, s.isSuspended]));
// Combine data // Combine data
const usersWithStats = usersToReturn.map(user => ({ const usersWithStats = usersToReturn.map((user: any) => ({
...user, ...user,
totalOrders: orderCountMap.get(user.id) || 0, totalOrders: orderCountMap.get(user.id) || 0,
lastOrderDate: lastOrderMap.get(user.id) || null, lastOrderDate: lastOrderMap.get(user.id) || null,
@ -169,69 +112,24 @@ export const userRouter = {
const { userId } = input; const { userId } = input;
// Get user info // Get user info
const user = await db const user = await getUserBasicInfo(userId);
.select({
id: users.id,
name: users.name,
mobile: users.mobile,
createdAt: users.createdAt,
})
.from(users)
.where(eq(users.id, userId))
.limit(1);
if (!user || user.length === 0) { if (!user) {
throw new ApiError('User not found', 404); throw new ApiError('User not found', 404);
} }
// Get user suspension status // Get user suspension status
const userDetail = await db const isSuspended = await getUserSuspensionStatus(userId);
.select({
isSuspended: userDetails.isSuspended,
})
.from(userDetails)
.where(eq(userDetails.userId, userId))
.limit(1);
// Get all orders for this user with order items count // Get all orders for this user
const userOrders = await db const userOrders = await getUserOrders(userId);
.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 // Get order status for each order
const orderIds = userOrders.map(o => o.id); const orderIds = userOrders.map((o: any) => o.id);
const orderStatuses = await getOrderStatusesByOrderIds(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 // Get item counts for each order
const itemCounts = await db const itemCounts = await getItemCountsByOrderIds(orderIds);
.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 // Create lookup maps
const statusMap = new Map(orderStatuses.map(s => [s.orderId, s])); const statusMap = new Map(orderStatuses.map(s => [s.orderId, s]));
@ -246,7 +144,7 @@ export const userRouter = {
}; };
// Combine data // Combine data
const ordersWithDetails = userOrders.map(order => { const ordersWithDetails = userOrders.map((order: any) => {
const status = statusMap.get(order.id); const status = statusMap.get(order.id);
return { return {
id: order.id, id: order.id,
@ -261,8 +159,8 @@ export const userRouter = {
return { return {
user: { user: {
...user[0], ...user,
isSuspended: userDetail[0]?.isSuspended ?? false, isSuspended,
}, },
orders: ordersWithDetails, orders: ordersWithDetails,
}; };
@ -276,39 +174,7 @@ export const userRouter = {
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
const { userId, isSuspended } = input; const { userId, isSuspended } = input;
// Check if user exists await upsertUserSuspension(userId, isSuspended);
const user = await db
.select({ id: users.id })
.from(users)
.where(eq(users.id, userId))
.limit(1);
if (!user || user.length === 0) {
throw new ApiError('User not found', 404);
}
// Check if user_details record exists
const existingDetail = await db
.select({ id: userDetails.id })
.from(userDetails)
.where(eq(userDetails.userId, userId))
.limit(1);
if (existingDetail.length > 0) {
// Update existing record
await db
.update(userDetails)
.set({ isSuspended })
.where(eq(userDetails.userId, userId));
} else {
// Insert new record
await db
.insert(userDetails)
.values({
userId,
isSuspended,
});
}
return { return {
success: true, success: true,
@ -323,36 +189,15 @@ export const userRouter = {
.query(async ({ input }) => { .query(async ({ input }) => {
const { search } = input; const { search } = input;
// Get all users const usersList = await searchUsers(search);
let usersList;
if (search && search.trim()) {
usersList = await db
.select({
id: users.id,
name: users.name,
mobile: users.mobile,
})
.from(users)
.where(sql`${users.mobile} ILIKE ${`%${search.trim()}%`} OR ${users.name} ILIKE ${`%${search.trim()}%`}`);
} else {
usersList = await db
.select({
id: users.id,
name: users.name,
mobile: users.mobile,
})
.from(users);
}
// Get eligible users (have notif_creds entry) // Get eligible users (have notif_creds entry)
const eligibleUsers = await db const eligibleUsers = await getAllNotifCreds();
.select({ userId: notifCreds.userId })
.from(notifCreds);
const eligibleSet = new Set(eligibleUsers.map(u => u.userId)); const eligibleSet = new Set(eligibleUsers.map(u => u.userId));
return { return {
users: usersList.map(user => ({ users: usersList.map((user: any) => ({
id: user.id, id: user.id,
name: user.name, name: user.name,
mobile: user.mobile, mobile: user.mobile,
@ -375,8 +220,8 @@ export const userRouter = {
if (userIds.length === 0) { if (userIds.length === 0) {
// Send to all users - get tokens from both logged-in and unlogged users // Send to all users - get tokens from both logged-in and unlogged users
const loggedInTokens = await db.select({ token: notifCreds.token }).from(notifCreds); const loggedInTokens = await getAllNotifCreds();
const unloggedTokens = await db.select({ token: unloggedUserTokens.token }).from(unloggedUserTokens); const unloggedTokens = await getAllUnloggedTokens();
tokens = [ tokens = [
...loggedInTokens.map(t => t.token), ...loggedInTokens.map(t => t.token),
@ -384,11 +229,7 @@ export const userRouter = {
]; ];
} else { } else {
// Send to specific users - get their tokens // Send to specific users - get their tokens
const userTokens = await db const userTokens = await getNotifTokensByUserIds(userIds);
.select({ token: notifCreds.token })
.from(notifCreds)
.where(inArray(notifCreds.userId, userIds));
tokens = userTokens.map(t => t.token); tokens = userTokens.map(t => t.token);
} }
@ -427,21 +268,10 @@ export const userRouter = {
.query(async ({ input }) => { .query(async ({ input }) => {
const { userId } = input; const { userId } = input;
const incidents = await db.query.userIncidents.findMany({ const incidents = await getUserIncidentsWithRelations(userId);
where: eq(userIncidents.userId, userId),
with: {
order: {
with: {
orderStatus: true,
},
},
addedBy: true,
},
orderBy: desc(userIncidents.dateAdded),
});
return { return {
incidents: incidents.map(incident => ({ incidents: incidents.map((incident: any) => ({
id: incident.id, id: incident.id,
userId: incident.userId, userId: incident.userId,
orderId: incident.orderId, orderId: incident.orderId,
@ -470,14 +300,13 @@ export const userRouter = {
throw new ApiError('Admin user not authenticated', 401); throw new ApiError('Admin user not authenticated', 401);
} }
const incident = await createUserIncident(
const incidentObj = { userId, orderId, adminComment, addedBy: adminUserId, negativityScore }; userId,
orderId,
const [incident] = await db.insert(userIncidents) adminComment,
.values({ adminUserId,
...incidentObj, negativityScore
}) );
.returning();
recomputeUserNegativityScore(userId); recomputeUserNegativityScore(userId);

View file

@ -1,10 +1,33 @@
import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index' import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'; import { z } from 'zod'
import dayjs from 'dayjs'; import dayjs from 'dayjs'
import { db } from '@/src/db/db_index'
import { vendorSnippets, deliverySlotInfo, productInfo, orders, orderItems, users, orderStatus } from '@/src/db/schema'
import { eq, and, inArray, isNotNull, gt, sql, asc, ne } from 'drizzle-orm';
import { appUrl } from '@/src/lib/env-exporter' import { appUrl } from '@/src/lib/env-exporter'
import {
checkVendorSnippetExists as checkVendorSnippetExistsInDb,
getVendorSnippetById as getVendorSnippetByIdInDb,
getVendorSnippetByCode as getVendorSnippetByCodeInDb,
getAllVendorSnippets as getAllVendorSnippetsInDb,
createVendorSnippet as createVendorSnippetInDb,
updateVendorSnippet as updateVendorSnippetInDb,
deleteVendorSnippet as deleteVendorSnippetInDb,
getProductsByIds as getProductsByIdsInDb,
getVendorSlotById as getVendorSlotByIdInDb,
getVendorOrdersBySlotId as getVendorOrdersBySlotIdInDb,
getVendorOrders as getVendorOrdersInDb,
updateVendorOrderItemPackaging as updateVendorOrderItemPackagingInDb,
getSlotsAfterDate as getSlotsAfterDateInDb,
} from '@/src/dbService'
import type {
AdminVendorSnippet,
AdminVendorSnippetWithProducts,
AdminVendorSnippetWithSlot,
AdminVendorSnippetDeleteResult,
AdminVendorSnippetOrdersResult,
AdminVendorSnippetOrdersWithSlotResult,
AdminVendorOrderSummary,
AdminUpcomingSlotsResult,
AdminVendorUpdatePackagingResult,
} from '@packages/shared'
const createSnippetSchema = z.object({ const createSnippetSchema = z.object({
snippetCode: z.string().min(1, "Snippet code is required"), snippetCode: z.string().min(1, "Snippet code is required"),
@ -26,7 +49,7 @@ const updateSnippetSchema = z.object({
export const vendorSnippetsRouter = router({ export const vendorSnippetsRouter = router({
create: protectedProcedure create: protectedProcedure
.input(createSnippetSchema) .input(createSnippetSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<AdminVendorSnippet> => {
const { snippetCode, slotId, productIds, validTill, isPermanent } = input; const { snippetCode, slotId, productIds, validTill, isPermanent } = input;
// Get staff user ID from auth middleware // Get staff user ID from auth middleware
@ -35,6 +58,33 @@ export const vendorSnippetsRouter = router({
throw new Error("Unauthorized"); throw new Error("Unauthorized");
} }
if(slotId) {
const slot = await getVendorSlotByIdInDb(slotId)
if (!slot) {
throw new Error("Invalid slot ID")
}
}
const products = await getProductsByIdsInDb(productIds)
if (products.length !== productIds.length) {
throw new Error("One or more invalid product IDs")
}
const existingSnippet = await checkVendorSnippetExistsInDb(snippetCode)
if (existingSnippet) {
throw new Error("Snippet code already exists")
}
const result = await createVendorSnippetInDb({
snippetCode,
slotId,
productIds,
isPermanent,
validTill: validTill ? new Date(validTill) : undefined,
})
/*
// Old implementation - direct DB queries:
// Validate slot exists // Validate slot exists
if(slotId) { if(slotId) {
const slot = await db.query.deliverySlotInfo.findFirst({ const slot = await db.query.deliverySlotInfo.findFirst({
@ -70,13 +120,32 @@ export const vendorSnippetsRouter = router({
}).returning(); }).returning();
return result[0]; return result[0];
*/
return result
}), }),
getAll: protectedProcedure getAll: protectedProcedure
.query(async () => { .query(async (): Promise<AdminVendorSnippetWithProducts[]> => {
console.log('from the vendor snipptes methods') console.log('from the vendor snipptes methods')
try { try {
const result = await getAllVendorSnippetsInDb()
const snippetsWithProducts = await Promise.all(
result.map(async (snippet) => {
const products = await getProductsByIdsInDb(snippet.productIds)
return {
...snippet,
accessUrl: `${appUrl}/vendor-order-list?id=${snippet.snippetCode}`,
products,
}
})
)
/*
// Old implementation - direct DB queries:
const result = await db.query.vendorSnippets.findMany({ const result = await db.query.vendorSnippets.findMany({
with: { with: {
slot: true, slot: true,
@ -100,18 +169,25 @@ export const vendorSnippetsRouter = router({
); );
return snippetsWithProducts; return snippetsWithProducts;
*/
return snippetsWithProducts
} }
catch(e) { catch(e) {
console.log(e) console.log(e)
} }
return []; return []
}), }),
getById: protectedProcedure getById: protectedProcedure
.input(z.object({ id: z.number().int().positive() })) .input(z.object({ id: z.number().int().positive() }))
.query(async ({ input }) => { .query(async ({ input }): Promise<AdminVendorSnippetWithSlot> => {
const { id } = input; const { id } = input;
const result = await getVendorSnippetByIdInDb(id)
/*
// Old implementation - direct DB queries:
const result = await db.query.vendorSnippets.findFirst({ const result = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.id, id), where: eq(vendorSnippets.id, id),
with: { with: {
@ -124,14 +200,57 @@ export const vendorSnippetsRouter = router({
} }
return result; return result;
*/
if (!result) {
throw new Error('Vendor snippet not found')
}
return result
}), }),
update: protectedProcedure update: protectedProcedure
.input(updateSnippetSchema) .input(updateSnippetSchema)
.mutation(async ({ input }) => { .mutation(async ({ input }): Promise<AdminVendorSnippet> => {
const { id, updates } = input; const { id, updates } = input;
// Check if snippet exists const existingSnippet = await getVendorSnippetByIdInDb(id)
if (!existingSnippet) {
throw new Error('Vendor snippet not found')
}
if (updates.slotId) {
const slot = await getVendorSlotByIdInDb(updates.slotId)
if (!slot) {
throw new Error('Invalid slot ID')
}
}
if (updates.productIds) {
const products = await getProductsByIdsInDb(updates.productIds)
if (products.length !== updates.productIds.length) {
throw new Error('One or more invalid product IDs')
}
}
if (updates.snippetCode && updates.snippetCode !== existingSnippet.snippetCode) {
const duplicateSnippet = await checkVendorSnippetExistsInDb(updates.snippetCode)
if (duplicateSnippet) {
throw new Error('Snippet code already exists')
}
}
const updateData = {
...updates,
validTill: updates.validTill !== undefined
? (updates.validTill ? new Date(updates.validTill) : null)
: undefined,
}
const result = await updateVendorSnippetInDb(id, updateData)
/*
// Old implementation - direct DB queries:
const existingSnippet = await db.query.vendorSnippets.findFirst({ const existingSnippet = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.id, id), where: eq(vendorSnippets.id, id),
}); });
@ -184,13 +303,24 @@ export const vendorSnippetsRouter = router({
} }
return result[0]; return result[0];
*/
if (!result) {
throw new Error('Failed to update vendor snippet')
}
return result
}), }),
delete: protectedProcedure delete: protectedProcedure
.input(z.object({ id: z.number().int().positive() })) .input(z.object({ id: z.number().int().positive() }))
.mutation(async ({ input }) => { .mutation(async ({ input }): Promise<AdminVendorSnippetDeleteResult> => {
const { id } = input; const { id } = input;
const result = await deleteVendorSnippetInDb(id)
/*
// Old implementation - direct DB queries:
const result = await db.delete(vendorSnippets) const result = await db.delete(vendorSnippets)
.where(eq(vendorSnippets.id, id)) .where(eq(vendorSnippets.id, id))
.returning(); .returning();
@ -200,15 +330,26 @@ export const vendorSnippetsRouter = router({
} }
return { message: "Vendor snippet deleted successfully" }; return { message: "Vendor snippet deleted successfully" };
*/
if (!result) {
throw new Error('Vendor snippet not found')
}
return { message: 'Vendor snippet deleted successfully' }
}), }),
getOrdersBySnippet: publicProcedure getOrdersBySnippet: publicProcedure
.input(z.object({ .input(z.object({
snippetCode: z.string().min(1, "Snippet code is required") snippetCode: z.string().min(1, "Snippet code is required")
})) }))
.query(async ({ input }) => { .query(async ({ input }): Promise<AdminVendorSnippetOrdersResult> => {
const { snippetCode } = input; const { snippetCode } = input;
const snippet = await getVendorSnippetByCodeInDb(snippetCode)
/*
// Old implementation - direct DB queries:
// Find the snippet // Find the snippet
const snippet = await db.query.vendorSnippets.findFirst({ const snippet = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.snippetCode, snippetCode), where: eq(vendorSnippets.snippetCode, snippetCode),
@ -242,6 +383,21 @@ export const vendorSnippetsRouter = router({
}, },
orderBy: (orders, { desc }) => [desc(orders.createdAt)], orderBy: (orders, { desc }) => [desc(orders.createdAt)],
}); });
*/
if (!snippet) {
throw new Error('Vendor snippet not found')
}
if (snippet.validTill && new Date(snippet.validTill) < new Date()) {
throw new Error('Vendor snippet has expired')
}
if (!snippet.slotId) {
throw new Error('Vendor snippet not associated with a slot')
}
const matchingOrders = await getVendorOrdersBySlotIdInDb(snippet.slotId)
// Filter orders that contain at least one of the snippet's products // Filter orders that contain at least one of the snippet's products
const filteredOrders = matchingOrders.filter(order => { const filteredOrders = matchingOrders.filter(order => {
@ -273,11 +429,11 @@ export const vendorSnippetsRouter = router({
const orderTotal = products.reduce((sum, p) => sum + p.subtotal, 0); const orderTotal = products.reduce((sum, p) => sum + p.subtotal, 0);
return { return {
orderId: `ORD${order.id}`, orderId: `ORD${order.id}`,
orderDate: order.createdAt.toISOString(), orderDate: order.createdAt.toISOString(),
customerName: order.user.name, customerName: order.user.name || '',
totalAmount: orderTotal, totalAmount: orderTotal,
slotInfo: order.slot ? { slotInfo: order.slot ? {
time: order.slot.deliveryTime.toISOString(), time: order.slot.deliveryTime.toISOString(),
sequence: order.slot.deliverySequence, sequence: order.slot.deliverySequence,
@ -300,11 +456,15 @@ export const vendorSnippetsRouter = router({
createdAt: snippet.createdAt.toISOString(), createdAt: snippet.createdAt.toISOString(),
isPermanent: snippet.isPermanent, isPermanent: snippet.isPermanent,
}, },
}; }
}), }),
getVendorOrders: protectedProcedure getVendorOrders: protectedProcedure
.query(async () => { .query(async (): Promise<AdminVendorOrderSummary[]> => {
const vendorOrders = await getVendorOrdersInDb()
/*
// Old implementation - direct DB queries:
const vendorOrders = await db.query.orders.findMany({ const vendorOrders = await db.query.orders.findMany({
with: { with: {
user: true, user: true,
@ -320,10 +480,11 @@ export const vendorSnippetsRouter = router({
}, },
orderBy: (orders, { desc }) => [desc(orders.createdAt)], orderBy: (orders, { desc }) => [desc(orders.createdAt)],
}); });
*/
return vendorOrders.map(order => ({ return vendorOrders.map(order => ({
id: order.id, id: order.id,
status: 'pending', // Default status since orders table may not have status field status: 'pending',
orderDate: order.createdAt.toISOString(), orderDate: order.createdAt.toISOString(),
totalQuantity: order.orderItems.reduce((sum, item) => sum + parseFloat(item.quantity || '0'), 0), totalQuantity: order.orderItems.reduce((sum, item) => sum + parseFloat(item.quantity || '0'), 0),
products: order.orderItems.map(item => ({ products: order.orderItems.map(item => ({
@ -331,12 +492,16 @@ export const vendorSnippetsRouter = router({
quantity: parseFloat(item.quantity || '0'), quantity: parseFloat(item.quantity || '0'),
unit: item.product.unit?.shortNotation || 'unit', unit: item.product.unit?.shortNotation || 'unit',
})), })),
})); }))
}), }),
getUpcomingSlots: publicProcedure getUpcomingSlots: publicProcedure
.query(async () => { .query(async (): Promise<AdminUpcomingSlotsResult> => {
const threeHoursAgo = dayjs().subtract(3, 'hour').toDate(); const threeHoursAgo = dayjs().subtract(3, 'hour').toDate();
const slots = await getSlotsAfterDateInDb(threeHoursAgo)
/*
// Old implementation - direct DB queries:
const slots = await db.query.deliverySlotInfo.findMany({ const slots = await db.query.deliverySlotInfo.findMany({
where: and( where: and(
eq(deliverySlotInfo.isActive, true), eq(deliverySlotInfo.isActive, true),
@ -344,6 +509,7 @@ export const vendorSnippetsRouter = router({
), ),
orderBy: asc(deliverySlotInfo.deliveryTime), orderBy: asc(deliverySlotInfo.deliveryTime),
}); });
*/
return { return {
success: true, success: true,
@ -353,7 +519,7 @@ export const vendorSnippetsRouter = router({
freezeTime: slot.freezeTime.toISOString(), freezeTime: slot.freezeTime.toISOString(),
deliverySequence: slot.deliverySequence, deliverySequence: slot.deliverySequence,
})), })),
}; }
}), }),
getOrdersBySnippetAndSlot: publicProcedure getOrdersBySnippetAndSlot: publicProcedure
@ -361,9 +527,14 @@ export const vendorSnippetsRouter = router({
snippetCode: z.string().min(1, "Snippet code is required"), snippetCode: z.string().min(1, "Snippet code is required"),
slotId: z.number().int().positive("Valid slot ID is required"), slotId: z.number().int().positive("Valid slot ID is required"),
})) }))
.query(async ({ input }) => { .query(async ({ input }): Promise<AdminVendorSnippetOrdersWithSlotResult> => {
const { snippetCode, slotId } = input; const { snippetCode, slotId } = input;
const snippet = await getVendorSnippetByCodeInDb(snippetCode)
const slot = await getVendorSlotByIdInDb(slotId)
/*
// Old implementation - direct DB queries:
// Find the snippet // Find the snippet
const snippet = await db.query.vendorSnippets.findFirst({ const snippet = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.snippetCode, snippetCode), where: eq(vendorSnippets.snippetCode, snippetCode),
@ -401,6 +572,17 @@ export const vendorSnippetsRouter = router({
}, },
orderBy: (orders, { desc }) => [desc(orders.createdAt)], orderBy: (orders, { desc }) => [desc(orders.createdAt)],
}); });
*/
if (!snippet) {
throw new Error('Vendor snippet not found')
}
if (!slot) {
throw new Error('Slot not found')
}
const matchingOrders = await getVendorOrdersBySlotIdInDb(slotId)
// Filter orders that contain at least one of the snippet's products // Filter orders that contain at least one of the snippet's products
const filteredOrders = matchingOrders.filter(order => { const filteredOrders = matchingOrders.filter(order => {
@ -435,7 +617,7 @@ export const vendorSnippetsRouter = router({
return { return {
orderId: `ORD${order.id}`, orderId: `ORD${order.id}`,
orderDate: order.createdAt.toISOString(), orderDate: order.createdAt.toISOString(),
customerName: order.user.name, customerName: order.user.name || '',
totalAmount: orderTotal, totalAmount: orderTotal,
slotInfo: order.slot ? { slotInfo: order.slot ? {
time: order.slot.deliveryTime.toISOString(), time: order.slot.deliveryTime.toISOString(),
@ -465,7 +647,7 @@ export const vendorSnippetsRouter = router({
freezeTime: slot.freezeTime.toISOString(), freezeTime: slot.freezeTime.toISOString(),
deliverySequence: slot.deliverySequence, deliverySequence: slot.deliverySequence,
}, },
}; }
}), }),
updateOrderItemPackaging: publicProcedure updateOrderItemPackaging: publicProcedure
@ -473,7 +655,7 @@ export const vendorSnippetsRouter = router({
orderItemId: z.number().int().positive("Valid order item ID required"), orderItemId: z.number().int().positive("Valid order item ID required"),
is_packaged: z.boolean() is_packaged: z.boolean()
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<AdminVendorUpdatePackagingResult> => {
const { orderItemId, is_packaged } = input; const { orderItemId, is_packaged } = input;
// Get staff user ID from auth middleware // Get staff user ID from auth middleware
@ -482,6 +664,10 @@ export const vendorSnippetsRouter = router({
// throw new Error("Unauthorized"); // throw new Error("Unauthorized");
// } // }
const result = await updateVendorOrderItemPackagingInDb(orderItemId, is_packaged)
/*
// Old implementation - direct DB queries:
// Check if order item exists and get related data // Check if order item exists and get related data
const orderItem = await db.query.orderItems.findFirst({ const orderItem = await db.query.orderItems.findFirst({
where: eq(orderItems.id, orderItemId), where: eq(orderItems.id, orderItemId),
@ -527,5 +713,12 @@ export const vendorSnippetsRouter = router({
orderItemId, orderItemId,
is_packaged is_packaged
}; };
*/
if (!result.success) {
throw new Error(result.message)
}
return result
}), }),
}); });

View file

@ -9,9 +9,32 @@ import { generateUploadUrl } from '@/src/lib/s3-client'
import { ApiError } from '@/src/lib/api-error' import { ApiError } from '@/src/lib/api-error'
import { getAllConstValues } from '@/src/lib/const-store' import { getAllConstValues } from '@/src/lib/const-store'
import { CONST_KEYS } from '@/src/lib/const-keys' import { CONST_KEYS } from '@/src/lib/const-keys'
import { assetsDomain, apiCacheKey } from '@/src/lib/env-exporter'
const polygon = turf.polygon(mbnrGeoJson.features[0].geometry.coordinates); const polygon = turf.polygon(mbnrGeoJson.features[0].geometry.coordinates);
export async function scaffoldEssentialConsts() {
const consts = await getAllConstValues();
return {
freeDeliveryThreshold: consts[CONST_KEYS.freeDeliveryThreshold] ?? 200,
deliveryCharge: consts[CONST_KEYS.deliveryCharge] ?? 0,
flashFreeDeliveryThreshold: consts[CONST_KEYS.flashFreeDeliveryThreshold] ?? 500,
flashDeliveryCharge: consts[CONST_KEYS.flashDeliveryCharge] ?? 69,
popularItems: consts[CONST_KEYS.popularItems] ?? '5,3,2,4,1',
versionNum: consts[CONST_KEYS.versionNum] ?? '1.1.0',
playStoreUrl: consts[CONST_KEYS.playStoreUrl] ?? 'https://play.google.com/store/apps/details?id=in.freshyo.app',
appStoreUrl: consts[CONST_KEYS.appStoreUrl] ?? 'https://apps.apple.com/in/app/freshyo/id6756889077',
webViewHtml: null,
isWebviewClosable: true,
isFlashDeliveryEnabled: consts[CONST_KEYS.isFlashDeliveryEnabled] ?? true,
supportMobile: consts[CONST_KEYS.supportMobile] ?? '',
supportEmail: consts[CONST_KEYS.supportEmail] ?? '',
assetsDomain,
apiCacheKey,
};
}
export const commonApiRouter = router({ export const commonApiRouter = router({
product: commonRouter, product: commonRouter,
getStoresSummary: publicProcedure getStoresSummary: publicProcedure
@ -99,23 +122,8 @@ export const commonApiRouter = router({
}), }),
essentialConsts: publicProcedure essentialConsts: publicProcedure
.query(async () => { .query(async () => {
const consts = await getAllConstValues(); const response = await scaffoldEssentialConsts();
return response;
return {
freeDeliveryThreshold: consts[CONST_KEYS.freeDeliveryThreshold] ?? 200,
deliveryCharge: consts[CONST_KEYS.deliveryCharge] ?? 0,
flashFreeDeliveryThreshold: consts[CONST_KEYS.flashFreeDeliveryThreshold] ?? 500,
flashDeliveryCharge: consts[CONST_KEYS.flashDeliveryCharge] ?? 69,
popularItems: consts[CONST_KEYS.popularItems] ?? '5,3,2,4,1',
versionNum: consts[CONST_KEYS.versionNum] ?? '1.1.0',
playStoreUrl: consts[CONST_KEYS.playStoreUrl] ?? 'https://play.google.com/store/apps/details?id=in.freshyo.app',
appStoreUrl: consts[CONST_KEYS.appStoreUrl] ?? 'https://apps.apple.com/in/app/freshyo/id6756889077',
webViewHtml: null,
isWebviewClosable: true,
isFlashDeliveryEnabled: consts[CONST_KEYS.isFlashDeliveryEnabled] ?? true,
supportMobile: consts[CONST_KEYS.supportMobile] ?? '',
supportEmail: consts[CONST_KEYS.supportEmail] ?? '',
};
}), }),
}); });

View file

@ -1,12 +1,10 @@
import { router, publicProcedure } from '@/src/trpc/trpc-index' import { router, publicProcedure } from '@/src/trpc/trpc-index'
import { db } from '@/src/db/db_index' import { db } from '@/src/db/db_index'
import { productInfo, units, productSlots, deliverySlotInfo, storeInfo, productTags, productTagInfo } from '@/src/db/schema' import { productInfo, units, productSlots, deliverySlotInfo, storeInfo } from '@/src/db/schema'
import { eq, gt, and, sql, inArray } from 'drizzle-orm'; import { eq, gt, and, sql, inArray } from 'drizzle-orm';
import { generateSignedUrlsFromS3Urls, generateSignedUrlFromS3Url } from '@/src/lib/s3-client' import { generateSignedUrlsFromS3Urls, generateSignedUrlFromS3Url } from '@/src/lib/s3-client'
import { z } from 'zod';
import { getAllProducts as getAllProductsFromCache } from '@/src/stores/product-store' import { getAllProducts as getAllProductsFromCache } from '@/src/stores/product-store'
import { getDashboardTags as getDashboardTagsFromCache } from '@/src/stores/product-tag-store' import { getDashboardTags as getDashboardTagsFromCache } from '@/src/stores/product-tag-store'
import Fuse from 'fuse.js';
export const getNextDeliveryDate = async (productId: number): Promise<Date | null> => { export const getNextDeliveryDate = async (productId: number): Promise<Date | null> => {
const result = await db const result = await db
@ -28,9 +26,51 @@ export const getNextDeliveryDate = async (productId: number): Promise<Date | nul
return result[0]?.deliveryTime || null; return result[0]?.deliveryTime || null;
}; };
export async function scaffoldProducts() {
// Get all products from cache
let products = await getAllProductsFromCache();
products = products.filter(item => Boolean(item.id))
// Get suspended product IDs to filter them out
const suspendedProducts = await db
.select({ id: productInfo.id })
.from(productInfo)
.where(eq(productInfo.isSuspended, true));
const suspendedProductIds = new Set(suspendedProducts.map(sp => sp.id));
// Filter out suspended products
products = products.filter(product => !suspendedProductIds.has(product.id));
// Format products to match the expected response structure
const formattedProducts = await Promise.all(
products.map(async (product) => {
const nextDeliveryDate = await getNextDeliveryDate(product.id);
return {
id: product.id,
name: product.name,
shortDescription: product.shortDescription,
price: parseFloat(product.price),
marketPrice: product.marketPrice ? parseFloat(product.marketPrice) : null,
unit: product.unitNotation,
unitNotation: product.unitNotation,
incrementStep: product.incrementStep,
productQuantity: product.productQuantity,
storeId: product.store?.id || null,
isOutOfStock: product.isOutOfStock,
isFlashAvailable: product.isFlashAvailable,
nextDeliveryDate: nextDeliveryDate ? nextDeliveryDate.toISOString() : null,
images: product.images,
flashPrice: product.flashPrice
};
})
);
return {
products: formattedProducts,
count: formattedProducts.length,
};
}
export const commonRouter = router({ export const commonRouter = router({
getDashboardTags: publicProcedure getDashboardTags: publicProcedure
@ -44,89 +84,9 @@ export const commonRouter = router({
}), }),
getAllProductsSummary: publicProcedure getAllProductsSummary: publicProcedure
.input(z.object({ .query(async () => {
searchQuery: z.string().optional(), const response = await scaffoldProducts();
tagId: z.number().optional() return response;
}))
.query(async ({ input }) => {
const { searchQuery, tagId } = input;
// Get all products from cache
let products = await getAllProductsFromCache();
products = products.filter(item => Boolean(item.id))
// Apply tag filtering if tagId is provided
if (tagId) {
// Get products that have this tag from the database
const taggedProducts = await db
.select({ productId: productTags.productId })
.from(productTags)
.where(eq(productTags.tagId, tagId));
const taggedProductIds = new Set(taggedProducts.map(tp => tp.productId));
// Filter products based on tag
products = products.filter(product => taggedProductIds.has(product.id));
}
// Apply search filtering if searchQuery is provided using Fuse.js
if (searchQuery) {
const fuse = new Fuse(products, {
keys: [
'name',
'shortDescription',
'longDescription',
'store.name', // Search in store name too
'productTags', // Search in product tags too
],
threshold: 0.3, // Adjust fuzziness (0.0 = exact match, 1.0 = match anything)
includeScore: true,
shouldSort: true,
});
const fuseResults = fuse.search(searchQuery);
products = fuseResults.map(result => result.item);
}
// Get suspended product IDs to filter them out
const suspendedProducts = await db
.select({ id: productInfo.id })
.from(productInfo)
.where(eq(productInfo.isSuspended, true));
const suspendedProductIds = new Set(suspendedProducts.map(sp => sp.id));
// Filter out suspended products
products = products.filter(product => !suspendedProductIds.has(product.id));
// Format products to match the expected response structure
const formattedProducts = await Promise.all(
products.map(async (product) => {
const nextDeliveryDate = await getNextDeliveryDate(product.id);
return {
id: product.id,
name: product.name,
shortDescription: product.shortDescription,
price: parseFloat(product.price),
marketPrice: product.marketPrice ? parseFloat(product.marketPrice) : null,
unit: product.unitNotation,
unitNotation: product.unitNotation,
incrementStep: product.incrementStep,
productQuantity: product.productQuantity,
storeId: product.store?.id || null,
isOutOfStock: product.isOutOfStock,
isFlashAvailable: product.isFlashAvailable,
nextDeliveryDate: nextDeliveryDate ? nextDeliveryDate.toISOString() : null,
images: product.images, // Already signed URLs from cache
};
})
);
return {
products: formattedProducts,
count: formattedProducts.length,
};
}), }),
getStoresSummary: publicProcedure getStoresSummary: publicProcedure

View file

@ -1,30 +1,52 @@
import { router, protectedProcedure } from '@/src/trpc/trpc-index'; import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'; import { z } from 'zod'
import { db } from '@/src/db/db_index'; import { extractCoordsFromRedirectUrl } from '@/src/lib/license-util'
import { addresses, orders, orderStatus, deliverySlotInfo } from '@/src/db/schema'; import {
import { eq, and, gte } from 'drizzle-orm'; getUserDefaultAddress as getDefaultAddressInDb,
import dayjs from 'dayjs'; getUserAddresses as getUserAddressesInDb,
import { extractCoordsFromRedirectUrl } from '@/src/lib/license-util'; getUserAddressById as getUserAddressByIdInDb,
clearUserDefaultAddress as clearDefaultAddressInDb,
createUserAddress as createUserAddressInDb,
updateUserAddress as updateUserAddressInDb,
deleteUserAddress as deleteUserAddressInDb,
hasOngoingOrdersForAddress as hasOngoingOrdersForAddressInDb,
} from '@/src/dbService'
import type {
UserAddressResponse,
UserAddressesResponse,
UserAddressDeleteResponse,
} from '@packages/shared'
export const addressRouter = router({ export const addressRouter = router({
getDefaultAddress: protectedProcedure getDefaultAddress: protectedProcedure
.query(async ({ ctx }) => { .query(async ({ ctx }): Promise<UserAddressResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
const defaultAddress = await getDefaultAddressInDb(userId)
/*
// Old implementation - direct DB queries:
const [defaultAddress] = await db const [defaultAddress] = await db
.select() .select()
.from(addresses) .from(addresses)
.where(and(eq(addresses.userId, userId), eq(addresses.isDefault, true))) .where(and(eq(addresses.userId, userId), eq(addresses.isDefault, true)))
.limit(1); .limit(1);
*/
return { success: true, data: defaultAddress || null }; return { success: true, data: defaultAddress }
}), }),
getUserAddresses: protectedProcedure getUserAddresses: protectedProcedure
.query(async ({ ctx }) => { .query(async ({ ctx }): Promise<UserAddressesResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
const userAddresses = await getUserAddressesInDb(userId)
/*
// Old implementation - direct DB queries:
const userAddresses = await db.select().from(addresses).where(eq(addresses.userId, userId)); const userAddresses = await db.select().from(addresses).where(eq(addresses.userId, userId));
return { success: true, data: userAddresses }; */
return { success: true, data: userAddresses }
}), }),
createAddress: protectedProcedure createAddress: protectedProcedure
@ -41,7 +63,7 @@ export const addressRouter = router({
longitude: z.number().optional(), longitude: z.number().optional(),
googleMapsUrl: z.string().optional(), googleMapsUrl: z.string().optional(),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<UserAddressResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
const { name, phone, addressLine1, addressLine2, city, state, pincode, isDefault, googleMapsUrl } = input; const { name, phone, addressLine1, addressLine2, city, state, pincode, isDefault, googleMapsUrl } = input;
@ -61,6 +83,27 @@ export const addressRouter = router({
} }
// If setting as default, unset other defaults // If setting as default, unset other defaults
if (isDefault) {
await clearDefaultAddressInDb(userId)
}
const newAddress = await createUserAddressInDb({
userId,
name,
phone,
addressLine1,
addressLine2,
city,
state,
pincode,
isDefault: isDefault || false,
latitude,
longitude,
googleMapsUrl,
})
/*
// Old implementation - direct DB queries:
if (isDefault) { if (isDefault) {
await db.update(addresses).set({ isDefault: false }).where(eq(addresses.userId, userId)); await db.update(addresses).set({ isDefault: false }).where(eq(addresses.userId, userId));
} }
@ -79,8 +122,9 @@ export const addressRouter = router({
longitude, longitude,
googleMapsUrl, googleMapsUrl,
}).returning(); }).returning();
*/
return { success: true, data: newAddress }; return { success: true, data: newAddress }
}), }),
updateAddress: protectedProcedure updateAddress: protectedProcedure
@ -98,7 +142,7 @@ export const addressRouter = router({
longitude: z.number().optional(), longitude: z.number().optional(),
googleMapsUrl: z.string().optional(), googleMapsUrl: z.string().optional(),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<UserAddressResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
const { id, name, phone, addressLine1, addressLine2, city, state, pincode, isDefault, googleMapsUrl } = input; const { id, name, phone, addressLine1, addressLine2, city, state, pincode, isDefault, googleMapsUrl } = input;
@ -113,12 +157,34 @@ export const addressRouter = router({
} }
// Check if address exists and belongs to user // Check if address exists and belongs to user
const existingAddress = await db.select().from(addresses).where(and(eq(addresses.id, id), eq(addresses.userId, userId))).limit(1); const existingAddress = await getUserAddressByIdInDb(userId, id)
if (existingAddress.length === 0) { if (!existingAddress) {
throw new Error('Address not found'); throw new Error('Address not found')
} }
// If setting as default, unset other defaults // If setting as default, unset other defaults
if (isDefault) {
await clearDefaultAddressInDb(userId)
}
const updatedAddress = await updateUserAddressInDb({
userId,
addressId: id,
name,
phone,
addressLine1,
addressLine2,
city,
state,
pincode,
isDefault: isDefault || false,
googleMapsUrl,
latitude,
longitude,
})
/*
// Old implementation - direct DB queries:
if (isDefault) { if (isDefault) {
await db.update(addresses).set({ isDefault: false }).where(eq(addresses.userId, userId)); await db.update(addresses).set({ isDefault: false }).where(eq(addresses.userId, userId));
} }
@ -143,25 +209,42 @@ export const addressRouter = router({
} }
const [updatedAddress] = await db.update(addresses).set(updateData).where(and(eq(addresses.id, id), eq(addresses.userId, userId))).returning(); const [updatedAddress] = await db.update(addresses).set(updateData).where(and(eq(addresses.id, id), eq(addresses.userId, userId))).returning();
*/
return { success: true, data: updatedAddress }; return { success: true, data: updatedAddress }
}), }),
deleteAddress: protectedProcedure deleteAddress: protectedProcedure
.input(z.object({ .input(z.object({
id: z.number().int().positive(), id: z.number().int().positive(),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<UserAddressDeleteResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
const { id } = input; const { id } = input;
// Check if address exists and belongs to user const existingAddress = await getUserAddressByIdInDb(userId, id)
if (!existingAddress) {
throw new Error('Address not found or does not belong to user')
}
const hasOngoingOrders = await hasOngoingOrdersForAddressInDb(id)
if (hasOngoingOrders) {
throw new Error('Address is attached to an ongoing order. Please cancel the order first.')
}
if (existingAddress.isDefault) {
throw new Error('Cannot delete default address. Please set another address as default first.')
}
const deleted = await deleteUserAddressInDb(userId, id)
/*
// Old implementation - direct DB queries:
const existingAddress = await db.select().from(addresses).where(and(eq(addresses.id, id), eq(addresses.userId, userId))).limit(1); const existingAddress = await db.select().from(addresses).where(and(eq(addresses.id, id), eq(addresses.userId, userId))).limit(1);
if (existingAddress.length === 0) { if (existingAddress.length === 0) {
throw new Error('Address not found or does not belong to user'); throw new Error('Address not found or does not belong to user');
} }
// Check if address is attached to any ongoing orders using joins
const ongoingOrders = await db.select({ const ongoingOrders = await db.select({
order: orders, order: orders,
status: orderStatus, status: orderStatus,
@ -181,14 +264,17 @@ export const addressRouter = router({
throw new Error('Address is attached to an ongoing order. Please cancel the order first.'); throw new Error('Address is attached to an ongoing order. Please cancel the order first.');
} }
// Prevent deletion of default address
if (existingAddress[0].isDefault) { if (existingAddress[0].isDefault) {
throw new Error('Cannot delete default address. Please set another address as default first.'); throw new Error('Cannot delete default address. Please set another address as default first.');
} }
// Delete the address
await db.delete(addresses).where(and(eq(addresses.id, id), eq(addresses.userId, userId))); await db.delete(addresses).where(and(eq(addresses.id, id), eq(addresses.userId, userId)));
*/
return { success: true, message: 'Address deleted successfully' }; if (!deleted) {
throw new Error('Address not found or does not belong to user')
}
return { success: true, message: 'Address deleted successfully' }
}), }),
}); });

View file

@ -1,23 +1,33 @@
import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index'; import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'; import { z } from 'zod'
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs'
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken'
import { eq } from 'drizzle-orm'; import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client'
import { db } from '@/src/db/db_index'; import { ApiError } from '@/src/lib/api-error'
import { jwtSecret } from '@/src/lib/env-exporter'
import { sendOtp, verifyOtpUtil, getOtpCreds } from '@/src/lib/otp-utils'
import { import {
users, userCreds, userDetails, addresses, cartItems, complaints, getUserAuthByEmail as getUserAuthByEmailInDb,
couponApplicableUsers, couponUsage, notifCreds, notifications, getUserAuthByMobile as getUserAuthByMobileInDb,
orderItems, orderStatus, orders, payments, refunds, getUserAuthById as getUserAuthByIdInDb,
productReviews, reservedCoupons getUserAuthCreds as getUserAuthCredsInDb,
} from '@/src/db/schema'; getUserAuthDetails as getUserAuthDetailsInDb,
import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client'; createUserAuthWithCreds as createUserAuthWithCredsInDb,
import { ApiError } from '@/src/lib/api-error'; createUserAuthWithMobile as createUserAuthWithMobileInDb,
import catchAsync from '@/src/lib/catch-async'; upsertUserAuthPassword as upsertUserAuthPasswordInDb,
import { jwtSecret } from '@/src/lib/env-exporter'; deleteUserAuthAccount as deleteUserAuthAccountInDb,
import { sendOtp, verifyOtpUtil, getOtpCreds } from '@/src/lib/otp-utils'; } from '@/src/dbService'
import type {
UserAuthResult,
UserAuthResponse,
UserOtpVerifyResponse,
UserPasswordUpdateResponse,
UserProfileResponse,
UserDeleteAccountResponse,
} from '@packages/shared'
interface LoginRequest { interface LoginRequest {
identifier: string; // email or mobile identifier: string;
password: string; password: string;
} }
@ -28,22 +38,6 @@ interface RegisterRequest {
password: string; password: string;
} }
interface AuthResponse {
token: string;
user: {
id: number;
name?: string | null;
email: string | null;
mobile: string | null;
createdAt: string;
profileImage: string | null;
bio?: string | null;
dateOfBirth?: string | null;
gender?: string | null;
occupation?: string | null;
};
}
const generateToken = (userId: number): string => { const generateToken = (userId: number): string => {
const secret = jwtSecret; const secret = jwtSecret;
if (!secret) { if (!secret) {
@ -61,7 +55,7 @@ export const authRouter = router({
identifier: z.string().min(1, 'Email/mobile is required'), identifier: z.string().min(1, 'Email/mobile is required'),
password: z.string().min(1, 'Password is required'), password: z.string().min(1, 'Password is required'),
})) }))
.mutation(async ({ input }) => { .mutation(async ({ input }): Promise<UserAuthResult> => {
const { identifier, password }: LoginRequest = input; const { identifier, password }: LoginRequest = input;
if (!identifier || !password) { if (!identifier || !password) {
@ -69,22 +63,13 @@ export const authRouter = router({
} }
// Find user by email or mobile // Find user by email or mobile
const [user] = await db const user = await getUserAuthByEmailInDb(identifier.toLowerCase())
.select() let foundUser = user || null
.from(users)
.where(eq(users.email, identifier.toLowerCase()))
.limit(1);
let foundUser = user;
if (!foundUser) { if (!foundUser) {
// Try mobile if email didn't work // Try mobile if email didn't work
const [userByMobile] = await db const userByMobile = await getUserAuthByMobileInDb(identifier)
.select() foundUser = userByMobile || null
.from(users)
.where(eq(users.mobile, identifier))
.limit(1);
foundUser = userByMobile;
} }
if (!foundUser) { if (!foundUser) {
@ -92,22 +77,14 @@ export const authRouter = router({
} }
// Get user credentials // Get user credentials
const [userCredentials] = await db const userCredentials = await getUserAuthCredsInDb(foundUser.id)
.select()
.from(userCreds)
.where(eq(userCreds.userId, foundUser.id))
.limit(1);
if (!userCredentials) { if (!userCredentials) {
throw new ApiError('Account setup incomplete. Please contact support.', 401); throw new ApiError('Account setup incomplete. Please contact support.', 401);
} }
// Get user details for profile image // Get user details for profile image
const [userDetail] = await db const userDetail = await getUserAuthDetailsInDb(foundUser.id)
.select()
.from(userDetails)
.where(eq(userDetails.userId, foundUser.id))
.limit(1);
// Generate signed URL for profile image if it exists // Generate signed URL for profile image if it exists
const profileImageSignedUrl = userDetail?.profileImage const profileImageSignedUrl = userDetail?.profileImage
@ -122,7 +99,7 @@ export const authRouter = router({
const token = generateToken(foundUser.id); const token = generateToken(foundUser.id);
const response: AuthResponse = { const response: UserAuthResponse = {
token, token,
user: { user: {
id: foundUser.id, id: foundUser.id,
@ -141,7 +118,7 @@ export const authRouter = router({
return { return {
success: true, success: true,
data: response, data: response,
}; }
}), }),
register: publicProcedure register: publicProcedure
@ -151,7 +128,7 @@ export const authRouter = router({
mobile: z.string().min(1, 'Mobile is required'), mobile: z.string().min(1, 'Mobile is required'),
password: z.string().min(1, 'Password is required'), password: z.string().min(1, 'Password is required'),
})) }))
.mutation(async ({ input }) => { .mutation(async ({ input }): Promise<UserAuthResult> => {
const { name, email, mobile, password }: RegisterRequest = input; const { name, email, mobile, password }: RegisterRequest = input;
if (!name || !email || !mobile || !password) { if (!name || !email || !mobile || !password) {
@ -171,22 +148,14 @@ export const authRouter = router({
} }
// Check if email already exists // Check if email already exists
const [existingEmail] = await db const existingEmail = await getUserAuthByEmailInDb(email.toLowerCase())
.select()
.from(users)
.where(eq(users.email, email.toLowerCase()))
.limit(1);
if (existingEmail) { if (existingEmail) {
throw new ApiError('Email already registered', 409); throw new ApiError('Email already registered', 409);
} }
// Check if mobile already exists // Check if mobile already exists
const [existingMobile] = await db const existingMobile = await getUserAuthByMobileInDb(cleanMobile)
.select()
.from(users)
.where(eq(users.mobile, cleanMobile))
.limit(1);
if (existingMobile) { if (existingMobile) {
throw new ApiError('Mobile number already registered', 409); throw new ApiError('Mobile number already registered', 409);
@ -196,31 +165,16 @@ export const authRouter = router({
const hashedPassword = await bcrypt.hash(password, 12); const hashedPassword = await bcrypt.hash(password, 12);
// Create user and credentials in a transaction // Create user and credentials in a transaction
const newUser = await db.transaction(async (tx) => { const newUser = await createUserAuthWithCredsInDb({
// Create user name: name.trim(),
const [user] = await tx email: email.toLowerCase().trim(),
.insert(users) mobile: cleanMobile,
.values({ hashedPassword,
name: name.trim(), })
email: email.toLowerCase().trim(),
mobile: cleanMobile,
})
.returning();
// Create user credentials
await tx
.insert(userCreds)
.values({
userId: user.id,
userPassword: hashedPassword,
});
return user;
});
const token = generateToken(newUser.id); const token = generateToken(newUser.id);
const response: AuthResponse = { const response: UserAuthResponse = {
token, token,
user: { user: {
id: newUser.id, id: newUser.id,
@ -235,7 +189,7 @@ export const authRouter = router({
return { return {
success: true, success: true,
data: response, data: response,
}; }
}), }),
sendOtp: publicProcedure sendOtp: publicProcedure
@ -252,7 +206,7 @@ export const authRouter = router({
mobile: z.string(), mobile: z.string(),
otp: z.string(), otp: z.string(),
})) }))
.mutation(async ({ input }) => { .mutation(async ({ input }): Promise<UserOtpVerifyResponse> => {
const verificationId = getOtpCreds(input.mobile); const verificationId = getOtpCreds(input.mobile);
if (!verificationId) { if (!verificationId) {
throw new ApiError("OTP not sent or expired", 400); throw new ApiError("OTP not sent or expired", 400);
@ -264,45 +218,35 @@ export const authRouter = router({
} }
// Find user // Find user
let user = await db.query.users.findFirst({ let user = await getUserAuthByMobileInDb(input.mobile)
where: eq(users.mobile, input.mobile),
});
// If user doesn't exist, create one // If user doesn't exist, create one
if (!user) { if (!user) {
const [newUser] = await db user = await createUserAuthWithMobileInDb(input.mobile)
.insert(users) }
.values({
name: null,
email: null,
mobile: input.mobile,
})
.returning();
user = newUser;
}
// Generate JWT // Generate JWT
const token = generateToken(user.id); const token = generateToken(user.id);
return { return {
success: true, success: true,
token, token,
user: { user: {
id: user.id, id: user.id,
name: user.name, name: user.name,
email: user.email, email: user.email,
mobile: user.mobile, mobile: user.mobile,
createdAt: user.createdAt.toISOString(), createdAt: user.createdAt.toISOString(),
profileImage: null, profileImage: null,
}, },
}; }
}), }),
updatePassword: protectedProcedure updatePassword: protectedProcedure
.input(z.object({ .input(z.object({
password: z.string().min(6, 'Password must be at least 6 characters'), password: z.string().min(6, 'Password must be at least 6 characters'),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<UserPasswordUpdateResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
if (!userId) { if (!userId) {
throw new ApiError('User not authenticated', 401); throw new ApiError('User not authenticated', 401);
@ -311,41 +255,38 @@ export const authRouter = router({
const hashedPassword = await bcrypt.hash(input.password, 10); const hashedPassword = await bcrypt.hash(input.password, 10);
// Insert if not exists, then update if exists // Insert if not exists, then update if exists
await upsertUserAuthPasswordInDb(userId, hashedPassword)
/*
// Old implementation - direct DB queries:
try { try {
await db.insert(userCreds).values({ await db.insert(userCreds).values({
userId: userId, userId: userId,
userPassword: hashedPassword, userPassword: hashedPassword,
}); });
// Insert succeeded - new credentials created
} catch (error: any) { } catch (error: any) {
// Insert failed - check if it's a unique constraint violation if (error.code === '23505') {
if (error.code === '23505') { // PostgreSQL unique constraint violation
// Update existing credentials
await db.update(userCreds).set({ await db.update(userCreds).set({
userPassword: hashedPassword, userPassword: hashedPassword,
}).where(eq(userCreds.userId, userId)); }).where(eq(userCreds.userId, userId));
} else { } else {
// Re-throw if it's a different error
throw error; throw error;
} }
} }
*/
return { success: true, message: 'Password updated successfully' }; return { success: true, message: 'Password updated successfully' }
}), }),
getProfile: protectedProcedure getProfile: protectedProcedure
.query(async ({ ctx }) => { .query(async ({ ctx }): Promise<UserProfileResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
if (!userId) { if (!userId) {
throw new ApiError('User not authenticated', 401); throw new ApiError('User not authenticated', 401);
} }
const [user] = await db const user = await getUserAuthByIdInDb(userId)
.select()
.from(users)
.where(eq(users.id, userId))
.limit(1);
if (!user) { if (!user) {
throw new ApiError('User not found', 404); throw new ApiError('User not found', 404);
@ -359,14 +300,14 @@ export const authRouter = router({
email: user.email, email: user.email,
mobile: user.mobile, mobile: user.mobile,
}, },
}; }
}), }),
deleteAccount: protectedProcedure deleteAccount: protectedProcedure
.input(z.object({ .input(z.object({
mobile: z.string().min(10, 'Mobile number is required'), mobile: z.string().min(10, 'Mobile number is required'),
})) }))
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }): Promise<UserDeleteAccountResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
const { mobile } = input; const { mobile } = input;
@ -375,10 +316,7 @@ export const authRouter = router({
} }
// Double-check: verify user exists and is the authenticated user // Double-check: verify user exists and is the authenticated user
const existingUser = await db.query.users.findFirst({ const existingUser = await getUserAuthByIdInDb(userId)
where: eq(users.id, userId),
columns: { id: true, mobile: true },
});
if (!existingUser) { if (!existingUser) {
throw new ApiError('User not found', 404); throw new ApiError('User not found', 404);
@ -399,8 +337,11 @@ export const authRouter = router({
} }
// Use transaction for atomic deletion // Use transaction for atomic deletion
await deleteUserAuthAccountInDb(userId)
/*
// Old implementation - direct DB queries:
await db.transaction(async (tx) => { 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(notifCreds).where(eq(notifCreds.userId, userId));
await tx.delete(couponApplicableUsers).where(eq(couponApplicableUsers.userId, userId)); await tx.delete(couponApplicableUsers).where(eq(couponApplicableUsers.userId, userId));
await tx.delete(couponUsage).where(eq(couponUsage.userId, userId)); await tx.delete(couponUsage).where(eq(couponUsage.userId, userId));
@ -408,13 +349,10 @@ export const authRouter = router({
await tx.delete(cartItems).where(eq(cartItems.userId, userId)); await tx.delete(cartItems).where(eq(cartItems.userId, userId));
await tx.delete(notifications).where(eq(notifications.userId, userId)); await tx.delete(notifications).where(eq(notifications.userId, userId));
await tx.delete(productReviews).where(eq(productReviews.userId, userId)); await tx.delete(productReviews).where(eq(productReviews.userId, userId));
// Update reserved coupons (set redeemedBy to null)
await tx.update(reservedCoupons) await tx.update(reservedCoupons)
.set({ redeemedBy: null }) .set({ redeemedBy: null })
.where(eq(reservedCoupons.redeemedBy, userId)); .where(eq(reservedCoupons.redeemedBy, userId));
// Phase 2: Order dependencies
const userOrders = await tx const userOrders = await tx
.select({ id: orders.id }) .select({ id: orders.id })
.from(orders) .from(orders)
@ -425,23 +363,18 @@ export const authRouter = router({
await tx.delete(orderStatus).where(eq(orderStatus.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(payments).where(eq(payments.orderId, order.id));
await tx.delete(refunds).where(eq(refunds.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(couponUsage).where(eq(couponUsage.orderId, order.id));
await tx.delete(complaints).where(eq(complaints.orderId, order.id)); await tx.delete(complaints).where(eq(complaints.orderId, order.id));
} }
// Delete orders
await tx.delete(orders).where(eq(orders.userId, userId)); 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)); 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(userDetails).where(eq(userDetails.userId, userId));
await tx.delete(userCreds).where(eq(userCreds.userId, userId)); await tx.delete(userCreds).where(eq(userCreds.userId, userId));
await tx.delete(users).where(eq(users.id, userId)); await tx.delete(users).where(eq(users.id, userId));
}); });
*/
return { success: true, message: 'Account deleted successfully' }; return { success: true, message: 'Account deleted successfully' }
}), }),
}); });

View file

@ -1,38 +1,33 @@
import { db } from '@/src/db/db_index'; import { publicProcedure, router } from '@/src/trpc/trpc-index'
import { homeBanners } from '@/src/db/schema'; import { scaffoldAssetUrl } from '@/src/lib/s3-client'
import { publicProcedure, router } from '@/src/trpc/trpc-index'; import { getUserActiveBanners as getUserActiveBannersInDb } from '@/src/dbService'
import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client'; import type { UserBannersResponse } from '@packages/shared'
import { isNotNull, asc } from 'drizzle-orm';
export async function scaffoldBanners(): Promise<UserBannersResponse> {
const banners = await getUserActiveBannersInDb()
/*
// Old implementation - direct DB queries:
const banners = await db.query.homeBanners.findMany({
where: isNotNull(homeBanners.serialNum), // Only show assigned banners
orderBy: asc(homeBanners.serialNum), // Order by slot number 1-4
});
*/
const bannersWithSignedUrls = banners.map((banner) => ({
...banner,
imageUrl: banner.imageUrl ? scaffoldAssetUrl(banner.imageUrl) : banner.imageUrl,
}))
return {
banners: bannersWithSignedUrls,
}
}
export const bannerRouter = router({ export const bannerRouter = router({
getBanners: publicProcedure getBanners: publicProcedure
.query(async () => { .query(async () => {
const banners = await db.query.homeBanners.findMany({ const response = await scaffoldBanners();
where: isNotNull(homeBanners.serialNum), // Only show assigned banners return response;
orderBy: asc(homeBanners.serialNum), // Order by slot number 1-4
});
// Convert S3 keys to signed URLs for client
const bannersWithSignedUrls = await Promise.all(
banners.map(async (banner) => {
try {
return {
...banner,
imageUrl: banner.imageUrl ? await generateSignedUrlFromS3Url(banner.imageUrl) : banner.imageUrl,
};
} catch (error) {
console.error(`Failed to generate signed URL for banner ${banner.id}:`, error);
return {
...banner,
imageUrl: banner.imageUrl, // Keep original on error
};
}
})
);
return {
banners: bannersWithSignedUrls,
};
}), }),
}); });

View file

@ -1,19 +1,25 @@
import { router, protectedProcedure, publicProcedure } from '@/src/trpc/trpc-index'; import { router, protectedProcedure, publicProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'; import { z } from 'zod'
import { db } from '@/src/db/db_index'; import { ApiError } from '@/src/lib/api-error'
import { cartItems, productInfo, units, productSlots, deliverySlotInfo } from '@/src/db/schema'; import { scaffoldAssetUrl } from '@/src/lib/s3-client'
import { eq, and, sql, inArray, gt } from 'drizzle-orm'; import { getMultipleProductsSlots } from '@/src/stores/slot-store'
import { ApiError } from '@/src/lib/api-error'; import {
import { generateSignedUrlsFromS3Urls, scaffoldAssetUrl } from '@/src/lib/s3-client'; getUserCartItemsWithProducts as getUserCartItemsWithProductsInDb,
import { getProductSlots, getMultipleProductsSlots } from '@/src/stores/slot-store'; getUserProductById as getUserProductByIdInDb,
getUserCartItemByUserProduct as getUserCartItemByUserProductInDb,
incrementUserCartItemQuantity as incrementUserCartItemQuantityInDb,
insertUserCartItem as insertUserCartItemInDb,
updateUserCartItemQuantity as updateUserCartItemQuantityInDb,
deleteUserCartItem as deleteUserCartItemInDb,
clearUserCart as clearUserCartInDb,
} from '@/src/dbService'
import type { UserCartResponse } from '@packages/shared'
interface CartResponse { const getCartData = async (userId: number): Promise<UserCartResponse> => {
items: any[]; const cartItemsWithProducts = await getUserCartItemsWithProductsInDb(userId)
totalItems: number;
totalAmount: number;
}
const getCartData = async (userId: number): Promise<CartResponse> => { /*
// Old implementation - direct DB queries:
const cartItemsWithProducts = await db const cartItemsWithProducts = await db
.select({ .select({
cartId: cartItems.id, cartId: cartItems.id,
@ -31,39 +37,28 @@ const getCartData = async (userId: number): Promise<CartResponse> => {
.innerJoin(productInfo, eq(cartItems.productId, productInfo.id)) .innerJoin(productInfo, eq(cartItems.productId, productInfo.id))
.innerJoin(units, eq(productInfo.unitId, units.id)) .innerJoin(units, eq(productInfo.unitId, units.id))
.where(eq(cartItems.userId, userId)); .where(eq(cartItems.userId, userId));
*/
// Generate signed URLs for images const cartWithSignedUrls = cartItemsWithProducts.map((item) => ({
const cartWithSignedUrls = await Promise.all( ...item,
cartItemsWithProducts.map(async (item) => ({ product: {
id: item.cartId, ...item.product,
productId: item.productId, images: scaffoldAssetUrl(item.product.images || []),
quantity: parseFloat(item.quantity), },
addedAt: item.addedAt, }))
product: {
id: item.productId,
name: item.productName,
price: item.productPrice,
productQuantity: item.productQuantity,
unit: item.unitShortNotation,
isOutOfStock: item.isOutOfStock,
images: scaffoldAssetUrl((item.productImages as string[]) || []),
},
subtotal: parseFloat(item.productPrice.toString()) * parseFloat(item.quantity),
}))
);
const totalAmount = cartWithSignedUrls.reduce((sum, item) => sum + item.subtotal, 0); const totalAmount = cartWithSignedUrls.reduce((sum, item) => sum + item.subtotal, 0)
return { return {
items: cartWithSignedUrls, items: cartWithSignedUrls,
totalItems: cartWithSignedUrls.length, totalItems: cartWithSignedUrls.length,
totalAmount, totalAmount,
}; }
}; }
export const cartRouter = router({ export const cartRouter = router({
getCart: protectedProcedure getCart: protectedProcedure
.query(async ({ ctx }): Promise<CartResponse> => { .query(async ({ ctx }): Promise<UserCartResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
return await getCartData(userId); return await getCartData(userId);
}), }),
@ -73,7 +68,7 @@ export const cartRouter = router({
productId: z.number().int().positive(), productId: z.number().int().positive(),
quantity: z.number().int().positive(), quantity: z.number().int().positive(),
})) }))
.mutation(async ({ input, ctx }): Promise<CartResponse> => { .mutation(async ({ input, ctx }): Promise<UserCartResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
const { productId, quantity } = input; const { productId, quantity } = input;
@ -83,6 +78,22 @@ export const cartRouter = router({
} }
// Check if product exists // Check if product exists
const product = await getUserProductByIdInDb(productId)
if (!product) {
throw new ApiError('Product not found', 404)
}
const existingItem = await getUserCartItemByUserProductInDb(userId, productId)
if (existingItem) {
await incrementUserCartItemQuantityInDb(existingItem.id, quantity)
} else {
await insertUserCartItemInDb(userId, productId, quantity)
}
/*
// Old implementation - direct DB queries:
const product = await db.query.productInfo.findFirst({ const product = await db.query.productInfo.findFirst({
where: eq(productInfo.id, productId), where: eq(productInfo.id, productId),
}); });
@ -91,29 +102,27 @@ export const cartRouter = router({
throw new ApiError("Product not found", 404); throw new ApiError("Product not found", 404);
} }
// Check if item already exists in cart
const existingItem = await db.query.cartItems.findFirst({ const existingItem = await db.query.cartItems.findFirst({
where: and(eq(cartItems.userId, userId), eq(cartItems.productId, productId)), where: and(eq(cartItems.userId, userId), eq(cartItems.productId, productId)),
}); });
if (existingItem) { if (existingItem) {
// Update quantity
await db.update(cartItems) await db.update(cartItems)
.set({ .set({
quantity: sql`${cartItems.quantity} + ${quantity}`, quantity: sql`${cartItems.quantity} + ${quantity}`,
}) })
.where(eq(cartItems.id, existingItem.id)); .where(eq(cartItems.id, existingItem.id));
} else { } else {
// Insert new item
await db.insert(cartItems).values({ await db.insert(cartItems).values({
userId, userId,
productId, productId,
quantity: quantity.toString(), quantity: quantity.toString(),
}); });
} }
*/
// Return updated cart // Return updated cart
return await getCartData(userId); return await getCartData(userId)
}), }),
updateCartItem: protectedProcedure updateCartItem: protectedProcedure
@ -121,7 +130,7 @@ export const cartRouter = router({
itemId: z.number().int().positive(), itemId: z.number().int().positive(),
quantity: z.number().int().min(0), quantity: z.number().int().min(0),
})) }))
.mutation(async ({ input, ctx }): Promise<CartResponse> => { .mutation(async ({ input, ctx }): Promise<UserCartResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
const { itemId, quantity } = input; const { itemId, quantity } = input;
@ -129,6 +138,10 @@ export const cartRouter = router({
throw new ApiError("Positive quantity required", 400); throw new ApiError("Positive quantity required", 400);
} }
const updated = await updateUserCartItemQuantityInDb(userId, itemId, quantity)
/*
// Old implementation - direct DB queries:
const [updatedItem] = await db.update(cartItems) const [updatedItem] = await db.update(cartItems)
.set({ quantity: quantity.toString() }) .set({ quantity: quantity.toString() })
.where(and( .where(and(
@ -140,19 +153,28 @@ export const cartRouter = router({
if (!updatedItem) { if (!updatedItem) {
throw new ApiError("Cart item not found", 404); throw new ApiError("Cart item not found", 404);
} }
*/
if (!updated) {
throw new ApiError('Cart item not found', 404)
}
// Return updated cart // Return updated cart
return await getCartData(userId); return await getCartData(userId)
}), }),
removeFromCart: protectedProcedure removeFromCart: protectedProcedure
.input(z.object({ .input(z.object({
itemId: z.number().int().positive(), itemId: z.number().int().positive(),
})) }))
.mutation(async ({ input, ctx }): Promise<CartResponse> => { .mutation(async ({ input, ctx }): Promise<UserCartResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
const { itemId } = input; const { itemId } = input;
const deleted = await deleteUserCartItemInDb(userId, itemId)
/*
// Old implementation - direct DB queries:
const [deletedItem] = await db.delete(cartItems) const [deletedItem] = await db.delete(cartItems)
.where(and( .where(and(
eq(cartItems.id, itemId), eq(cartItems.id, itemId),
@ -163,23 +185,33 @@ export const cartRouter = router({
if (!deletedItem) { if (!deletedItem) {
throw new ApiError("Cart item not found", 404); throw new ApiError("Cart item not found", 404);
} }
*/
if (!deleted) {
throw new ApiError('Cart item not found', 404)
}
// Return updated cart // Return updated cart
return await getCartData(userId); return await getCartData(userId)
}), }),
clearCart: protectedProcedure clearCart: protectedProcedure
.mutation(async ({ ctx }) => { .mutation(async ({ ctx }): Promise<UserCartResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
await clearUserCartInDb(userId)
/*
// Old implementation - direct DB query:
await db.delete(cartItems).where(eq(cartItems.userId, userId)); await db.delete(cartItems).where(eq(cartItems.userId, userId));
*/
return { return {
items: [], items: [],
totalItems: 0, totalItems: 0,
totalAmount: 0, totalAmount: 0,
message: "Cart cleared successfully", message: "Cart cleared successfully",
}; }
}), }),
// Original DB-based getCartSlots (commented out) // Original DB-based getCartSlots (commented out)

View file

@ -1,14 +1,20 @@
import { router, protectedProcedure } from '@/src/trpc/trpc-index'; import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'; import { z } from 'zod'
import { db } from '@/src/db/db_index'; import {
import { complaints } from '@/src/db/schema'; getUserComplaints as getUserComplaintsInDb,
import { eq } from 'drizzle-orm'; createUserComplaint as createUserComplaintInDb,
} from '@/src/dbService'
import type { UserComplaintsResponse, UserRaiseComplaintResponse } from '@packages/shared'
export const complaintRouter = router({ export const complaintRouter = router({
getAll: protectedProcedure getAll: protectedProcedure
.query(async ({ ctx }) => { .query(async ({ ctx }): Promise<UserComplaintsResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
const userComplaints = await getUserComplaintsInDb(userId)
/*
// Old implementation - direct DB queries:
const userComplaints = await db const userComplaints = await db
.select({ .select({
id: complaints.id, id: complaints.id,
@ -32,6 +38,11 @@ export const complaintRouter = router({
orderId: c.orderId, orderId: c.orderId,
})), })),
}; };
*/
return {
complaints: userComplaints,
}
}), }),
raise: protectedProcedure raise: protectedProcedure
@ -39,7 +50,7 @@ export const complaintRouter = router({
orderId: z.string().optional(), orderId: z.string().optional(),
complaintBody: z.string().min(1, 'Complaint body is required'), complaintBody: z.string().min(1, 'Complaint body is required'),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<UserRaiseComplaintResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
const { orderId, complaintBody } = input; const { orderId, complaintBody } = input;
@ -52,12 +63,17 @@ export const complaintRouter = router({
} }
} }
await createUserComplaintInDb(userId, orderIdNum, complaintBody.trim())
/*
// Old implementation - direct DB query:
await db.insert(complaints).values({ await db.insert(complaints).values({
userId, userId,
orderId: orderIdNum, orderId: orderIdNum,
complaintBody: complaintBody.trim(), complaintBody: complaintBody.trim(),
}); });
*/
return { success: true, message: 'Complaint raised successfully' }; return { success: true, message: 'Complaint raised successfully' }
}), }),
}); });

View file

@ -1,31 +1,20 @@
import { router, protectedProcedure } from '@/src/trpc/trpc-index'; import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'; import { z } from 'zod'
import { db } from '@/src/db/db_index'; import { ApiError } from '@/src/lib/api-error'
import { coupons, couponUsage, couponApplicableUsers, reservedCoupons, couponApplicableProducts } from '@/src/db/schema'; import {
import { eq, and, or, gt, isNull, sql } from 'drizzle-orm'; getUserActiveCouponsWithRelations as getUserActiveCouponsWithRelationsInDb,
import { ApiError } from '@/src/lib/api-error'; getUserAllCouponsWithRelations as getUserAllCouponsWithRelationsInDb,
getUserReservedCouponByCode as getUserReservedCouponByCodeInDb,
redeemUserReservedCoupon as redeemUserReservedCouponInDb,
} from '@/src/dbService'
import type {
UserCouponDisplay,
UserEligibleCouponsResponse,
UserMyCouponsResponse,
UserRedeemCouponResponse,
} from '@packages/shared'
import { users } from '@/src/db/schema'; const generateCouponDescription = (coupon: { discountPercent?: string | null; flatDiscount?: string | null; minOrder?: string | null; maxValue?: string | null }): string => {
type CouponWithRelations = typeof coupons.$inferSelect & {
applicableUsers: (typeof couponApplicableUsers.$inferSelect & { user: typeof users.$inferSelect })[];
usages: typeof couponUsage.$inferSelect[];
};
export interface EligibleCoupon {
id: number;
code: string;
discountType: 'percentage' | 'flat';
discountValue: number;
maxValue?: number;
minOrder?: number;
description: string;
exclusiveApply?: boolean;
isEligible: boolean;
ineligibilityReason?: string;
}
const generateCouponDescription = (coupon: any): string => {
let desc = ''; let desc = '';
if (coupon.discountPercent) { if (coupon.discountPercent) {
@ -45,29 +34,17 @@ const generateCouponDescription = (coupon: any): string => {
return desc; return desc;
}; };
export interface CouponDisplay {
id: number;
code: string;
discountType: 'percentage' | 'flat';
discountValue: number;
maxValue?: number;
minOrder?: number;
description: string;
validTill?: Date;
usageCount: number;
maxLimitForUser?: number;
isExpired: boolean;
isUsedUp: boolean;
}
export const userCouponRouter = router({ export const userCouponRouter = router({
getEligible: protectedProcedure getEligible: protectedProcedure
.query(async ({ ctx }) => { .query(async ({ ctx }): Promise<UserEligibleCouponsResponse> => {
try { try {
const userId = ctx.user.userId; const userId = ctx.user.userId;
// Get all active, non-expired coupons const allCoupons = await getUserActiveCouponsWithRelationsInDb(userId)
/*
// Old implementation - direct DB queries:
const allCoupons = await db.query.coupons.findMany({ const allCoupons = await db.query.coupons.findMany({
where: and( where: and(
eq(coupons.isInvalidated, false), eq(coupons.isInvalidated, false),
@ -92,6 +69,7 @@ export const userCouponRouter = router({
}, },
} }
}); });
*/
// Filter to only coupons applicable to current user // Filter to only coupons applicable to current user
const applicableCoupons = allCoupons.filter(coupon => { const applicableCoupons = allCoupons.filter(coupon => {
@ -100,7 +78,7 @@ export const userCouponRouter = router({
return applicableUsers.some(au => au.userId === userId); return applicableUsers.some(au => au.userId === userId);
}); });
return { success: true, data: applicableCoupons }; return { success: true, data: applicableCoupons };
} }
catch(e) { catch(e) {
console.log(e) console.log(e)
@ -110,11 +88,15 @@ export const userCouponRouter = router({
getProductCoupons: protectedProcedure getProductCoupons: protectedProcedure
.input(z.object({ productId: z.number().int().positive() })) .input(z.object({ productId: z.number().int().positive() }))
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }): Promise<UserEligibleCouponsResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
const { productId } = input; const { productId } = input;
// Get all active, non-expired coupons // Get all active, non-expired coupons
const allCoupons = await getUserActiveCouponsWithRelationsInDb(userId)
/*
// Old implementation - direct DB queries:
const allCoupons = await db.query.coupons.findMany({ const allCoupons = await db.query.coupons.findMany({
where: and( where: and(
eq(coupons.isInvalidated, false), eq(coupons.isInvalidated, false),
@ -139,6 +121,7 @@ export const userCouponRouter = router({
}, },
} }
}); });
*/
// Filter to only coupons applicable to current user and product // Filter to only coupons applicable to current user and product
const applicableCoupons = allCoupons.filter(coupon => { const applicableCoupons = allCoupons.filter(coupon => {
@ -155,10 +138,13 @@ export const userCouponRouter = router({
}), }),
getMyCoupons: protectedProcedure getMyCoupons: protectedProcedure
.query(async ({ ctx }) => { .query(async ({ ctx }): Promise<UserMyCouponsResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
// Get all coupons const allCoupons = await getUserAllCouponsWithRelationsInDb(userId)
/*
// Old implementation - direct DB queries:
const allCoupons = await db.query.coupons.findMany({ const allCoupons = await db.query.coupons.findMany({
with: { with: {
usages: { usages: {
@ -171,9 +157,10 @@ export const userCouponRouter = router({
} }
} }
}); });
*/
// Filter coupons in JS: not invalidated, applicable to user, and not expired // Filter coupons in JS: not invalidated, applicable to user, and not expired
const applicableCoupons = (allCoupons as CouponWithRelations[]).filter(coupon => { const applicableCoupons = allCoupons.filter(coupon => {
const isNotInvalidated = !coupon.isInvalidated; const isNotInvalidated = !coupon.isInvalidated;
const applicableUsers = coupon.applicableUsers || []; const applicableUsers = coupon.applicableUsers || [];
const isApplicable = coupon.isApplyForAll || applicableUsers.some(au => au.userId === userId); const isApplicable = coupon.isApplyForAll || applicableUsers.some(au => au.userId === userId);
@ -182,15 +169,15 @@ export const userCouponRouter = router({
}); });
// Categorize coupons // Categorize coupons
const personalCoupons: CouponDisplay[] = []; const personalCoupons: UserCouponDisplay[] = [];
const generalCoupons: CouponDisplay[] = []; const generalCoupons: UserCouponDisplay[] = [];
applicableCoupons.forEach(coupon => { applicableCoupons.forEach(coupon => {
const usageCount = coupon.usages.length; const usageCount = coupon.usages.length;
const isExpired = false; // Already filtered out expired coupons const isExpired = false; // Already filtered out expired coupons
const isUsedUp = Boolean(coupon.maxLimitForUser && usageCount >= coupon.maxLimitForUser); const isUsedUp = Boolean(coupon.maxLimitForUser && usageCount >= coupon.maxLimitForUser);
const couponDisplay: CouponDisplay = { const couponDisplay: UserCouponDisplay = {
id: coupon.id, id: coupon.id,
code: coupon.couponCode, code: coupon.couponCode,
discountType: coupon.discountPercent ? 'percentage' : 'flat', discountType: coupon.discountPercent ? 'percentage' : 'flat',
@ -225,17 +212,21 @@ export const userCouponRouter = router({
redeemReservedCoupon: protectedProcedure redeemReservedCoupon: protectedProcedure
.input(z.object({ secretCode: z.string() })) .input(z.object({ secretCode: z.string() }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<UserRedeemCouponResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
const { secretCode } = input; const { secretCode } = input;
// Find the reserved coupon const reservedCoupon = await getUserReservedCouponByCodeInDb(secretCode)
/*
// Old implementation - direct DB queries:
const reservedCoupon = await db.query.reservedCoupons.findFirst({ const reservedCoupon = await db.query.reservedCoupons.findFirst({
where: and( where: and(
eq(reservedCoupons.secretCode, secretCode.toUpperCase()), eq(reservedCoupons.secretCode, secretCode.toUpperCase()),
eq(reservedCoupons.isRedeemed, false) eq(reservedCoupons.isRedeemed, false)
), ),
}); });
*/
if (!reservedCoupon) { if (!reservedCoupon) {
throw new ApiError("Invalid or already redeemed coupon code", 400); throw new ApiError("Invalid or already redeemed coupon code", 400);
@ -246,9 +237,11 @@ export const userCouponRouter = router({
throw new ApiError("You have already redeemed this coupon", 400); throw new ApiError("You have already redeemed this coupon", 400);
} }
// Create the coupon in the main table const couponResult = await redeemUserReservedCouponInDb(userId, reservedCoupon)
/*
// Old implementation - direct DB queries:
const couponResult = await db.transaction(async (tx) => { const couponResult = await db.transaction(async (tx) => {
// Insert into coupons
const couponInsert = await tx.insert(coupons).values({ const couponInsert = await tx.insert(coupons).values({
couponCode: reservedCoupon.couponCode, couponCode: reservedCoupon.couponCode,
isUserBased: true, isUserBased: true,
@ -266,22 +259,11 @@ export const userCouponRouter = router({
const coupon = couponInsert[0]; const coupon = couponInsert[0];
// Insert into couponApplicableUsers
await tx.insert(couponApplicableUsers).values({ await tx.insert(couponApplicableUsers).values({
couponId: coupon.id, couponId: coupon.id,
userId, 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({ await tx.update(reservedCoupons).set({
isRedeemed: true, isRedeemed: true,
redeemedBy: userId, redeemedBy: userId,
@ -290,6 +272,7 @@ export const userCouponRouter = router({
return coupon; return coupon;
}); });
*/
return { success: true, coupon: couponResult }; return { success: true, coupon: couponResult };
}), }),

View file

@ -1,109 +1,43 @@
import { router, protectedProcedure } from "@/src/trpc/trpc-index"; import { router, protectedProcedure } from "@/src/trpc/trpc-index";
import { z } from "zod"; import { z } from "zod";
import { db } from "@/src/db/db_index";
import { import {
orders, validateAndGetUserCoupon,
orderItems, applyDiscountToUserOrder,
orderStatus, getUserAddressByIdAndUser,
addresses, getOrderProductById,
productInfo, checkUserSuspended,
paymentInfoTable, getUserSlotCapacityStatus,
coupons, placeUserOrderTransaction,
couponUsage, deleteUserCartItemsForOrder,
payments, recordUserCouponUsage,
cartItems, getUserOrdersWithRelations,
refunds, getUserOrderCount,
units, getUserOrderByIdWithRelations,
userDetails, getUserCouponUsageForOrder,
} from "@/src/db/schema"; getUserOrderBasic,
import { eq, and, inArray, desc, gte, lte } from "drizzle-orm"; cancelUserOrderTransaction,
updateUserOrderNotes,
getUserRecentlyDeliveredOrderIds,
getUserProductIdsFromOrders,
getUserProductsForRecentOrders,
} from "@/src/dbService";
import { getNextDeliveryDate } from "@/src/trpc/apis/common-apis/common";
import { scaffoldAssetUrl } from "@/src/lib/s3-client"; import { scaffoldAssetUrl } from "@/src/lib/s3-client";
import { ApiError } from "@/src/lib/api-error"; import { ApiError } from "@/src/lib/api-error";
import { import {
sendOrderPlacedNotification, sendOrderPlacedNotification,
sendOrderCancelledNotification, sendOrderCancelledNotification,
} from "@/src/lib/notif-job"; } from "@/src/lib/notif-job";
import { RazorpayPaymentService } from "@/src/lib/payments-utils";
import { getNextDeliveryDate } from "@/src/trpc/apis/common-apis/common";
import { CONST_KEYS, getConstant, getConstants } from "@/src/lib/const-store"; import { CONST_KEYS, getConstant, getConstants } from "@/src/lib/const-store";
import { publishFormattedOrder, publishCancellation } from "@/src/lib/post-order-handler"; import { publishFormattedOrder, publishCancellation } from "@/src/lib/post-order-handler";
import { getSlotById } from "@/src/stores/slot-store"; import { getSlotById } from "@/src/stores/slot-store";
import type {
UserOrdersResponse,
const validateAndGetCoupon = async ( UserOrderDetail,
couponId: number | undefined, UserCancelOrderResponse,
userId: number, UserUpdateNotesResponse,
totalAmount: number UserRecentProductsResponse,
) => { } from "@/src/dbService";
if (!couponId) return null;
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.isInvalidated)
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);
if (
coupon.maxLimitForUser &&
coupon.usages.length >= coupon.maxLimitForUser
)
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",
400
);
return coupon;
};
const applyDiscountToOrder = (
orderTotal: number,
appliedCoupon: typeof coupons.$inferSelect | null,
proportion: number
) => {
let finalOrderTotal = orderTotal;
// const proportion = totalAmount / orderTotal;
if (appliedCoupon) {
if (appliedCoupon.discountPercent) {
const discount = Math.min(
(orderTotal *
parseFloat(appliedCoupon.discountPercent.toString())) /
100,
appliedCoupon.maxValue
? parseFloat(appliedCoupon.maxValue.toString()) * proportion
: Infinity
);
finalOrderTotal -= discount;
} else if (appliedCoupon.flatDiscount) {
const discount = Math.min(
parseFloat(appliedCoupon.flatDiscount.toString()) * proportion,
appliedCoupon.maxValue
? parseFloat(appliedCoupon.maxValue.toString()) * proportion
: finalOrderTotal
);
finalOrderTotal -= discount;
}
}
// let orderDeliveryCharge = 0;
// if (isFirstOrder && finalOrderTotal < minOrderValue) {
// orderDeliveryCharge = deliveryCharge;
// finalOrderTotal += deliveryCharge;
// }
return { finalOrderTotal, orderGroupProportion: proportion };
};
const placeOrderUtil = async (params: { const placeOrderUtil = async (params: {
userId: number; userId: number;
@ -140,9 +74,7 @@ const placeOrderUtil = async (params: {
const orderGroupId = `${Date.now()}-${userId}`; const orderGroupId = `${Date.now()}-${userId}`;
const address = await db.query.addresses.findFirst({ const address = await getUserAddressByIdAndUser(addressId, userId);
where: and(eq(addresses.userId, userId), eq(addresses.id, addressId)),
});
if (!address) { if (!address) {
throw new ApiError("Invalid address", 400); throw new ApiError("Invalid address", 400);
} }
@ -153,14 +85,12 @@ const placeOrderUtil = async (params: {
productId: number; productId: number;
quantity: number; quantity: number;
slotId: number | null; slotId: number | null;
product: any; product: Awaited<ReturnType<typeof getOrderProductById>>;
}> }>
>(); >();
for (const item of selectedItems) { for (const item of selectedItems) {
const product = await db.query.productInfo.findFirst({ const product = await getOrderProductById(item.productId);
where: eq(productInfo.id, item.productId),
});
if (!product) { if (!product) {
throw new ApiError(`Product ${item.productId} not found`, 400); throw new ApiError(`Product ${item.productId} not found`, 400);
} }
@ -173,9 +103,7 @@ const placeOrderUtil = async (params: {
if (params.isFlash) { if (params.isFlash) {
for (const item of selectedItems) { for (const item of selectedItems) {
const product = await db.query.productInfo.findFirst({ const product = await getOrderProductById(item.productId);
where: eq(productInfo.id, item.productId),
});
if (!product?.isFlashAvailable) { if (!product?.isFlashAvailable) {
throw new ApiError(`Product ${item.productId} is not available for flash delivery`, 400); throw new ApiError(`Product ${item.productId} is not available for flash delivery`, 400);
} }
@ -186,6 +114,7 @@ const placeOrderUtil = async (params: {
for (const [slotId, items] of ordersBySlot) { for (const [slotId, items] of ordersBySlot) {
const orderTotal = items.reduce( const orderTotal = items.reduce(
(sum, item) => { (sum, item) => {
if (!item.product) return sum
const itemPrice = params.isFlash const itemPrice = params.isFlash
? parseFloat((item.product.flashPrice || item.product.price).toString()) ? parseFloat((item.product.flashPrice || item.product.price).toString())
: parseFloat(item.product.price.toString()); : parseFloat(item.product.price.toString());
@ -196,13 +125,16 @@ const placeOrderUtil = async (params: {
totalAmount += orderTotal; totalAmount += orderTotal;
} }
const appliedCoupon = await validateAndGetCoupon(couponId, userId, totalAmount); const appliedCoupon = await validateAndGetUserCoupon(couponId, userId, totalAmount);
const expectedDeliveryCharge = const expectedDeliveryCharge =
totalAmount < minOrderValue ? deliveryCharge : 0; totalAmount < minOrderValue ? deliveryCharge : 0;
const totalWithDelivery = totalAmount + expectedDeliveryCharge; const totalWithDelivery = totalAmount + expectedDeliveryCharge;
const { db } = await import("postgresService");
const { orders, orderItems, orderStatus } = await import("postgresService");
type OrderData = { type OrderData = {
order: Omit<typeof orders.$inferInsert, "id">; order: Omit<typeof orders.$inferInsert, "id">;
orderItems: Omit<typeof orderItems.$inferInsert, "id">[]; orderItems: Omit<typeof orderItems.$inferInsert, "id">[];
@ -215,6 +147,7 @@ const placeOrderUtil = async (params: {
for (const [slotId, items] of ordersBySlot) { for (const [slotId, items] of ordersBySlot) {
const subOrderTotal = items.reduce( const subOrderTotal = items.reduce(
(sum, item) => { (sum, item) => {
if (!item.product) return sum
const itemPrice = params.isFlash const itemPrice = params.isFlash
? parseFloat((item.product.flashPrice || item.product.price).toString()) ? parseFloat((item.product.flashPrice || item.product.price).toString())
: parseFloat(item.product.price.toString()); : parseFloat(item.product.price.toString());
@ -227,7 +160,7 @@ const placeOrderUtil = async (params: {
const orderGroupProportion = subOrderTotal / totalAmount; const orderGroupProportion = subOrderTotal / totalAmount;
const orderTotalAmount = isFirstOrder ? subOrderTotalWithDelivery : subOrderTotal; const orderTotalAmount = isFirstOrder ? subOrderTotalWithDelivery : subOrderTotal;
const { finalOrderTotal: finalOrderAmount } = applyDiscountToOrder( const { finalOrderTotal: finalOrderAmount } = applyDiscountToUserOrder(
orderTotalAmount, orderTotalAmount,
appliedCoupon, appliedCoupon,
orderGroupProportion orderGroupProportion
@ -249,21 +182,23 @@ const placeOrderUtil = async (params: {
isFlashDelivery: params.isFlash, isFlashDelivery: params.isFlash,
}; };
const orderItemsData: Omit<typeof orderItems.$inferInsert, "id">[] = items.map( const orderItemsData: Omit<typeof orderItems.$inferInsert, "id">[] = items
(item) => ({ .filter((item) => item.product !== null && item.product !== undefined)
orderId: 0, .map(
productId: item.productId, (item) => ({
quantity: item.quantity.toString(), orderId: 0,
price: params.isFlash productId: item.productId,
? item.product.flashPrice || item.product.price quantity: item.quantity.toString(),
: item.product.price, price: params.isFlash
discountedPrice: ( ? item.product!.flashPrice || item.product!.price
params.isFlash : item.product!.price,
? item.product.flashPrice || item.product.price discountedPrice: (
: item.product.price params.isFlash
).toString(), ? item.product!.flashPrice || item.product!.price
}) : item.product!.price
); ).toString(),
})
);
const orderStatusData: Omit<typeof orderStatus.$inferInsert, "id"> = { const orderStatusData: Omit<typeof orderStatus.$inferInsert, "id"> = {
userId, userId,
@ -275,79 +210,24 @@ const placeOrderUtil = async (params: {
isFirstOrder = false; isFirstOrder = false;
} }
const createdOrders = await db.transaction(async (tx) => { const createdOrders = await placeUserOrderTransaction({
let sharedPaymentInfoId: number | null = null; userId,
if (paymentMethod === "online") { ordersData,
const [paymentInfo] = await tx paymentMethod,
.insert(paymentInfoTable) totalWithDelivery,
.values({
status: "pending",
gateway: "razorpay",
merchantOrderId: `multi_order_${Date.now()}`,
})
.returning();
sharedPaymentInfoId = paymentInfo.id;
}
const ordersToInsert: Omit<typeof orders.$inferInsert, "id">[] = ordersData.map(
(od) => ({
...od.order,
paymentInfoId: sharedPaymentInfoId,
})
);
const insertedOrders = await tx.insert(orders).values(ordersToInsert).returning();
const allOrderItems: Omit<typeof orderItems.$inferInsert, "id">[] = [];
const allOrderStatuses: Omit<typeof orderStatus.$inferInsert, "id">[] = [];
insertedOrders.forEach((order, index) => {
const od = ordersData[index];
od.orderItems.forEach((item) => {
allOrderItems.push({ ...item, orderId: order.id as number });
});
allOrderStatuses.push({
...od.orderStatus,
orderId: order.id as number,
});
});
await tx.insert(orderItems).values(allOrderItems);
await tx.insert(orderStatus).values(allOrderStatuses);
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( await deleteUserCartItemsForOrder(
and( userId,
eq(cartItems.userId, userId), selectedItems.map((item) => item.productId)
inArray(
cartItems.productId,
selectedItems.map((item) => item.productId)
)
)
); );
if (appliedCoupon && createdOrders.length > 0) { if (appliedCoupon && createdOrders.length > 0) {
await db.insert(couponUsage).values({ await recordUserCouponUsage(
userId, userId,
couponId: appliedCoupon.id, appliedCoupon.id,
orderId: createdOrders[0].id as number, createdOrders[0].id
orderItemId: null, );
usedAt: new Date(),
});
} }
for (const order of createdOrders) { for (const order of createdOrders) {
@ -380,12 +260,8 @@ export const orderRouter = router({
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
// Check if user is suspended from placing orders const isSuspended = await checkUserSuspended(userId);
const userDetail = await db.query.userDetails.findFirst({ if (isSuspended) {
where: eq(userDetails.userId, userId),
});
if (userDetail?.isSuspended) {
throw new ApiError("Unable to place order", 403); throw new ApiError("Unable to place order", 403);
} }
@ -398,7 +274,6 @@ export const orderRouter = router({
isFlashDelivery, isFlashDelivery,
} = input; } = input;
// Check if flash delivery is enabled when placing a flash delivery order
if (isFlashDelivery) { if (isFlashDelivery) {
const isFlashDeliveryEnabled = await getConstant<boolean>(CONST_KEYS.isFlashDeliveryEnabled); const isFlashDeliveryEnabled = await getConstant<boolean>(CONST_KEYS.isFlashDeliveryEnabled);
if (!isFlashDeliveryEnabled) { if (!isFlashDeliveryEnabled) {
@ -406,12 +281,11 @@ export const orderRouter = router({
} }
} }
// Check if any selected slot is at full capacity (only for regular delivery)
if (!isFlashDelivery) { if (!isFlashDelivery) {
const slotIds = [...new Set(selectedItems.filter(i => i.slotId !== null).map(i => i.slotId as number))]; const slotIds = [...new Set(selectedItems.filter(i => i.slotId !== null).map(i => i.slotId as number))];
for (const slotId of slotIds) { for (const slotId of slotIds) {
const slot = await getSlotById(slotId); const isCapacityFull = await getUserSlotCapacityStatus(slotId);
if (slot?.isCapacityFull) { if (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);
} }
} }
@ -419,12 +293,10 @@ export const orderRouter = router({
let processedItems = selectedItems; let processedItems = selectedItems;
// Handle flash delivery slot resolution
if (isFlashDelivery) { if (isFlashDelivery) {
// For flash delivery, set slotId to null (no specific slot assigned)
processedItems = selectedItems.map(item => ({ processedItems = selectedItems.map(item => ({
...item, ...item,
slotId: null as any, // Type override for flash delivery slotId: null as any,
})); }));
} }
@ -448,35 +320,13 @@ export const orderRouter = router({
}) })
.optional() .optional()
) )
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }): Promise<UserOrdersResponse> => {
const { page = 1, pageSize = 10 } = input || {}; const { page = 1, pageSize = 10 } = input || {};
const userId = ctx.user.userId; const userId = ctx.user.userId;
const offset = (page - 1) * pageSize; const offset = (page - 1) * pageSize;
// Get total count for pagination const totalCount = await getUserOrderCount(userId);
const totalCountResult = await db.$count( const userOrders = await getUserOrdersWithRelations(userId, offset, pageSize);
orders,
eq(orders.userId, userId)
);
const totalCount = totalCountResult;
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( const mappedOrders = await Promise.all(
userOrders.map(async (order) => { userOrders.map(async (order) => {
@ -516,7 +366,6 @@ export const orderRouter = router({
const items = await Promise.all( const items = await Promise.all(
order.orderItems.map(async (item) => { order.orderItems.map(async (item) => {
const signedImages = item.product.images const signedImages = item.product.images
? scaffoldAssetUrl( ? scaffoldAssetUrl(
item.product.images as string[] item.product.images as string[]
@ -572,44 +421,20 @@ export const orderRouter = router({
getOrderById: protectedProcedure getOrderById: protectedProcedure
.input(z.object({ orderId: z.string() })) .input(z.object({ orderId: z.string() }))
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }): Promise<UserOrderDetail> => {
const { orderId } = input; const { orderId } = input;
const userId = ctx.user.userId; const userId = ctx.user.userId;
const order = await db.query.orders.findFirst({ const order = await getUserOrderByIdWithRelations(parseInt(orderId), userId);
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) { if (!order) {
throw new Error("Order not found"); throw new Error("Order not found");
} }
// Get coupon usage for this specific order using new orderId field const couponUsageData = await getUserCouponUsageForOrder(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; let couponData = null;
if (couponUsageData.length > 0) { if (couponUsageData.length > 0) {
// Calculate total discount from multiple coupons
let totalDiscountAmount = 0; let totalDiscountAmount = 0;
const orderTotal = parseFloat(order.totalAmount.toString()); const orderTotal = parseFloat(order.totalAmount.toString());
@ -625,7 +450,6 @@ export const orderRouter = router({
discountAmount = parseFloat(usage.coupon.flatDiscount.toString()); discountAmount = parseFloat(usage.coupon.flatDiscount.toString());
} }
// Apply max value limit if set
if ( if (
usage.coupon.maxValue && usage.coupon.maxValue &&
discountAmount > parseFloat(usage.coupon.maxValue.toString()) discountAmount > parseFloat(usage.coupon.maxValue.toString())
@ -652,7 +476,7 @@ export const orderRouter = router({
type OrderStatus = "cancelled" | "success"; type OrderStatus = "cancelled" | "success";
let deliveryStatus: DeliveryStatus; let deliveryStatus: DeliveryStatus;
let orderStatus: OrderStatus; let orderStatusResult: OrderStatus;
const allItemsPackaged = order.orderItems.every( const allItemsPackaged = order.orderItems.every(
(item) => item.is_packaged (item) => item.is_packaged
@ -660,16 +484,16 @@ export const orderRouter = router({
if (status?.isCancelled) { if (status?.isCancelled) {
deliveryStatus = "cancelled"; deliveryStatus = "cancelled";
orderStatus = "cancelled"; orderStatusResult = "cancelled";
} else if (status?.isDelivered) { } else if (status?.isDelivered) {
deliveryStatus = "success"; deliveryStatus = "success";
orderStatus = "success"; orderStatusResult = "success";
} else if (allItemsPackaged) { } else if (allItemsPackaged) {
deliveryStatus = "packaged"; deliveryStatus = "packaged";
orderStatus = "success"; orderStatusResult = "success";
} else { } else {
deliveryStatus = "pending"; deliveryStatus = "pending";
orderStatus = "success"; orderStatusResult = "success";
} }
const paymentMode = order.isCod ? "CoD" : "Online"; const paymentMode = order.isCod ? "CoD" : "Online";
@ -706,8 +530,8 @@ export const orderRouter = router({
orderDate: order.createdAt.toISOString(), orderDate: order.createdAt.toISOString(),
deliveryStatus, deliveryStatus,
deliveryDate: order.slot?.deliveryTime.toISOString(), deliveryDate: order.slot?.deliveryTime.toISOString(),
orderStatus: order.orderStatus, orderStatus: orderStatusResult,
cancellationStatus: orderStatus, cancellationStatus: orderStatusResult,
cancelReason: status?.cancelReason || null, cancelReason: status?.cancelReason || null,
paymentMode, paymentMode,
paymentStatus, paymentStatus,
@ -721,29 +545,24 @@ export const orderRouter = router({
orderAmount: parseFloat(order.totalAmount.toString()), orderAmount: parseFloat(order.totalAmount.toString()),
isFlashDelivery: order.isFlashDelivery, isFlashDelivery: order.isFlashDelivery,
createdAt: order.createdAt.toISOString(), createdAt: order.createdAt.toISOString(),
totalAmount: parseFloat(order.totalAmount.toString()),
deliveryCharge: parseFloat(order.deliveryCharge.toString()),
}; };
}), }),
cancelOrder: protectedProcedure cancelOrder: protectedProcedure
.input( .input(
z.object({ z.object({
// id: z.string().regex(/^ORD\d+$/, "Invalid order ID format"),
id: z.number(), id: z.number(),
reason: z.string().min(1, "Cancellation reason is required"), reason: z.string().min(1, "Cancellation reason is required"),
}) })
) )
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<UserCancelOrderResponse> => {
try { try {
const userId = ctx.user.userId; const userId = ctx.user.userId;
const { id, reason } = input; const { id, reason } = input;
// Check if order exists and belongs to user const order = await getUserOrderBasic(id);
const order = await db.query.orders.findFirst({
where: eq(orders.id, Number(id)),
with: {
orderStatus: true,
},
});
if (!order) { if (!order) {
console.error("Order not found:", id); console.error("Order not found:", id);
@ -776,39 +595,11 @@ export const orderRouter = router({
throw new ApiError("Cannot cancel delivered order", 400); throw new ApiError("Cannot cancel delivered order", 400);
} }
// Perform database operations in transaction await cancelUserOrderTransaction(id, status.id, reason, order.isCod);
const result = await db.transaction(async (tx) => {
// Update order status
await tx
.update(orderStatus)
.set({
isCancelled: true,
cancelReason: reason,
cancellationUserNotes: reason,
cancellationReviewed: false,
})
.where(eq(orderStatus.id, status.id));
// Determine refund status based on payment method await sendOrderCancelledNotification(userId, id.toString());
const refundStatus = order.isCod ? "na" : "pending";
// Insert refund record await publishCancellation(id, 'user', reason);
await tx.insert(refunds).values({
orderId: order.id,
refundStatus,
});
return { orderId: order.id, userId };
});
// Send notification outside transaction (idempotent operation)
await sendOrderCancelledNotification(
result.userId,
result.orderId.toString()
);
// 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) { } catch (e) {
@ -824,25 +615,11 @@ export const orderRouter = router({
userNotes: z.string(), userNotes: z.string(),
}) })
) )
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<UserUpdateNotesResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
const { id, userNotes } = input; const { id, userNotes } = input;
// Extract readable ID from orderId (e.g., ORD001 -> 1) const order = await getUserOrderBasic(id);
// const readableIdMatch = id.match(/^ORD(\d+)$/);
// if (!readableIdMatch) {
// console.error("Invalid order ID format:", id);
// throw new ApiError("Invalid order ID format", 400);
// }
// const readableId = parseInt(readableIdMatch[1]);
// Check if order exists and belongs to user
const order = await db.query.orders.findFirst({
where: eq(orders.id, Number(id)),
with: {
orderStatus: true,
},
});
if (!order) { if (!order) {
console.error("Order not found:", id); console.error("Order not found:", id);
@ -864,7 +641,6 @@ export const orderRouter = router({
throw new ApiError("Order status not found", 400); throw new ApiError("Order status not found", 400);
} }
// Only allow updating notes for orders that are not delivered or cancelled
if (status.isDelivered) { if (status.isDelivered) {
console.error("Cannot update notes for delivered order:", id); console.error("Cannot update notes for delivered order:", id);
throw new ApiError("Cannot update notes for delivered order", 400); throw new ApiError("Cannot update notes for delivered order", 400);
@ -875,13 +651,7 @@ export const orderRouter = router({
throw new ApiError("Cannot update notes for cancelled order", 400); throw new ApiError("Cannot update notes for cancelled order", 400);
} }
// Update user notes await updateUserOrderNotes(id, userNotes);
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" };
}), }),
@ -894,72 +664,27 @@ export const orderRouter = router({
}) })
.optional() .optional()
) )
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }): Promise<UserRecentProductsResponse> => {
const { limit = 20 } = input || {}; const { limit = 20 } = input || {};
const userId = ctx.user.userId; const userId = ctx.user.userId;
// Get user's recent delivered orders (last 30 days)
const thirtyDaysAgo = new Date(); const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const recentOrders = await db const recentOrderIds = await getUserRecentlyDeliveredOrderIds(userId, 10, thirtyDaysAgo);
.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) { if (recentOrderIds.length === 0) {
return { success: true, products: [] }; return { success: true, products: [] };
} }
const orderIds = recentOrders.map((order) => order.id); const productIds = await getUserProductIdsFromOrders(recentOrderIds);
// Get unique product IDs from recent orders
const orderItemsResult = await db
.select({ productId: orderItems.productId })
.from(orderItems)
.where(inArray(orderItems.orderId, orderIds));
const productIds = [
...new Set(orderItemsResult.map((item) => item.productId)),
];
if (productIds.length === 0) { if (productIds.length === 0) {
return { success: true, products: [] }; return { success: true, products: [] };
} }
// Get product details const productsWithUnits = await getUserProductsForRecentOrders(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( const formattedProducts = await Promise.all(
productsWithUnits.map(async (product) => { productsWithUnits.map(async (product) => {
const nextDeliveryDate = await getNextDeliveryDate(product.id); const nextDeliveryDate = await getNextDeliveryDate(product.id);
@ -974,7 +699,7 @@ export const orderRouter = router({
nextDeliveryDate: nextDeliveryDate nextDeliveryDate: nextDeliveryDate
? nextDeliveryDate.toISOString() ? nextDeliveryDate.toISOString()
: null, : null,
images: scaffoldAssetUrl( images: scaffoldAssetUrl(
(product.images as string[]) || [] (product.images as string[]) || []
), ),
}; };

View file

@ -1,14 +1,23 @@
import { router, protectedProcedure } from '@/src/trpc/trpc-index'; import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'; import { z } from 'zod'
import { db } from '@/src/db/db_index'; import { ApiError } from '@/src/lib/api-error'
import { orders, payments, orderStatus } from '@/src/db/schema'; import crypto from 'crypto'
import { eq } from 'drizzle-orm'; import { razorpayId, razorpaySecret } from "@/src/lib/env-exporter"
import { ApiError } from '@/src/lib/api-error'; import { RazorpayPaymentService } from "@/src/lib/payments-utils"
import crypto from 'crypto'; import {
import { razorpayId, razorpaySecret } from "@/src/lib/env-exporter"; getUserPaymentOrderById as getUserPaymentOrderByIdInDb,
import { DiskPersistedSet } from "@/src/lib/disk-persisted-set"; getUserPaymentByOrderId as getUserPaymentByOrderIdInDb,
import { RazorpayPaymentService } from "@/src/lib/payments-utils"; getUserPaymentByMerchantOrderId as getUserPaymentByMerchantOrderIdInDb,
updateUserPaymentSuccess as updateUserPaymentSuccessInDb,
updateUserOrderPaymentStatus as updateUserOrderPaymentStatusInDb,
markUserPaymentFailed as markUserPaymentFailedInDb,
} from '@/src/dbService'
import type {
UserPaymentOrderResponse,
UserPaymentVerifyResponse,
UserPaymentFailResponse,
} from '@packages/shared'
@ -18,27 +27,36 @@ export const paymentRouter = router({
.input(z.object({ .input(z.object({
orderId: z.string(), orderId: z.string(),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<UserPaymentOrderResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
const { orderId } = input; const { orderId } = input;
// Validate order exists and belongs to user const order = await getUserPaymentOrderByIdInDb(parseInt(orderId))
/*
// Old implementation - direct DB queries:
const order = await db.query.orders.findFirst({ const order = await db.query.orders.findFirst({
where: eq(orders.id, parseInt(orderId)), where: eq(orders.id, parseInt(orderId)),
}); });
*/
if (!order) { if (!order) {
throw new ApiError("Order not found", 404); throw new ApiError("Order not found", 404)
} }
if (order.userId !== userId) { if (order.userId !== userId) {
throw new ApiError("Order does not belong to user", 403); throw new ApiError("Order does not belong to user", 403)
} }
// Check for existing pending payment // Check for existing pending payment
const existingPayment = await getUserPaymentByOrderIdInDb(parseInt(orderId))
/*
// Old implementation - direct DB queries:
const existingPayment = await db.query.payments.findFirst({ const existingPayment = await db.query.payments.findFirst({
where: eq(payments.orderId, parseInt(orderId)), where: eq(payments.orderId, parseInt(orderId)),
}); });
*/
if (existingPayment && existingPayment.status === 'pending') { if (existingPayment && existingPayment.status === 'pending') {
return { return {
@ -48,13 +66,13 @@ export const paymentRouter = router({
} }
// Create Razorpay order and insert payment record // Create Razorpay order and insert payment record
const razorpayOrder = await RazorpayPaymentService.createOrder(parseInt(orderId), order.totalAmount); const razorpayOrder = await RazorpayPaymentService.createOrder(parseInt(orderId), order.totalAmount);
await RazorpayPaymentService.insertPaymentRecord(parseInt(orderId), razorpayOrder); await RazorpayPaymentService.insertPaymentRecord(parseInt(orderId), razorpayOrder);
return { return {
razorpayOrderId: razorpayOrder.id, razorpayOrderId: 0,
key: razorpayId, key: razorpayId,
}; }
}), }),
@ -65,7 +83,7 @@ export const paymentRouter = router({
razorpay_order_id: z.string(), razorpay_order_id: z.string(),
razorpay_signature: z.string(), razorpay_signature: z.string(),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<UserPaymentVerifyResponse> => {
const { razorpay_payment_id, razorpay_order_id, razorpay_signature } = input; const { razorpay_payment_id, razorpay_order_id, razorpay_signature } = input;
// Verify signature // Verify signature
@ -79,9 +97,14 @@ export const paymentRouter = router({
} }
// Get current payment record // Get current payment record
const currentPayment = await getUserPaymentByMerchantOrderIdInDb(razorpay_order_id)
/*
// Old implementation - direct DB queries:
const currentPayment = await db.query.payments.findFirst({ const currentPayment = await db.query.payments.findFirst({
where: eq(payments.merchantOrderId, razorpay_order_id), where: eq(payments.merchantOrderId, razorpay_order_id),
}); });
*/
if (!currentPayment) { if (!currentPayment) {
throw new ApiError("Payment record not found", 404); throw new ApiError("Payment record not found", 404);
@ -94,6 +117,10 @@ export const paymentRouter = router({
signature: razorpay_signature, signature: razorpay_signature,
}; };
const updatedPayment = await updateUserPaymentSuccessInDb(razorpay_order_id, updatedPayload)
/*
// Old implementation - direct DB queries:
const [updatedPayment] = await db const [updatedPayment] = await db
.update(payments) .update(payments)
.set({ .set({
@ -103,56 +130,77 @@ export const paymentRouter = router({
.where(eq(payments.merchantOrderId, razorpay_order_id)) .where(eq(payments.merchantOrderId, razorpay_order_id))
.returning(); .returning();
// Update order status to mark payment as processed
await db await db
.update(orderStatus) .update(orderStatus)
.set({ .set({
paymentStatus: 'success', paymentStatus: 'success',
}) })
.where(eq(orderStatus.orderId, updatedPayment.orderId)); .where(eq(orderStatus.orderId, updatedPayment.orderId));
*/
if (!updatedPayment) {
throw new ApiError("Payment record not found", 404)
}
await updateUserOrderPaymentStatusInDb(updatedPayment.orderId, 'success')
return { return {
success: true, success: true,
message: "Payment verified successfully", message: "Payment verified successfully",
}; }
}), }),
markPaymentFailed: protectedProcedure markPaymentFailed: protectedProcedure
.input(z.object({ .input(z.object({
merchantOrderId: z.string(), merchantOrderId: z.string(),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<UserPaymentFailResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
const { merchantOrderId } = input; const { merchantOrderId } = input;
// Find payment by merchantOrderId // Find payment by merchantOrderId
const payment = await getUserPaymentByMerchantOrderIdInDb(merchantOrderId)
/*
// Old implementation - direct DB queries:
const payment = await db.query.payments.findFirst({ const payment = await db.query.payments.findFirst({
where: eq(payments.merchantOrderId, merchantOrderId), where: eq(payments.merchantOrderId, merchantOrderId),
}); });
*/
if (!payment) { if (!payment) {
throw new ApiError("Payment not found", 404); throw new ApiError("Payment not found", 404);
} }
// Check if payment belongs to user's order // Check if payment belongs to user's order
const order = await getUserPaymentOrderByIdInDb(payment.orderId)
/*
// Old implementation - direct DB queries:
const order = await db.query.orders.findFirst({ const order = await db.query.orders.findFirst({
where: eq(orders.id, payment.orderId), where: eq(orders.id, payment.orderId),
}); });
*/
if (!order || order.userId !== userId) { if (!order || order.userId !== userId) {
throw new ApiError("Payment does not belong to user", 403); throw new ApiError("Payment does not belong to user", 403);
} }
// Update payment status to failed // Update payment status to failed
await markUserPaymentFailedInDb(payment.id)
/*
// Old implementation - direct DB queries:
await db await db
.update(payments) .update(payments)
.set({ status: 'failed' }) .set({ status: 'failed' })
.where(eq(payments.id, payment.id)); .where(eq(payments.id, payment.id));
*/
return { return {
success: true, success: true,
message: "Payment marked as failed", message: "Payment marked as failed",
}; }
}), }),
}); });

View file

@ -1,39 +1,34 @@
import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index'; import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'; import { z } from 'zod'
import { db } from '@/src/db/db_index'; import { claimUploadUrl, extractKeyFromPresignedUrl, scaffoldAssetUrl } from '@/src/lib/s3-client'
import { productInfo, units, productSlots, deliverySlotInfo, specialDeals, storeInfo, productTagInfo, productTags, productReviews, users } from '@/src/db/schema'; import { ApiError } from '@/src/lib/api-error'
import { claimUploadUrl, extractKeyFromPresignedUrl, scaffoldAssetUrl } from '@/src/lib/s3-client'; import { getProductById as getProductByIdFromCache, getAllProducts as getAllProductsFromCache } from '@/src/stores/product-store'
import { ApiError } from '@/src/lib/api-error'; import dayjs from 'dayjs'
import { eq, and, gt, sql, inArray, desc } from 'drizzle-orm'; import {
import { getProductById as getProductByIdFromCache, getAllProducts as getAllProductsFromCache } from '@/src/stores/product-store'; getUserProductDetailById as getUserProductDetailByIdInDb,
import dayjs from 'dayjs'; getUserProductReviews as getUserProductReviewsInDb,
getUserProductByIdBasic as getUserProductByIdBasicInDb,
createUserProductReview as createUserProductReviewInDb,
} from '@/src/dbService'
import type {
UserProductDetail,
UserProductDetailData,
UserProductReviewsResponse,
UserCreateReviewResponse,
UserProductReviewWithSignedUrls,
} from '@packages/shared'
// Uniform Product Type const signProductImages = (product: UserProductDetailData): UserProductDetail => ({
interface Product { ...product,
id: number; images: scaffoldAssetUrl(product.images || []),
name: string; })
shortDescription: string | null;
longDescription: string | null;
price: string;
marketPrice: string | null;
unitNotation: string;
images: string[];
isOutOfStock: boolean;
store: { id: number; name: string; description: string | null } | null;
incrementStep: number;
productQuantity: number;
isFlashAvailable: boolean;
flashPrice: string | null;
deliverySlots: Array<{ id: number; deliveryTime: Date; freezeTime: Date }>;
specialDeals: Array<{ quantity: string; price: string; validTill: Date }>;
}
export const productRouter = router({ export const productRouter = router({
getProductDetails: publicProcedure getProductDetails: publicProcedure
.input(z.object({ .input(z.object({
id: z.string().regex(/^\d+$/, 'Invalid product ID'), id: z.string().regex(/^\d+$/, 'Invalid product ID'),
})) }))
.query(async ({ input }): Promise<Product> => { .query(async ({ input }): Promise<UserProductDetail> => {
const { id } = input; const { id } = input;
const productId = parseInt(id); const productId = parseInt(id);
@ -60,6 +55,10 @@ export const productRouter = router({
} }
// If not in cache, fetch from database (fallback) // If not in cache, fetch from database (fallback)
const productData = await getUserProductDetailByIdInDb(productId)
/*
// Old implementation - direct DB queries:
const productData = await db const productData = await db
.select({ .select({
id: productInfo.id, id: productInfo.id,
@ -81,82 +80,13 @@ export const productRouter = router({
.innerJoin(units, eq(productInfo.unitId, units.id)) .innerJoin(units, eq(productInfo.unitId, units.id))
.where(eq(productInfo.id, productId)) .where(eq(productInfo.id, productId))
.limit(1); .limit(1);
*/
if (productData.length === 0) { if (!productData) {
throw new Error('Product not found'); throw new Error('Product not found')
} }
const product = productData[0]; return signProductImages(productData)
// Fetch store info for this product
const storeData = product.storeId ? await db.query.storeInfo.findFirst({
where: eq(storeInfo.id, product.storeId),
columns: { id: true, name: true, description: true },
}) : null;
// Fetch delivery slots for this product
const deliverySlotsData = await db
.select({
id: deliverySlotInfo.id,
deliveryTime: deliverySlotInfo.deliveryTime,
freezeTime: deliverySlotInfo.freezeTime,
})
.from(productSlots)
.innerJoin(deliverySlotInfo, eq(productSlots.slotId, deliverySlotInfo.id))
.where(
and(
eq(productSlots.productId, productId),
eq(deliverySlotInfo.isActive, true),
eq(deliverySlotInfo.isCapacityFull, false),
gt(deliverySlotInfo.deliveryTime, sql`NOW()`),
gt(deliverySlotInfo.freezeTime, sql`NOW()`)
)
)
.orderBy(deliverySlotInfo.deliveryTime);
// Fetch special deals for this product
const specialDealsData = await db
.select({
quantity: specialDeals.quantity,
price: specialDeals.price,
validTill: specialDeals.validTill,
})
.from(specialDeals)
.where(
and(
eq(specialDeals.productId, productId),
gt(specialDeals.validTill, sql`NOW()`)
)
)
.orderBy(specialDeals.quantity);
// Generate signed URLs for images
const signedImages = scaffoldAssetUrl((product.images as string[]) || []);
const response: Product = {
id: product.id,
name: product.name,
shortDescription: product.shortDescription,
longDescription: product.longDescription,
price: product.price.toString(),
marketPrice: product.marketPrice?.toString() || null,
unitNotation: product.unitShortNotation,
images: signedImages,
isOutOfStock: product.isOutOfStock,
store: storeData ? {
id: storeData.id,
name: storeData.name,
description: storeData.description,
} : null,
incrementStep: product.incrementStep,
productQuantity: product.productQuantity,
isFlashAvailable: product.isFlashAvailable,
flashPrice: product.flashPrice?.toString() || null,
deliverySlots: deliverySlotsData,
specialDeals: specialDealsData.map(d => ({ quantity: d.quantity.toString(), price: d.price.toString(), validTill: d.validTill })),
};
return response;
}), }),
getProductReviews: publicProcedure getProductReviews: publicProcedure
@ -165,9 +95,13 @@ export const productRouter = router({
limit: z.number().int().min(1).max(50).optional().default(10), limit: z.number().int().min(1).max(50).optional().default(10),
offset: z.number().int().min(0).optional().default(0), offset: z.number().int().min(0).optional().default(0),
})) }))
.query(async ({ input }) => { .query(async ({ input }): Promise<UserProductReviewsResponse> => {
const { productId, limit, offset } = input; const { productId, limit, offset } = input;
const { reviews, totalCount } = await getUserProductReviewsInDb(productId, limit, offset)
/*
// Old implementation - direct DB queries:
const reviews = await db const reviews = await db
.select({ .select({
id: productReviews.id, id: productReviews.id,
@ -184,15 +118,6 @@ export const productRouter = router({
.limit(limit) .limit(limit)
.offset(offset); .offset(offset);
// Generate signed URLs for images
const reviewsWithSignedUrls = await Promise.all(
reviews.map(async (review) => ({
...review,
signedImageUrls: scaffoldAssetUrl((review.imageUrls as string[]) || []),
}))
);
// Check if more reviews exist
const totalCountResult = await db const totalCountResult = await db
.select({ count: sql`count(*)` }) .select({ count: sql`count(*)` })
.from(productReviews) .from(productReviews)
@ -200,8 +125,16 @@ export const productRouter = router({
const totalCount = Number(totalCountResult[0].count); const totalCount = Number(totalCountResult[0].count);
const hasMore = offset + limit < totalCount; const hasMore = offset + limit < totalCount;
*/
return { reviews: reviewsWithSignedUrls, hasMore }; const reviewsWithSignedUrls: UserProductReviewWithSignedUrls[] = reviews.map((review) => ({
...review,
signedImageUrls: scaffoldAssetUrl(review.imageUrls || []),
}))
const hasMore = offset + limit < totalCount
return { reviews: reviewsWithSignedUrls, hasMore }
}), }),
createReview: protectedProcedure createReview: protectedProcedure
@ -212,11 +145,20 @@ export const productRouter = router({
imageUrls: z.array(z.string()).optional().default([]), imageUrls: z.array(z.string()).optional().default([]),
uploadUrls: z.array(z.string()).optional().default([]), uploadUrls: z.array(z.string()).optional().default([]),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<UserCreateReviewResponse> => {
const { productId, reviewBody, ratings, imageUrls, uploadUrls } = input; const { productId, reviewBody, ratings, imageUrls, uploadUrls } = input;
const userId = ctx.user.userId; const userId = ctx.user.userId;
// Optional: Check if product exists const product = await getUserProductByIdBasicInDb(productId)
if (!product) {
throw new ApiError('Product not found', 404)
}
const imageKeys = uploadUrls.map(item => extractKeyFromPresignedUrl(item))
const newReview = await createUserProductReviewInDb(userId, productId, reviewBody, ratings, imageKeys)
/*
// Old implementation - direct DB queries:
const product = await db.query.productInfo.findFirst({ const product = await db.query.productInfo.findFirst({
where: eq(productInfo.id, productId), where: eq(productInfo.id, productId),
}); });
@ -224,7 +166,6 @@ export const productRouter = router({
throw new ApiError('Product not found', 404); throw new ApiError('Product not found', 404);
} }
// Insert review
const [newReview] = await db.insert(productReviews).values({ const [newReview] = await db.insert(productReviews).values({
userId, userId,
productId, productId,
@ -232,6 +173,7 @@ export const productRouter = router({
ratings, ratings,
imageUrls: uploadUrls.map(item => extractKeyFromPresignedUrl(item)), imageUrls: uploadUrls.map(item => extractKeyFromPresignedUrl(item)),
}).returning(); }).returning();
*/
// Claim upload URLs // Claim upload URLs
if (uploadUrls && uploadUrls.length > 0) { if (uploadUrls && uploadUrls.length > 0) {
@ -243,24 +185,25 @@ export const productRouter = router({
} }
} }
return { success: true, review: newReview }; return { success: true, review: newReview }
}), }),
getAllProductsSummary: publicProcedure getAllProductsSummary: publicProcedure
.query(async (): Promise<Product[]> => { .query(async (): Promise<UserProductDetail[]> => {
// Get all products from cache // Get all products from cache
const allCachedProducts = await getAllProductsFromCache(); const allCachedProducts = await getAllProductsFromCache();
// Transform the cached products to match the expected summary format // Transform the cached products to match the expected summary format
// (with empty deliverySlots and specialDeals arrays for summary view) // (with empty deliverySlots and specialDeals arrays for summary view)
const transformedProducts = allCachedProducts.map(product => ({ const transformedProducts: UserProductDetail[] = allCachedProducts.map(product => ({
...product, ...product,
deliverySlots: [], // Empty for summary view images: product.images || [],
specialDeals: [], // Empty for summary view deliverySlots: [],
})); specialDeals: [],
}))
return transformedProducts; return transformedProducts
}), }),
}); });

View file

@ -1,15 +1,9 @@
import { router, publicProcedure } from "@/src/trpc/trpc-index"; import { router, publicProcedure } from "@/src/trpc/trpc-index"
import { z } from "zod"; import { z } from "zod"
import { db } from "@/src/db/db_index"; import { getAllSlots as getAllSlotsFromCache, getSlotById as getSlotByIdFromCache } from "@/src/stores/slot-store"
import { import dayjs from 'dayjs'
deliverySlotInfo, import { getUserActiveSlotsList as getUserActiveSlotsListInDb, getUserProductAvailability as getUserProductAvailabilityInDb } from '@/src/dbService'
productSlots, import type { UserSlotData, UserSlotsListResponse, UserSlotsWithProductsResponse } from '@packages/shared'
productInfo,
units,
} from "@/src/db/schema";
import { eq, and, gt, asc } from "drizzle-orm";
import { getAllSlots as getAllSlotsFromCache, getSlotById as getSlotByIdFromCache } from "@/src/stores/slot-store";
import dayjs from 'dayjs';
// Helper method to get formatted slot data by ID // Helper method to get formatted slot data by ID
async function getSlotData(slotId: number) { async function getSlotData(slotId: number) {
@ -32,57 +26,71 @@ async function getSlotData(slotId: number) {
}; };
} }
export async function scaffoldSlotsWithProducts(): Promise<UserSlotsWithProductsResponse> {
const allSlots = await getAllSlotsFromCache();
const currentTime = new Date();
const validSlots = allSlots
.filter((slot) => {
return dayjs(slot.freezeTime).isAfter(currentTime) &&
dayjs(slot.deliveryTime).isAfter(currentTime) &&
!slot.isCapacityFull;
})
.sort((a, b) => dayjs(a.deliveryTime).valueOf() - dayjs(b.deliveryTime).valueOf());
const productAvailability = await getUserProductAvailabilityInDb()
/*
// Old implementation - direct DB query:
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,
name: product.name,
isOutOfStock: product.isOutOfStock,
isFlashAvailable: product.isFlashAvailable,
}));
*/
return {
slots: validSlots,
productAvailability,
count: validSlots.length,
};
}
export const slotsRouter = router({ export const slotsRouter = router({
getSlots: publicProcedure.query(async () => { getSlots: publicProcedure.query(async (): Promise<UserSlotsListResponse> => {
const slots = await getUserActiveSlotsListInDb()
/*
// Old implementation - direct DB query:
const slots = await db.query.deliverySlotInfo.findMany({ const slots = await db.query.deliverySlotInfo.findMany({
where: eq(deliverySlotInfo.isActive, true), where: eq(deliverySlotInfo.isActive, true),
}); });
*/
return { return {
slots, slots,
count: slots.length, count: slots.length,
};
}),
getSlotsWithProducts: publicProcedure.query(async () => {
const allSlots = await getAllSlotsFromCache();
const currentTime = new Date();
const validSlots = allSlots
.filter((slot) => {
return dayjs(slot.freezeTime).isAfter(currentTime) &&
dayjs(slot.deliveryTime).isAfter(currentTime) &&
!slot.isCapacityFull;
})
.sort((a, b) => dayjs(a.deliveryTime).valueOf() - dayjs(b.deliveryTime).valueOf());
return {
slots: validSlots,
count: validSlots.length,
};
}),
nextMajorDelivery: publicProcedure.query(async () => {
const now = new Date();
// Find the next upcoming active delivery slot ID
const nextSlot = await db.query.deliverySlotInfo.findFirst({
where: and(
eq(deliverySlotInfo.isActive, true),
gt(deliverySlotInfo.deliveryTime, now),
),
orderBy: asc(deliverySlotInfo.deliveryTime),
});
if (!nextSlot) {
return null; // No upcoming delivery slots
} }
}),
// Get formatted data using helper method getSlotsWithProducts: publicProcedure.query(async (): Promise<UserSlotsWithProductsResponse> => {
return await getSlotData(nextSlot.id); const response = await scaffoldSlotsWithProducts();
return response;
}), }),
getSlotById: publicProcedure getSlotById: publicProcedure
.input(z.object({ slotId: z.number() })) .input(z.object({ slotId: z.number() }))
.query(async ({ input }) => { .query(async ({ input }): Promise<UserSlotData | null> => {
return await getSlotData(input.slotId); return await getSlotData(input.slotId);
}), }),
}); });

View file

@ -1,143 +1,195 @@
import { router, publicProcedure } from '@/src/trpc/trpc-index'; import { router, publicProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'; import { z } from 'zod'
import { db } from '@/src/db/db_index'; import { scaffoldAssetUrl } from '@/src/lib/s3-client'
import { storeInfo, productInfo, units } from '@/src/db/schema'; import { ApiError } from '@/src/lib/api-error'
import { eq, and, sql } from 'drizzle-orm'; import { getTagsByStoreId } from '@/src/stores/product-tag-store'
import { scaffoldAssetUrl } from '@/src/lib/s3-client'; import {
import { ApiError } from '@/src/lib/api-error'; getUserStoreSummaries as getUserStoreSummariesInDb,
getUserStoreDetail as getUserStoreDetailInDb,
} from '@/src/dbService'
import type {
UserStoresResponse,
UserStoreDetail,
UserStoreSummary,
} from '@packages/shared'
export async function scaffoldStores(): Promise<UserStoresResponse> {
const storesData = await getUserStoreSummariesInDb()
/*
// Old implementation - direct DB queries:
const storesData = await db
.select({
id: storeInfo.id,
name: storeInfo.name,
description: storeInfo.description,
imageUrl: storeInfo.imageUrl,
productCount: sql<number>`count(${productInfo.id})`.as('productCount'),
})
.from(storeInfo)
.leftJoin(
productInfo,
and(eq(productInfo.storeId, storeInfo.id), eq(productInfo.isSuspended, false))
)
.groupBy(storeInfo.id);
*/
const storesWithDetails: UserStoreSummary[] = storesData.map((store) => {
const signedImageUrl = store.imageUrl ? scaffoldAssetUrl(store.imageUrl) : null
const sampleProducts = store.sampleProducts.map((product) => ({
id: product.id,
name: product.name,
signedImageUrl: product.images && product.images.length > 0
? scaffoldAssetUrl(product.images[0])
: null,
}))
return {
id: store.id,
name: store.name,
description: store.description,
signedImageUrl,
productCount: store.productCount,
sampleProducts,
}
})
return {
stores: storesWithDetails,
}
}
export async function scaffoldStoreWithProducts(storeId: number): Promise<UserStoreDetail> {
const storeDetail = await getUserStoreDetailInDb(storeId)
/*
// Old implementation - direct DB queries:
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);
}
const signedImageUrl = storeData.imageUrl ? scaffoldAssetUrl(storeData.imageUrl) : null;
const productsData = await db
.select({
id: productInfo.id,
name: productInfo.name,
shortDescription: productInfo.shortDescription,
price: productInfo.price,
marketPrice: productInfo.marketPrice,
images: productInfo.images,
isOutOfStock: productInfo.isOutOfStock,
incrementStep: productInfo.incrementStep,
unitShortNotation: units.shortNotation,
unitNotation: units.shortNotation,
productQuantity: productInfo.productQuantity,
})
.from(productInfo)
.innerJoin(units, eq(productInfo.unitId, units.id))
.where(and(eq(productInfo.storeId, storeId), eq(productInfo.isSuspended, false)));
const productsWithSignedUrls = await Promise.all(
productsData.map(async (product) => ({
id: product.id,
name: product.name,
shortDescription: product.shortDescription,
price: product.price,
marketPrice: product.marketPrice,
incrementStep: product.incrementStep,
unit: product.unitShortNotation,
unitNotation: product.unitNotation,
images: scaffoldAssetUrl((product.images as string[]) || []),
isOutOfStock: product.isOutOfStock,
productQuantity: product.productQuantity
}))
);
const tags = await getTagsByStoreId(storeId);
return {
store: {
id: storeData.id,
name: storeData.name,
description: storeData.description,
signedImageUrl,
},
products: productsWithSignedUrls,
tags: tags.map(tag => ({
id: tag.id,
tagName: tag.tagName,
tagDescription: tag.tagDescription,
imageUrl: tag.imageUrl,
productIds: tag.productIds,
})),
};
*/
if (!storeDetail) {
throw new ApiError('Store not found', 404)
}
const signedImageUrl = storeDetail.store.imageUrl
? scaffoldAssetUrl(storeDetail.store.imageUrl)
: null
const productsWithSignedUrls = storeDetail.products.map((product) => ({
id: product.id,
name: product.name,
shortDescription: product.shortDescription,
price: product.price,
marketPrice: product.marketPrice,
incrementStep: product.incrementStep,
unit: product.unit,
unitNotation: product.unitNotation,
images: scaffoldAssetUrl(product.images || []),
isOutOfStock: product.isOutOfStock,
productQuantity: product.productQuantity,
}))
const tags = await getTagsByStoreId(storeId)
return {
store: {
id: storeDetail.store.id,
name: storeDetail.store.name,
description: storeDetail.store.description,
signedImageUrl,
},
products: productsWithSignedUrls,
tags: tags.map(tag => ({
id: tag.id,
tagName: tag.tagName,
tagDescription: tag.tagDescription,
imageUrl: tag.imageUrl,
productIds: tag.productIds,
})),
}
}
export const storesRouter = router({ export const storesRouter = router({
getStores: publicProcedure getStores: publicProcedure
.query(async () => { .query(async (): Promise<UserStoresResponse> => {
const storesData = await db const response = await scaffoldStores();
.select({ return response;
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(
storesData.map(async (store) => {
const signedImageUrl = store.imageUrl ? scaffoldAssetUrl(store.imageUrl) : null;
// Fetch up to 3 products for this store
const sampleProducts = await db
.select({
id: productInfo.id,
name: productInfo.name,
images: productInfo.images,
})
.from(productInfo)
.where(and(eq(productInfo.storeId, store.id), eq(productInfo.isSuspended, false)))
.limit(3);
// Generate signed URLs for product images
const productsWithSignedUrls = await Promise.all(
sampleProducts.map(async (product) => {
const images = product.images as string[];
return {
id: product.id,
name: product.name,
signedImageUrl: (images && images.length > 0) ? scaffoldAssetUrl(images[0]) : null,
};
})
);
return {
id: store.id,
name: store.name,
description: store.description,
signedImageUrl,
productCount: store.productCount,
sampleProducts: productsWithSignedUrls,
};
})
);
return {
stores: storesWithDetails,
};
}), }),
getStoreWithProducts: publicProcedure getStoreWithProducts: publicProcedure
.input(z.object({ .input(z.object({
storeId: z.number(), storeId: z.number(),
})) }))
.query(async ({ input }) => { .query(async ({ input }): Promise<UserStoreDetail> => {
const { storeId } = input; const { storeId } = input;
const response = await scaffoldStoreWithProducts(storeId);
// Fetch store info return response;
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);
}
// Generate signed URL for store image
const signedImageUrl = storeData.imageUrl ? scaffoldAssetUrl(storeData.imageUrl) : null;
// Fetch products for this store
const productsData = await db
.select({
id: productInfo.id,
name: productInfo.name,
shortDescription: productInfo.shortDescription,
price: productInfo.price,
marketPrice: productInfo.marketPrice,
images: productInfo.images,
isOutOfStock: productInfo.isOutOfStock,
incrementStep: productInfo.incrementStep,
unitShortNotation: units.shortNotation,
unitNotation: units.shortNotation,
productQuantity: productInfo.productQuantity,
})
.from(productInfo)
.innerJoin(units, eq(productInfo.unitId, units.id))
.where(and(eq(productInfo.storeId, storeId), eq(productInfo.isSuspended, false)));
// Generate signed URLs for product images
const productsWithSignedUrls = await Promise.all(
productsData.map(async (product) => ({
id: product.id,
name: product.name,
shortDescription: product.shortDescription,
price: product.price,
marketPrice: product.marketPrice,
incrementStep: product.incrementStep,
unit: product.unitShortNotation,
unitNotation: product.unitNotation,
images: scaffoldAssetUrl((product.images as string[]) || []),
isOutOfStock: product.isOutOfStock,
productQuantity: product.productQuantity
}))
);
return {
store: {
id: storeData.id,
name: storeData.name,
description: storeData.description,
signedImageUrl,
},
products: productsWithSignedUrls,
};
}), }),
}); });

View file

@ -1,27 +1,23 @@
import { router, protectedProcedure, publicProcedure } from '@/src/trpc/trpc-index'; import { router, protectedProcedure, publicProcedure } from '@/src/trpc/trpc-index'
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken'
import { eq, and } from 'drizzle-orm'; import { z } from 'zod'
import { z } from 'zod'; import { ApiError } from '@/src/lib/api-error'
import { db } from '@/src/db/db_index'; import { jwtSecret } from '@/src/lib/env-exporter'
import { users, userDetails, userCreds, notifCreds, unloggedUserTokens } from '@/src/db/schema'; import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client'
import { ApiError } from '@/src/lib/api-error'; import {
import { jwtSecret } from '@/src/lib/env-exporter'; getUserProfileById as getUserProfileByIdInDb,
import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client'; getUserProfileDetailById as getUserProfileDetailByIdInDb,
getUserWithCreds as getUserWithCredsInDb,
interface AuthResponse { upsertUserNotifCred as upsertUserNotifCredInDb,
token: string; deleteUserUnloggedToken as deleteUserUnloggedTokenInDb,
user: { getUserUnloggedToken as getUserUnloggedTokenInDb,
id: number; upsertUserUnloggedToken as upsertUserUnloggedTokenInDb,
name: string | null; } from '@/src/dbService'
email: string | null; import type {
mobile: string | null; UserSelfDataResponse,
profileImage?: string | null; UserProfileCompleteResponse,
bio?: string | null; UserSavePushTokenResponse,
dateOfBirth?: string | null; } from '@packages/shared'
gender?: string | null;
occupation?: string | null;
};
}
const generateToken = (userId: number): string => { const generateToken = (userId: number): string => {
const secret = jwtSecret; const secret = jwtSecret;
@ -34,137 +30,87 @@ const generateToken = (userId: number): string => {
export const userRouter = router({ export const userRouter = router({
getSelfData: protectedProcedure getSelfData: protectedProcedure
.query(async ({ ctx }) => { .query(async ({ ctx }): Promise<UserSelfDataResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
if (!userId) { if (!userId) {
throw new ApiError('User not authenticated', 401); throw new ApiError('User not authenticated', 401);
} }
const [user] = await db const user = await getUserProfileByIdInDb(userId)
.select()
.from(users)
.where(eq(users.id, userId))
.limit(1);
if (!user) { if (!user) {
throw new ApiError('User not found', 404); throw new ApiError('User not found', 404);
} }
// Get user details for profile image // Get user details for profile image
const [userDetail] = await db const userDetail = await getUserProfileDetailByIdInDb(userId)
.select()
.from(userDetails)
.where(eq(userDetails.userId, userId))
.limit(1);
// Generate signed URL for profile image if it exists // Generate signed URL for profile image if it exists
const profileImageSignedUrl = userDetail?.profileImage const profileImageSignedUrl = userDetail?.profileImage
? await generateSignedUrlFromS3Url(userDetail.profileImage) ? await generateSignedUrlFromS3Url(userDetail.profileImage)
: null; : null;
const response: Omit<AuthResponse, 'token'> = {
user: {
id: user.id,
name: user.name,
email: user.email,
mobile: user.mobile,
profileImage: profileImageSignedUrl,
bio: userDetail?.bio || null,
dateOfBirth: userDetail?.dateOfBirth || null,
gender: userDetail?.gender || null,
occupation: userDetail?.occupation || null,
},
};
return { return {
success: true, success: true,
data: response, data: {
}; user: {
id: user.id,
name: user.name,
email: user.email,
mobile: user.mobile,
profileImage: profileImageSignedUrl,
bio: userDetail?.bio || null,
dateOfBirth: userDetail?.dateOfBirth || null,
gender: userDetail?.gender || null,
occupation: userDetail?.occupation || null,
},
},
}
}), }),
checkProfileComplete: protectedProcedure checkProfileComplete: protectedProcedure
.query(async ({ ctx }) => { .query(async ({ ctx }): Promise<UserProfileCompleteResponse> => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
if (!userId) { if (!userId) {
throw new ApiError('User not authenticated', 401); throw new ApiError('User not authenticated', 401);
} }
const result = await db const result = await getUserWithCredsInDb(userId)
.select()
.from(users)
.leftJoin(userCreds, eq(users.id, userCreds.userId))
.where(eq(users.id, userId))
.limit(1);
if (result.length === 0) { if (!result) {
throw new ApiError('User not found', 404); throw new ApiError('User not found', 404)
} }
const { users: user, user_creds: creds } = result[0];
return { return {
isComplete: !!(user.name && user.email && creds), isComplete: !!(result.user.name && result.user.email && result.creds),
}; };
}), }),
savePushToken: publicProcedure savePushToken: publicProcedure
.input(z.object({ token: z.string() })) .input(z.object({ token: z.string() }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<UserSavePushTokenResponse> => {
const { token } = input; const { token } = input;
const userId = ctx.user?.userId; const userId = ctx.user?.userId;
if (userId) { if (userId) {
// AUTHENTICATED USER // AUTHENTICATED USER
// Check if token exists in notif_creds for this user // Check if token exists in notif_creds for this user
const existing = await db.query.notifCreds.findFirst({ await upsertUserNotifCredInDb(userId, token)
where: and( await deleteUserUnloggedTokenInDb(token)
eq(notifCreds.userId, userId),
eq(notifCreds.token, token)
),
});
if (existing) {
// Update lastVerified timestamp
await db
.update(notifCreds)
.set({ lastVerified: new Date() })
.where(eq(notifCreds.id, existing.id));
} else {
// Insert new token into notif_creds
await db.insert(notifCreds).values({
userId,
token,
lastVerified: new Date(),
});
}
// Remove from unlogged_user_tokens if it exists
await db
.delete(unloggedUserTokens)
.where(eq(unloggedUserTokens.token, token));
} else { } else {
// UNAUTHENTICATED USER // UNAUTHENTICATED USER
// Save/update in unlogged_user_tokens // Save/update in unlogged_user_tokens
const existing = await db.query.unloggedUserTokens.findFirst({ const existing = await getUserUnloggedTokenInDb(token)
where: eq(unloggedUserTokens.token, token),
});
if (existing) { if (existing) {
await db await upsertUserUnloggedTokenInDb(token)
.update(unloggedUserTokens)
.set({ lastVerified: new Date() })
.where(eq(unloggedUserTokens.id, existing.id));
} else { } else {
await db.insert(unloggedUserTokens).values({ await upsertUserUnloggedTokenInDb(token)
token,
lastVerified: new Date(),
});
} }
} }
return { success: true }; return { success: true }
}), }),
}); });

View file

@ -3,6 +3,11 @@ import { z } from 'zod';
import { adminRouter } from '@/src/trpc/apis/admin-apis/apis/admin-trpc-index' import { adminRouter } from '@/src/trpc/apis/admin-apis/apis/admin-trpc-index'
import { userRouter } from '@/src/trpc/apis/user-apis/apis/user-trpc-index' import { userRouter } from '@/src/trpc/apis/user-apis/apis/user-trpc-index'
import { commonApiRouter } from '@/src/trpc/apis/common-apis/common-trpc-index' import { commonApiRouter } from '@/src/trpc/apis/common-apis/common-trpc-index'
import { scaffoldProducts } from './apis/common-apis/common';
import { scaffoldStores, scaffoldStoreWithProducts } from './apis/user-apis/apis/stores';
import { scaffoldSlotsWithProducts } from './apis/user-apis/apis/slots';
import { scaffoldEssentialConsts } from './apis/common-apis/common-trpc-index';
import { scaffoldBanners } from './apis/user-apis/apis/banners';
// Create the main app router // Create the main app router
export const appRouter = router({ export const appRouter = router({
@ -16,5 +21,13 @@ export const appRouter = router({
common: commonApiRouter, common: commonApiRouter,
}); });
// Export type definition of API // Export type definition of API
export type AppRouter = typeof appRouter; export type AppRouter = typeof appRouter;
export type AllProductsApiType = Awaited<ReturnType<typeof scaffoldProducts>>;
export type StoresApiType = Awaited<ReturnType<typeof scaffoldStores>>;
export type SlotsApiType = Awaited<ReturnType<typeof scaffoldSlotsWithProducts>>;
export type EssentialConstsApiType = Awaited<ReturnType<typeof scaffoldEssentialConsts>>;
export type BannersApiType = Awaited<ReturnType<typeof scaffoldBanners>>;
export type StoreWithProductsApiType = Awaited<ReturnType<typeof scaffoldStoreWithProducts>>;

View file

@ -0,0 +1,4 @@
// Database Types - Re-exports from shared package
// Central type definitions for backend database operations
export type { Banner } from '@packages/shared';

View file

@ -33,6 +33,12 @@
"shared-types": ["../shared-types"], "shared-types": ["../shared-types"],
"@commonTypes": ["../../packages/ui/shared-types"], "@commonTypes": ["../../packages/ui/shared-types"],
"@commonTypes/*": ["../../packages/ui/shared-types/*"], "@commonTypes/*": ["../../packages/ui/shared-types/*"],
"@packages/shared": ["../../packages/shared"],
"@packages/shared/*": ["../../packages/shared/*"],
"postgresService": ["../../packages/db_helper_postgres"],
"postgresService/*": ["../../packages/db_helper_postgres/*"],
"global-shared": ["../../packages/shared"],
"global-shared/*": ["../../packages/shared/*"]
}, },
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [""], /* Specify multiple folders that act like './node_modules/@types'. */ // "typeRoots": [""], /* Specify multiple folders that act like './node_modules/@types'. */
@ -116,6 +122,6 @@
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */
}, },
"include": ["src", "types", "index.ts", "../shared-types"] "include": ["src", "types", "index.ts", "../shared-types", "../../packages/shared"]
} }

View file

@ -11,6 +11,7 @@ import { CreateCouponRoute } from './routes/create-coupon'
import { LocationMarkerRoute } from './routes/location-marker' import { LocationMarkerRoute } from './routes/location-marker'
import { UserConnectRoute } from './routes/user-connect' import { UserConnectRoute } from './routes/user-connect'
import Inauguration from './routes/inauguration' import Inauguration from './routes/inauguration'
import { DemoRoute } from './routes/demo'
import { AuthWrapper } from './components/AuthWrapper' import { AuthWrapper } from './components/AuthWrapper'
import { SuperAdminGuard } from './components/SuperAdminGuard' import { SuperAdminGuard } from './components/SuperAdminGuard'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
@ -124,6 +125,16 @@ const locationMarkerRoute = new Route({
) )
}) })
const demoRoute = new Route({
getParentRoute: () => rootRoute,
path: '/demo',
component: () => (
<Suspense fallback={<p>Loading demo</p>}>
<DemoRoute />
</Suspense>
)
})
const routeTree = rootRoute.addChildren([ const routeTree = rootRoute.addChildren([
dashboardRoute, dashboardRoute,
vendorOrderListRoute, vendorOrderListRoute,
@ -133,7 +144,8 @@ const routeTree = rootRoute.addChildren([
createCouponRoute, createCouponRoute,
userConnectRoute, userConnectRoute,
locationMarkerRoute, locationMarkerRoute,
inaugurationRoute inaugurationRoute,
demoRoute
]) ])
export function createAppRouter() { export function createAppRouter() {

View file

@ -0,0 +1,259 @@
import { useState } from 'react'
import { getAuthToken } from '@/services/auth'
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:4000'
export function DemoRoute() {
const [method, setMethod] = useState<string>('GET')
const [endpoint, setEndpoint] = useState<string>('/api/test')
const [headers, setHeaders] = useState<string>('{}')
const [body, setBody] = useState<string>('{}')
const [response, setResponse] = useState<any>(null)
const [error, setError] = useState<string>('')
const [loading, setLoading] = useState(false)
const [history, setHistory] = useState<Array<{ method: string; endpoint: string; timestamp: string }>>([])
const handleSubmit = async () => {
setLoading(true)
setError('')
setResponse(null)
try {
const token = await getAuthToken()
const url = `${API_BASE_URL}${endpoint}`
let parsedHeaders: Record<string, string> = {}
try {
parsedHeaders = JSON.parse(headers)
} catch {
throw new Error('Invalid headers JSON')
}
const fetchOptions: RequestInit = {
method,
headers: {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
...parsedHeaders,
},
}
if (method !== 'GET' && method !== 'HEAD') {
try {
const parsedBody = JSON.parse(body)
fetchOptions.body = JSON.stringify(parsedBody)
} catch {
throw new Error('Invalid body JSON')
}
}
const startTime = performance.now()
const res = await fetch(url, fetchOptions)
const endTime = performance.now()
const duration = Math.round(endTime - startTime)
let data
const contentType = res.headers.get('content-type')
if (contentType && contentType.includes('application/json')) {
data = await res.json()
} else {
data = await res.text()
}
setResponse({
status: res.status,
statusText: res.statusText,
duration: `${duration}ms`,
headers: Object.fromEntries(res.headers.entries()),
data,
})
// Add to history
setHistory(prev => [
{ method, endpoint, timestamp: new Date().toLocaleTimeString() },
...prev.slice(0, 9), // Keep last 10
])
} catch (err: any) {
setError(err.message || 'An error occurred')
} finally {
setLoading(false)
}
}
const loadFromHistory = (item: { method: string; endpoint: string }) => {
setMethod(item.method)
setEndpoint(item.endpoint)
}
const getStatusColor = (status: number) => {
if (status >= 200 && status < 300) return 'bg-green-500'
if (status >= 300 && status < 400) return 'bg-yellow-500'
if (status >= 400) return 'bg-red-500'
return 'bg-gray-500'
}
return (
<div className="max-w-6xl mx-auto p-6 space-y-6">
<div className="flex items-center justify-between mb-6">
<h1 className="text-3xl font-bold text-gray-900">API Demo & Testing</h1>
<span className="px-3 py-1 bg-gray-200 rounded-full text-sm text-gray-700">
{API_BASE_URL}
</span>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Request Panel */}
<div className="lg:col-span-2 bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold mb-4">Request</h2>
<div className="space-y-4">
<div className="flex gap-2">
<select
value={method}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => setMethod(e.target.value)}
className="px-4 py-2 border rounded-md bg-white w-32"
>
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="PATCH">PATCH</option>
<option value="DELETE">DELETE</option>
</select>
<input
type="text"
placeholder="/api/endpoint"
value={endpoint}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setEndpoint(e.target.value)}
className="flex-1 px-4 py-2 border rounded-md"
/>
<button
onClick={handleSubmit}
disabled={loading}
className="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:bg-blue-400 transition-colors"
>
{loading ? 'Sending...' : 'Send'}
</button>
</div>
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700">Headers (JSON)</label>
<textarea
value={headers}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setHeaders(e.target.value)}
placeholder='{"Custom-Header": "value"}'
className="w-full px-4 py-2 border rounded-md font-mono text-sm min-h-[80px]"
/>
</div>
{method !== 'GET' && method !== 'HEAD' && (
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700">Body (JSON)</label>
<textarea
value={body}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setBody(e.target.value)}
placeholder='{"key": "value"}'
className="w-full px-4 py-2 border rounded-md font-mono text-sm min-h-[120px]"
/>
</div>
)}
</div>
</div>
{/* History Panel */}
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold mb-4">History</h2>
<div className="space-y-2 max-h-[400px] overflow-y-auto">
{history.length === 0 ? (
<p className="text-gray-500 text-sm">No requests yet</p>
) : (
history.map((item, index) => (
<button
key={index}
onClick={() => loadFromHistory(item)}
className="w-full text-left p-3 rounded hover:bg-gray-100 transition-colors text-sm border"
>
<div className="flex items-center gap-2">
<span className={`px-2 py-0.5 rounded text-xs font-medium ${
item.method === 'GET' ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-700'
}`}>
{item.method}
</span>
<span className="truncate flex-1 text-gray-900">{item.endpoint}</span>
</div>
<div className="text-xs text-gray-500 mt-1">
{item.timestamp}
</div>
</button>
))
)}
</div>
</div>
{/* Response Panel */}
<div className="lg:col-span-3 bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold mb-4">Response</h2>
{error ? (
<div className="p-4 bg-red-50 border border-red-200 rounded-md text-red-700">
<p className="font-medium">Error</p>
<p className="text-sm">{error}</p>
</div>
) : response ? (
<div className="space-y-4">
<div className="flex items-center gap-4">
<span className={`px-3 py-1 rounded-full text-white text-sm font-medium ${getStatusColor(response.status)}`}>
{response.status} {response.statusText}
</span>
<span className="px-3 py-1 border rounded-full text-sm">
{response.duration}
</span>
</div>
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700">Response Headers</label>
<pre className="bg-gray-100 p-3 rounded-md overflow-x-auto text-xs">
{JSON.stringify(response.headers, null, 2)}
</pre>
</div>
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700">Response Body</label>
<pre className="bg-gray-100 p-3 rounded-md overflow-x-auto text-xs">
{typeof response.data === 'string'
? response.data
: JSON.stringify(response.data, null, 2)}
</pre>
</div>
</div>
) : (
<div className="text-center py-12 text-gray-500">
<p>Send a request to see the response</p>
</div>
)}
</div>
</div>
{/* Quick Links */}
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold mb-4">Quick Test Endpoints</h2>
<div className="flex flex-wrap gap-2">
{[
{ method: 'GET', endpoint: '/trpc/user.banner.getActiveBanners' },
{ method: 'GET', endpoint: '/trpc/user.product.getAllProducts' },
{ method: 'GET', endpoint: '/trpc/user.user.getCurrentUser' },
{ method: 'POST', endpoint: '/trpc/user.auth.login' },
].map((item, index) => (
<button
key={index}
onClick={() => loadFromHistory(item)}
className="px-4 py-2 border rounded-md hover:bg-gray-50 transition-colors text-sm"
>
<span className="mr-2 px-2 py-0.5 bg-gray-200 rounded text-xs">
{item.method}
</span>
{item.endpoint}
</button>
))}
</div>
</div>
</div>
)
}

View file

@ -24,12 +24,6 @@
"@/*": [ "@/*": [
"./src/*" "./src/*"
], ],
"common-ui": [
"../../packages/ui"
],
"common-ui/*": [
"../../packages/ui/*"
]
}, },
"types": [ "types": [
"node" "node"

View file

@ -5,7 +5,7 @@ import { trpc } from '@/src/trpc-client';
import { MyText, MyTouchableOpacity, tw, AppContainer } from 'common-ui'; import { MyText, MyTouchableOpacity, tw, AppContainer } from 'common-ui';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import MaterialIcons from '@expo/vector-icons/MaterialIcons'; import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { useGetEssentialConsts } from '@/src/api-hooks/essential-consts.api'; import { useGetEssentialConsts } from '@/src/hooks/prominent-api-hooks';
export default function FlashDeliveryBaseLayout() { export default function FlashDeliveryBaseLayout() {
const router = useRouter(); const router = useRouter();

View file

@ -21,13 +21,15 @@ import AddToCartDialog from "@/src/components/AddToCartDialog";
import MyFlatList from "common-ui/src/components/flat-list"; import MyFlatList from "common-ui/src/components/flat-list";
import { trpc } from "@/src/trpc-client"; import { trpc } from "@/src/trpc-client";
import { useAllProducts, useStores, useSlots, useGetEssentialConsts } from "@/src/hooks/prominent-api-hooks";
import { useProductSlotIdentifier } from "@/hooks/useProductSlotIdentifier"; import { useProductSlotIdentifier } from "@/hooks/useProductSlotIdentifier";
import { useCentralSlotStore } from "@/src/store/centralSlotStore";
import { useCentralProductStore } from "@/src/store/centralProductStore";
import FloatingCartBar from "@/components/floating-cart-bar"; import FloatingCartBar from "@/components/floating-cart-bar";
import BannerCarousel from "@/components/BannerCarousel"; import BannerCarousel from "@/components/BannerCarousel";
import { useUserDetails } from "@/src/contexts/AuthContext"; import { useUserDetails } from "@/src/contexts/AuthContext";
import TabLayoutWrapper from "@/components/TabLayoutWrapper"; import TabLayoutWrapper from "@/components/TabLayoutWrapper";
import { useNavigationStore } from "@/src/store/navigationStore"; import { useNavigationStore } from "@/src/store/navigationStore";
import { useGetEssentialConsts } from "@/src/api-hooks/essential-consts.api";
import NextOrderGlimpse from "@/components/NextOrderGlimpse"; import NextOrderGlimpse from "@/components/NextOrderGlimpse";
dayjs.extend(relativeTime); dayjs.extend(relativeTime);
@ -360,8 +362,6 @@ export default function Dashboard() {
const router = useRouter(); const router = useRouter();
const userDetails = useUserDetails(); const userDetails = useUserDetails();
const [inputQuery, setInputQuery] = useState(""); const [inputQuery, setInputQuery] = useState("");
const [searchQuery, setSearchQuery] = useState("");
const [selectedTagId, setSelectedTagId] = useState<number | null>(null);
const [isLoadingDialogOpen, setIsLoadingDialogOpen] = useState(false); const [isLoadingDialogOpen, setIsLoadingDialogOpen] = useState(false);
const [gradientHeight, setGradientHeight] = useState(0); const [gradientHeight, setGradientHeight] = useState(0);
const [displayedProducts, setDisplayedProducts] = useState<any[]>([]); const [displayedProducts, setDisplayedProducts] = useState<any[]>([]);
@ -369,22 +369,21 @@ export default function Dashboard() {
const [isLoadingMore, setIsLoadingMore] = useState(false); const [isLoadingMore, setIsLoadingMore] = useState(false);
const { backgroundColor } = useStatusBarStore(); const { backgroundColor } = useStatusBarStore();
const { getQuickestSlot } = useProductSlotIdentifier(); const { getQuickestSlot } = useProductSlotIdentifier();
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
const refetchProducts = useCentralProductStore((state) => state.refetchProducts);
const refetchSlotsFromStore = useCentralSlotStore((state) => state.refetchSlots);
const [isRefreshing, setIsRefreshing] = useState(false); const [isRefreshing, setIsRefreshing] = useState(false);
const { const {
data: productsData, data: productsData,
isLoading, isLoading,
error, error,
refetch, } = useAllProducts();
} = trpc.common.product.getAllProductsSummary.useQuery({
searchQuery: searchQuery || undefined,
tagId: selectedTagId || undefined,
});
const { data: essentialConsts, isLoading: isLoadingConsts, error: constsError, refetch: refetchConsts } = useGetEssentialConsts(); const { data: essentialConsts, isLoading: isLoadingConsts, error: constsError, refetch: refetchConsts } = useGetEssentialConsts();
const { data: storesData, refetch: refetchStores } = trpc.user.stores.getStores.useQuery(); const { data: storesData, refetch: refetchStores } = useStores();
const { data: slotsData, refetch: refetchSlots } = trpc.user.slots.getSlotsWithProducts.useQuery(); const { data: slotsData } = useSlots();
const products = productsData?.products || []; const products = productsData?.products || [];
@ -397,15 +396,18 @@ export default function Dashboard() {
const slotB = getQuickestSlot(b.id); const slotB = getQuickestSlot(b.id);
if (slotA && !slotB) return -1; if (slotA && !slotB) return -1;
if (!slotA && slotB) return 1; if (!slotA && slotB) return 1;
if (a.isOutOfStock && !b.isOutOfStock) return 1; const aOutOfStock = productSlotsMap[a.id]?.isOutOfStock;
if (!a.isOutOfStock && b.isOutOfStock) return -1; const bOutOfStock = productSlotsMap[b.id]?.isOutOfStock;
if (aOutOfStock && !bOutOfStock) return 1;
if (!aOutOfStock && bOutOfStock) return -1;
return 0; return 0;
}); });
console.log('setting the displayed products')
setDisplayedProducts(initialBatch); setDisplayedProducts(initialBatch);
setHasMore(products.length > 10); setHasMore(products.length > 10);
} }
}, [productsData]); }, [productsData, productSlotsMap]);
const popularItemIds = useMemo(() => { const popularItemIds = useMemo(() => {
const popularItems = essentialConsts?.popularItems; const popularItems = essentialConsts?.popularItems;
@ -440,11 +442,22 @@ export default function Dashboard() {
const handleRefresh = useCallback(async () => { const handleRefresh = useCallback(async () => {
setIsRefreshing(true); setIsRefreshing(true);
try { try {
await Promise.all([refetch(), refetchStores(), refetchSlots(), refetchConsts()]); const promises = [];
if (refetchProducts) {
promises.push(refetchProducts());
}
if (refetchSlotsFromStore) {
promises.push(refetchSlotsFromStore());
}
promises.push(refetchStores());
promises.push(refetchConsts());
await Promise.all(promises);
} finally { } finally {
setIsRefreshing(false); setIsRefreshing(false);
} }
}, [refetch, refetchStores, refetchSlots, refetchConsts]); }, [refetchProducts, refetchSlotsFromStore, refetchStores, refetchConsts]);
useManualRefresh(() => { useManualRefresh(() => {
handleRefresh(); handleRefresh();
@ -468,6 +481,7 @@ export default function Dashboard() {
const renderProductItem = useCallback(({ item }: { item: any }) => ( const renderProductItem = useCallback(({ item }: { item: any }) => (
<ProductItem item={item} onPress={handleProductPress} /> <ProductItem item={item} onPress={handleProductPress} />
// <Image style={{ width: 150, height: 235 }} source={{ uri: item.images[0]}} />
), [handleProductPress]); ), [handleProductPress]);
const listHeader = useMemo(() => ( const listHeader = useMemo(() => (
@ -512,7 +526,9 @@ export default function Dashboard() {
</View> </View>
); );
} }
let str = ''
displayedProducts.forEach(product => str += `${product.id}-`)
// console.log(str)
return ( return (
<TabLayoutWrapper> <TabLayoutWrapper>
<View style={searchBarContainerStyle}> <View style={searchBarContainerStyle}>

View file

@ -1,4 +1,4 @@
import React, { useState, useRef, useEffect, useCallback } from "react"; import React, { useState, useRef, useEffect, useCallback, useMemo } from "react";
import { View, Dimensions } from "react-native"; import { View, Dimensions } from "react-native";
import { useRouter, useLocalSearchParams } from "expo-router"; import { useRouter, useLocalSearchParams } from "expo-router";
import { import {
@ -10,7 +10,8 @@ import {
SearchBar, SearchBar,
} from "common-ui"; } from "common-ui";
import MaterialIcons from "@expo/vector-icons/MaterialIcons"; import MaterialIcons from "@expo/vector-icons/MaterialIcons";
import { trpc } from "@/src/trpc-client"; import Fuse from "fuse.js";
import { useAllProducts } from "@/src/hooks/prominent-api-hooks";
import ProductCard from "@/components/ProductCard"; import ProductCard from "@/components/ProductCard";
import FloatingCartBar from "@/components/floating-cart-bar"; import FloatingCartBar from "@/components/floating-cart-bar";
@ -51,12 +52,27 @@ export default function SearchResults() {
}); });
}, []); }, []);
const { data: productsData, isLoading, error, refetch } = const { data: productsData, isLoading, error, refetch } = useAllProducts();
trpc.common.product.getAllProductsSummary.useQuery({
searchQuery: debouncedQuery || undefined, const allProducts = productsData?.products || [];
// Client-side search filtering using Fuse.js
const products = useMemo(() => {
if (!debouncedQuery.trim()) return allProducts;
const fuse = new Fuse(allProducts, {
keys: [
'name',
'shortDescription',
],
threshold: 0.3,
includeScore: true,
shouldSort: true,
}); });
const products = productsData?.products || []; const fuseResults = fuse.search(debouncedQuery);
return fuseResults.map(result => result.item);
}, [allProducts, debouncedQuery]);
useManualRefresh(() => { useManualRefresh(() => {
refetch(); refetch();

View file

@ -4,7 +4,7 @@ import { Image } from 'expo-image';
import { MyText, tw, useManualRefresh, MyFlatList, useMarkDataFetchers, theme, MyTouchableOpacity } from 'common-ui'; import { MyText, tw, useManualRefresh, MyFlatList, useMarkDataFetchers, theme, MyTouchableOpacity } from 'common-ui';
import { MaterialIcons, Ionicons } from '@expo/vector-icons'; import { MaterialIcons, Ionicons } from '@expo/vector-icons';
import { trpc } from '@/src/trpc-client'; import { trpc } from '@/src/trpc-client';
import { useGetEssentialConsts } from '@/src/api-hooks/essential-consts.api'; import { useGetEssentialConsts } from '@/src/hooks/prominent-api-hooks';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';

View file

@ -1,9 +0,0 @@
import { Stack } from 'expo-router'
function DeliverySlotsLayout() {
return (
<Stack screenOptions={{ headerShown: true, title: 'Delivery Slots' }} />
)
}
export default DeliverySlotsLayout

View file

@ -1,230 +0,0 @@
import React, { useState } from 'react';
import { View, ScrollView } from 'react-native';
import { Image } from 'expo-image';
import { useRouter } from 'expo-router';
import { MyFlatList, MyText, tw, useMarkDataFetchers, BottomDialog, theme, MyTouchableOpacity } from 'common-ui';
import { trpc } from '@/src/trpc-client';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import dayjs from 'dayjs';
export default function DeliverySlots() {
const router = useRouter();
const { data, isLoading, error, refetch } = trpc.user.slots.getSlotsWithProducts.useQuery();
const [selectedSlotForDialog, setSelectedSlotForDialog] = useState<any>(null);
useMarkDataFetchers(() => {
refetch();
});
if (isLoading) {
return (
<MyFlatList
data={[]}
renderItem={() => null}
ListHeaderComponent={() => (
<View style={tw`flex-1 justify-center items-center p-8`}>
<MyText style={tw`text-gray-600`}>Loading delivery slots...</MyText>
</View>
)}
/>
);
}
if (error) {
return (
<MyFlatList
data={[]}
renderItem={() => null}
ListHeaderComponent={() => (
<View style={tw`flex-1 justify-center items-center p-8`}>
<MyText style={tw`text-red-600`}>Error loading delivery slots</MyText>
<MyTouchableOpacity
onPress={() => refetch()}
style={tw`mt-4 bg-blue-500 px-4 py-2 rounded-lg`}
>
<MyText style={tw`text-white font-semibold`}>Retry</MyText>
</MyTouchableOpacity>
</View>
)}
/>
);
}
const slots = data?.slots || [];
if (slots.length === 0) {
return (
<MyFlatList
data={[]}
renderItem={() => null}
ListHeaderComponent={() => (
<View style={tw`flex-1 justify-center items-center p-8`}>
<MaterialIcons name="schedule" size={64} color="#D1D5DB" />
<MyText style={tw`text-gray-500 text-center mt-4 text-lg`}>
No upcoming delivery slots available
</MyText>
<MyText style={tw`text-gray-400 text-center mt-2`}>
Check back later for new delivery schedules
</MyText>
</View>
)}
/>
);
}
return (
<>
<MyFlatList
data={slots}
keyExtractor={(item) => item.id.toString()}
// ListHeaderComponent={() => (
// <View style={tw`p-4 pb-2`}>
// <MyText style={tw`text-2xl font-bold text-gray-800`}>Delivery Slots</MyText>
// <MyText style={tw`text-gray-600 mt-1`}>
// Choose your preferred delivery time
// </MyText>
// </View>
// )}
renderItem={({ item: slot }) => (
<View style={tw`mx-4 mb-4 bg-white rounded-xl shadow-md overflow-hidden`}>
{/* Slot Header */}
<View style={tw`bg-pink-50 p-4 border-b border-pink-100`}>
<View style={tw`flex-row items-center justify-between`}>
<View>
<MyText style={tw`text-lg font-bold text-gray-800`}>
{dayjs(slot.deliveryTime).format('ddd DD MMM, h:mm a')}
</MyText>
<MyText style={tw`text-sm text-gray-600 mt-1`}>
Orders close by: {dayjs(slot.freezeTime).format('h:mm a')}
</MyText>
</View>
<View style={tw`flex-row items-center`}>
<View style={tw`bg-pink-500 px-3 py-1 rounded-full mr-3`}>
<MyText style={tw`text-white text-sm font-semibold`}>
{slot.products.length} items
</MyText>
</View>
<MyTouchableOpacity
onPress={() => router.push(`/(drawer)/(tabs)/home/cart?slot=${slot.id}`)}
style={tw`bg-pink-500 p-2 rounded-full`}
>
<MaterialIcons name="flash-on" size={16} color="white" />
</MyTouchableOpacity>
</View>
</View>
</View>
{/* Products List */}
<View style={tw`p-4`}>
<MyText style={tw`text-base font-semibold text-gray-700 mb-3`}>
Available Products
</MyText>
<View style={tw`space-y-2`}>
{slot.products.slice(0, 2).map((product) => (
<MyTouchableOpacity
key={product.id}
onPress={() => router.push(`/(drawer)/(tabs)/home/product-detail/${product.id}`)}
style={tw`bg-gray-50 rounded-lg p-3 flex-row items-center`}
>
{product.images && product.images.length > 0 ? (
<Image
source={{ uri: product.images[0] }}
style={tw`w-8 h-8 rounded mr-3`}
resizeMode="cover"
/>
) : (
<View style={tw`w-8 h-8 bg-gray-200 rounded mr-3 justify-center items-center`}>
<MaterialIcons name="image" size={16} color="#9CA3AF" />
</View>
)}
<View style={tw`flex-1`}>
<MyText style={tw`text-sm font-medium text-gray-800`} numberOfLines={1}>
{product.name}
</MyText>
<MyText style={tw`text-xs text-gray-600`}>
{product.price} {product.unit && `per ${product.unit}`}
</MyText>
</View>
{product.isOutOfStock && (
<MyText style={tw`text-xs text-red-500 font-medium`}>Out of stock</MyText>
)}
</MyTouchableOpacity>
))}
{slot.products.length > 2 && (
<MyTouchableOpacity
onPress={() => setSelectedSlotForDialog(slot)}
style={tw`bg-pink-50 rounded-lg p-3 flex-row items-center justify-center border border-pink-200`}
>
<MyText style={tw`text-sm font-medium text-pink-700`}>
+{slot.products.length - 2} more products
</MyText>
<MaterialIcons name="chevron-right" size={16} color={theme.colors.brand500} style={tw`ml-1`} />
</MyTouchableOpacity>
)}
</View>
</View>
</View>
)}
ListFooterComponent={() => <View style={tw`h-4`} />}
showsVerticalScrollIndicator={false}
contentContainerStyle={tw`pt-2`}
/>
{/* Products Dialog */}
<BottomDialog
open={!!selectedSlotForDialog}
onClose={() => setSelectedSlotForDialog(null)}
>
<View style={tw`p-6`}>
<MyText style={tw`text-xl font-bold text-gray-800 mb-4`}>
All Products - {dayjs(selectedSlotForDialog?.deliveryTime).format('ddd DD MMM, h:mm a')}
</MyText>
<ScrollView style={tw`max-h-96`} showsVerticalScrollIndicator={false}>
<View style={tw`space-y-3`}>
{selectedSlotForDialog?.products.map((product: any) => (
<MyTouchableOpacity
key={product.id}
onPress={() => {
setSelectedSlotForDialog(null);
router.push(`/(drawer)/(tabs)/home/product-detail/${product.id}`);
}}
style={tw`bg-gray-50 rounded-lg p-4 flex-row items-center`}
>
{product.images && product.images.length > 0 ? (
<Image
source={{ uri: product.images[0] }}
style={tw`w-12 h-12 rounded mr-4`}
resizeMode="cover"
/>
) : (
<View style={tw`w-12 h-12 bg-gray-200 rounded mr-4 justify-center items-center`}>
<MaterialIcons name="image" size={20} color="#9CA3AF" />
</View>
)}
<View style={tw`flex-1`}>
<MyText style={tw`text-base font-medium text-gray-800`} numberOfLines={1}>
{product.name}
</MyText>
<MyText style={tw`text-sm text-gray-600 mt-1`}>
{product.price} {product.unit && `per ${product.unit}`}
</MyText>
{product.marketPrice && (
<MyText style={tw`text-sm text-gray-500 line-through`}>
{product.marketPrice}
</MyText>
)}
</View>
{product.isOutOfStock && (
<MyText style={tw`text-xs text-red-500 font-medium`}>Out of stock</MyText>
)}
</MyTouchableOpacity>
))}
</View>
</ScrollView>
</View>
</BottomDialog>
</>
);
}

View file

@ -16,7 +16,7 @@ import {
} from "common-ui"; } from "common-ui";
import MaterialIcons from "@expo/vector-icons/MaterialIcons"; import MaterialIcons from "@expo/vector-icons/MaterialIcons";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { trpc } from "@/src/trpc-client"; import { useStores } from "@/src/hooks/prominent-api-hooks";
import { LinearGradient } from "expo-linear-gradient"; import { LinearGradient } from "expo-linear-gradient";
import TabLayoutWrapper from "@/components/TabLayoutWrapper"; import TabLayoutWrapper from "@/components/TabLayoutWrapper";
import FloatingCartBar from "@/components/floating-cart-bar"; import FloatingCartBar from "@/components/floating-cart-bar";
@ -157,7 +157,7 @@ export default function Stores() {
isLoading, isLoading,
error, error,
refetch, refetch,
} = trpc.user.stores.getStores.useQuery(); } = useStores();
const stores = storesData?.stores || []; const stores = storesData?.stores || [];

View file

@ -1,4 +1,4 @@
import React, { useState } from "react"; import React, { useMemo, useState } from "react";
import { View, Dimensions, ScrollView, TouchableOpacity } from "react-native"; import { View, Dimensions, ScrollView, TouchableOpacity } from "react-native";
import { useRouter, useLocalSearchParams } from "expo-router"; import { useRouter, useLocalSearchParams } from "expo-router";
import { Image } from 'expo-image'; import { Image } from 'expo-image';
@ -13,10 +13,10 @@ import {
} from "common-ui"; } from "common-ui";
import MaterialIcons from "@expo/vector-icons/MaterialIcons"; import MaterialIcons from "@expo/vector-icons/MaterialIcons";
import FontAwesome5 from "@expo/vector-icons/FontAwesome5"; import FontAwesome5 from "@expo/vector-icons/FontAwesome5";
import { trpc } from "@/src/trpc-client";
import ProductCard from "@/components/ProductCard"; import ProductCard from "@/components/ProductCard";
import FloatingCartBar from "@/components/floating-cart-bar"; import FloatingCartBar from "@/components/floating-cart-bar";
import { useStoreHeaderStore } from "@/src/store/storeHeaderStore"; import { useStoreHeaderStore } from "@/src/store/storeHeaderStore";
import { useAllProducts, useStoreWithProducts } from "@/src/hooks/prominent-api-hooks";
const { width: screenWidth } = Dimensions.get("window"); const { width: screenWidth } = Dimensions.get("window");
const itemWidth = (screenWidth - 48) / 2; const itemWidth = (screenWidth - 48) / 2;
@ -63,24 +63,32 @@ export default function StoreDetail() {
const [selectedTagId, setSelectedTagId] = useState<number | null>(null); const [selectedTagId, setSelectedTagId] = useState<number | null>(null);
const { data: storeData, isLoading, refetch, error } = const { data: storeData, isLoading, refetch, error } =
trpc.user.stores.getStoreWithProducts.useQuery( useStoreWithProducts(storeIdNum);
{ storeId: storeIdNum },
{ enabled: !!storeIdNum }
);
const { data: tagsData, isLoading: isLoadingTags } = const { data: productsData, isLoading: isProductsLoading } = useAllProducts();
trpc.user.tags.getTagsByStore.useQuery(
{ storeId: storeIdNum }, const productById = useMemo(() => {
{ enabled: !!storeIdNum } const map = new Map<number, any>();
); productsData?.products?.forEach((product) => {
map.set(product.id, product);
});
return map;
}, [productsData]);
const storeProducts = useMemo(() => {
if (!storeData?.products) return [];
return storeData.products
.map((product) => productById.get(product.id))
.filter(Boolean);
}, [storeData, productById]);
// Filter products based on selected tag // Filter products based on selected tag
const filteredProducts = selectedTagId const filteredProducts = selectedTagId
? storeData?.products.filter(product => { ? storeProducts.filter(product => {
const selectedTag = tagsData?.tags.find(t => t.id === selectedTagId); const selectedTag = storeData?.tags.find(t => t.id === selectedTagId);
return selectedTag?.productIds?.includes(product.id) ?? false; return selectedTag?.productIds?.includes(product.id) ?? false;
}) || [] })
: storeData?.products || []; : storeProducts;
// Set the store header title // Set the store header title
const setStoreHeaderTitle = useStoreHeaderStore((state) => state.setTitle); const setStoreHeaderTitle = useStoreHeaderStore((state) => state.setTitle);
@ -98,10 +106,12 @@ export default function StoreDetail() {
useDrawerTitle(storeData?.store?.name || "Store", [storeData?.store?.name]); useDrawerTitle(storeData?.store?.name || "Store", [storeData?.store?.name]);
if (isLoading) { if (isLoading || isProductsLoading) {
return ( return (
<View style={tw`flex-1 justify-center items-center bg-gray-50`}> <View style={tw`flex-1 justify-center items-center bg-gray-50`}>
<MyText style={tw`text-gray-500 font-medium`}>Loading store...</MyText> <MyText style={tw`text-gray-500 font-medium`}>
{isLoading ? 'Loading store...' : 'Loading products...'}
</MyText>
</View> </View>
); );
} }
@ -184,13 +194,13 @@ export default function StoreDetail() {
)} )}
</View> </View>
{/* Tags Section */} {/* Tags Section */}
{tagsData && tagsData.tags.length > 0 && ( {storeData?.tags && storeData.tags.length > 0 && (
<ScrollView <ScrollView
horizontal horizontal
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}
contentContainerStyle={tw`gap-2 mt-6`} contentContainerStyle={tw`gap-2 mt-6`}
> >
{tagsData.tags.map((tag) => ( {storeData.tags.map((tag) => (
<Chip <Chip
key={tag.id} key={tag.id}
tag={tag} tag={tag}
@ -206,7 +216,7 @@ export default function StoreDetail() {
<MaterialIcons name="grid-view" size={20} color="#374151" /> <MaterialIcons name="grid-view" size={20} color="#374151" />
<MyText style={tw`text-lg font-bold text-gray-900 ml-2`}> <MyText style={tw`text-lg font-bold text-gray-900 ml-2`}>
{selectedTagId {selectedTagId
? `${tagsData?.tags.find(t => t.id === selectedTagId)?.tagName} items` ? `${storeData?.tags.find(t => t.id === selectedTagId)?.tagName} items`
: `${filteredProducts.length} products`} : `${filteredProducts.length} products`}
</MyText> </MyText>
</View> </View>

View file

@ -22,6 +22,7 @@ import LocationTestWrapper from "@/components/LocationTestWrapper";
import HealthTestWrapper from "@/components/HealthTestWrapper"; import HealthTestWrapper from "@/components/HealthTestWrapper";
import FirstUserWrapper from "@/components/FirstUserWrapper"; import FirstUserWrapper from "@/components/FirstUserWrapper";
import UpdateChecker from "@/components/UpdateChecker"; import UpdateChecker from "@/components/UpdateChecker";
import CentralStoreInitializer from "@/src/components/CentralStoreInitializer";
import { RefreshProvider } from "../../../packages/ui/src/lib/refresh-context"; import { RefreshProvider } from "../../../packages/ui/src/lib/refresh-context";
import WebViewWrapper from "@/components/WebViewWrapper"; import WebViewWrapper from "@/components/WebViewWrapper";
import BackHandlerWrapper from "@/components/BackHandler"; import BackHandlerWrapper from "@/components/BackHandler";
@ -68,10 +69,12 @@ export default function RootLayout() {
<PaperProvider> <PaperProvider>
<LocationTestWrapper> <LocationTestWrapper>
<RefreshProvider queryClient={queryClient}> <RefreshProvider queryClient={queryClient}>
<CentralStoreInitializer>
<BackHandlerWrapper /> <BackHandlerWrapper />
<Stack screenOptions={{ headerShown: false }} /> <Stack screenOptions={{ headerShown: false }} />
<AddToCartDialog /> <AddToCartDialog />
</RefreshProvider> </CentralStoreInitializer>
</RefreshProvider>
</LocationTestWrapper> </LocationTestWrapper>
</PaperProvider> </PaperProvider>
</NotificationProvider> </NotificationProvider>

View file

@ -2,7 +2,7 @@ import React, { useState, useRef, useEffect } from 'react';
import { View, Dimensions, Image, ScrollView, NativeSyntheticEvent, NativeScrollEvent } from 'react-native'; import { View, Dimensions, Image, ScrollView, NativeSyntheticEvent, NativeScrollEvent } from 'react-native';
import { MyTouchableOpacity, MyText, tw } from 'common-ui'; import { MyTouchableOpacity, MyText, tw } from 'common-ui';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import { trpc } from '@/src/trpc-client'; import { useBanners } from '@/src/hooks/prominent-api-hooks';
import MaterialIcons from '@expo/vector-icons/MaterialIcons'; import MaterialIcons from '@expo/vector-icons/MaterialIcons';
const { width: screenWidth } = Dimensions.get('window'); const { width: screenWidth } = Dimensions.get('window');
@ -25,7 +25,7 @@ export default function BannerCarousel() {
const [isAutoPlaying, setIsAutoPlaying] = useState(true); const [isAutoPlaying, setIsAutoPlaying] = useState(true);
// Fetch banners data // Fetch banners data
const { data: bannersData, isLoading, error } = trpc.user.banner.getBanners.useQuery(); const { data: bannersData, isLoading, error } = useBanners();
const banners = bannersData?.banners || []; const banners = bannersData?.banners || [];
@ -123,7 +123,7 @@ export default function BannerCarousel() {
{/* Pagination Dots */} {/* Pagination Dots */}
{banners.length > 1 && ( {banners.length > 1 && (
<View style={tw`flex-row justify-center mt-3`}> <View style={tw`flex-row justify-center mt-3`}>
{banners.map((_, index: number) => ( {banners.map((_: Banner, index: number) => (
<MyTouchableOpacity <MyTouchableOpacity
key={index} key={index}
onPress={() => goToSlide(index)} onPress={() => goToSlide(index)}

View file

@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import { View, ActivityIndicator, Platform } from 'react-native'; import { View, ActivityIndicator, Platform } from 'react-native';
import { tw, theme, MyText, MyTouchableOpacity , BottomDialog } from 'common-ui'; import { tw, theme, MyText, MyTouchableOpacity , BottomDialog } from 'common-ui';
import { trpc, trpcClient } from '@/src/trpc-client'; import { trpc, trpcClient } from '@/src/trpc-client';
import { useGetEssentialConsts } from '@/src/api-hooks/essential-consts.api'; import { useGetEssentialConsts } from '@/src/hooks/prominent-api-hooks';
import Constants from 'expo-constants'; import Constants from 'expo-constants';
import * as Linking from 'expo-linking'; import * as Linking from 'expo-linking';

View file

@ -8,7 +8,7 @@ import dayjs from 'dayjs';
import { trpc } from '@/src/trpc-client'; import { trpc } from '@/src/trpc-client';
import { Image } from 'expo-image'; import { Image } from 'expo-image';
import { orderStatusManipulator } from '@/src/lib/string-manipulators'; import { orderStatusManipulator } from '@/src/lib/string-manipulators';
import { useGetEssentialConsts } from '@/src/api-hooks/essential-consts.api'; import { useGetEssentialConsts } from '@/src/hooks/prominent-api-hooks';
interface OrderItem { interface OrderItem {
productName: string; productName: string;

View file

@ -6,6 +6,8 @@ import MaterialIcons from '@expo/vector-icons/MaterialIcons';
// import RazorpayCheckout from 'react-native-razorpay'; // import RazorpayCheckout from 'react-native-razorpay';
import { trpc } from '@/src/trpc-client'; import { trpc } from '@/src/trpc-client';
import { useCentralProductStore } from '@/src/store/centralProductStore';
import { useCentralSlotStore } from '@/src/store/centralSlotStore';
import { clearLocalCart } from '@/hooks/cart-query-hooks'; import { clearLocalCart } from '@/hooks/cart-query-hooks';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { FontAwesome5, FontAwesome6 } from '@expo/vector-icons'; import { FontAwesome5, FontAwesome6 } from '@expo/vector-icons';
@ -54,17 +56,19 @@ const PaymentAndOrderComponent: React.FC<PaymentAndOrderProps> = ({
queryClient.invalidateQueries({ queryKey: [`local-cart-${cartType}`] }); queryClient.invalidateQueries({ queryKey: [`local-cart-${cartType}`] });
}; };
const { data: productsData } = trpc.common.product.getAllProductsSummary.useQuery({}); const products = useCentralProductStore((state) => state.products);
const productsById = useCentralProductStore((state) => state.productsById);
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
// Memoized flash-eligible product IDs // Memoized flash-eligible product IDs
const flashEligibleProductIds = useMemo(() => { const flashEligibleProductIds = useMemo(() => {
if (!productsData?.products) return new Set<number>(); if (!products.length) return new Set<number>();
return new Set( return new Set(
productsData.products products
.filter((product: any) => product.isFlashAvailable) .filter((product) => productSlotsMap[product.id]?.isFlashAvailable)
.map((product: any) => product.id) .map((product) => product.id)
); );
}, [productsData]); }, [products, productSlotsMap]);
const placeOrderMutation = trpc.user.order.placeOrder.useMutation({ const placeOrderMutation = trpc.user.order.placeOrder.useMutation({
onSuccess: (data) => { onSuccess: (data) => {
@ -126,7 +130,7 @@ const PaymentAndOrderComponent: React.FC<PaymentAndOrderProps> = ({
const availableItems = cartItems const availableItems = cartItems
.filter(item => { .filter(item => {
if (item.product?.isOutOfStock) return false; if (productSlotsMap[item.productId]?.isOutOfStock) return false;
// For flash delivery, check if product supports flash delivery // For flash delivery, check if product supports flash delivery
if (isFlashDelivery) { if (isFlashDelivery) {
return flashEligibleProductIds.has(item.productId); return flashEligibleProductIds.has(item.productId);

View file

@ -1,5 +1,5 @@
import React, { useMemo } from 'react'; import React from 'react';
import { View, Alert, TouchableOpacity, Text } from 'react-native'; import { View, Alert, ActivityIndicator } from 'react-native';
import { Image } from 'expo-image'; import { Image } from 'expo-image';
import { tw, theme, MyText, MyTouchableOpacity, Quantifier, MiniQuantifier } from 'common-ui'; import { tw, theme, MyText, MyTouchableOpacity, Quantifier, MiniQuantifier } from 'common-ui';
import CartIcon from '@/components/icons/CartIcon'; import CartIcon from '@/components/icons/CartIcon';
@ -14,7 +14,7 @@ import {
} from '@/hooks/cart-query-hooks'; } from '@/hooks/cart-query-hooks';
import { useProductSlotIdentifier } from '@/hooks/useProductSlotIdentifier'; import { useProductSlotIdentifier } from '@/hooks/useProductSlotIdentifier';
import { useCartStore } from '@/src/store/cartStore'; import { useCartStore } from '@/src/store/cartStore';
import { trpc } from '@/src/trpc-client'; import { useCentralSlotStore } from '@/src/store/centralSlotStore';
import { Image as RnImage } from 'react-native' import { Image as RnImage } from 'react-native'
@ -46,6 +46,18 @@ const ProductCard: React.FC<ProductCardProps> = ({
containerComp: ContainerComp = React.Fragment, containerComp: ContainerComp = React.Fragment,
useAddToCartDialog = false, useAddToCartDialog = false,
}) => { }) => {
const imageUri = item.images?.[0]
const [imageStatus, setImageStatus] = React.useState<'loading' | 'loaded' | 'error'>('loading')
const [imageError, setImageError] = React.useState<string | null>(null)
const [updater, setUpdater] = React.useState(0)
React.useEffect(() => {
const intervalId = setInterval(() => {
setUpdater(prev => prev + 1)
}, 5000)
return () => clearInterval(intervalId)
}, [])
const { data: cartData } = useGetCart(); const { data: cartData } = useGetCart();
const { getQuickestSlot } = useProductSlotIdentifier(); const { getQuickestSlot } = useProductSlotIdentifier();
const { setAddedToCartProduct } = useCartStore(); const { setAddedToCartProduct } = useCartStore();
@ -69,25 +81,41 @@ const ProductCard: React.FC<ProductCardProps> = ({
const cartItem = cartData?.items?.find((cartItem: any) => cartItem.productId === item.id); const cartItem = cartData?.items?.find((cartItem: any) => cartItem.productId === item.id);
const quantity = cartItem?.quantity || 0; const quantity = cartItem?.quantity || 0;
// Query all slots with products // Get slots data from central store
const { data: slotsData } = trpc.user.slots.getSlotsWithProducts.useQuery(); const slots = useCentralSlotStore((state) => state.slots);
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
// Create slot lookup map // Create slot lookup map
const slotMap = useMemo(() => { const slotMap = React.useMemo(() => {
const map: Record<number, any> = {}; const map: Record<number, any> = {};
slotsData?.slots?.forEach((slot: any) => { slots?.forEach((slot: any) => {
map[slot.id] = slot; map[slot.id] = slot;
}); });
return map; return map;
}, [slotsData]); }, [slots]);
// Get cart item's slot delivery time if item is in cart // Get cart item's slot delivery time if item is in cart
const cartSlot = cartItem?.slotId ? slotMap[cartItem.slotId] : null; const cartSlot = cartItem?.slotId ? slotMap[cartItem.slotId] : null;
const displayDeliveryDate = cartSlot?.deliveryTime || item.nextDeliveryDate; const displayDeliveryDate = cartSlot?.deliveryTime || item.nextDeliveryDate;
React.useEffect(() => {
if (imageUri) {
setImageStatus('loading')
setImageError(null)
return
}
setImageStatus('error')
setImageError('No image available')
}, [imageUri])
// Precompute the next slot and determine display out of stock status // Precompute the next slot and determine display out of stock status
const slotId = getQuickestSlot(item.id); const slotId = getQuickestSlot(item.id);
const displayIsOutOfStock = item.isOutOfStock || !slotId;
// Use isOutOfStock from productSlotsMap (all products now included)
const productSlotInfo = productSlotsMap[item.id];
const isOutOfStockFromSlots = productSlotInfo?.isOutOfStock;
const displayIsOutOfStock = isOutOfStockFromSlots || !slotId;
// if(item.name.startsWith('Mutton Curry Cut')) { // if(item.name.startsWith('Mutton Curry Cut')) {
// console.log({slotId, displayIsOutOfStock}) // console.log({slotId, displayIsOutOfStock})
@ -119,6 +147,7 @@ const ProductCard: React.FC<ProductCardProps> = ({
} }
}; };
// console.log('rendering the product cart for id', item.id)
return ( return (
<ContainerComp> <ContainerComp>
<MyTouchableOpacity <MyTouchableOpacity
@ -131,9 +160,32 @@ const ProductCard: React.FC<ProductCardProps> = ({
> >
<View style={tw`relative`}> <View style={tw`relative`}>
<RnImage <RnImage
source={{ uri: item.images?.[0] }} source={{ uri: imageUri }}
// source={{uri: 'https://pub-6bf1fbc4048a4cbaa533ddbb13bf9de6.r2.dev/product-images/1763796113884-0'}}
style={{ width: "100%", height: itemWidth, resizeMode: "cover" }} style={{ width: "100%", height: itemWidth, resizeMode: "cover" }}
onLoadStart={() => {
setImageStatus('loading')
setImageError(null)
}}
// onLoadEnd={() => {
// setImageError('loading stopped indefinitely')
//
// }}
onLoad={() => setImageStatus('loaded')}
onError={(event) => {
setImageStatus('error')
setImageError( 'Image failed to load')
}}
/> />
{imageStatus === 'error' && (
<View style={tw`absolute inset-0 items-center justify-center bg-gray-100`}>
<MaterialIcons name="broken-image" size={22} color="#94A3B8" />
<MyText style={tw`text-[10px] text-gray-500 mt-1`}>
{imageError || 'Image failed to load'}
</MyText>
</View>
)}
{displayIsOutOfStock && ( {displayIsOutOfStock && (
<View style={tw`absolute inset-0 bg-black/40 items-center justify-center`}> <View style={tw`absolute inset-0 bg-black/40 items-center justify-center`}>
<View style={tw`bg-red-500 px-3 py-1 rounded-full`}> <View style={tw`bg-red-500 px-3 py-1 rounded-full`}>

View file

@ -12,9 +12,11 @@ import { trpc, trpcClient } from '@/src/trpc-client';
import { useAddToCart, useGetCart, useUpdateCartItem, useRemoveFromCart } from '@/hooks/cart-query-hooks'; import { useAddToCart, useGetCart, useUpdateCartItem, useRemoveFromCart } from '@/hooks/cart-query-hooks';
import { useProductSlotIdentifier } from '@/hooks/useProductSlotIdentifier'; import { useProductSlotIdentifier } from '@/hooks/useProductSlotIdentifier';
import { useFlashNavigationStore } from '@/components/stores/flashNavigationStore'; import { useFlashNavigationStore } from '@/components/stores/flashNavigationStore';
import { useSlots } from '@/src/hooks/prominent-api-hooks';
import FloatingCartBar from './floating-cart-bar'; import FloatingCartBar from './floating-cart-bar';
import { useStoreHeaderStore } from '@/src/store/storeHeaderStore'; import { useStoreHeaderStore } from '@/src/store/storeHeaderStore';
import { useCartStore } from '@/src/store/cartStore'; import { useCartStore } from '@/src/store/cartStore';
import { useCentralSlotStore } from '@/src/store/centralSlotStore';
const { width: screenWidth } = Dimensions.get("window"); const { width: screenWidth } = Dimensions.get("window");
const carouselWidth = screenWidth; const carouselWidth = screenWidth;
@ -57,15 +59,28 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
const { getQuickestSlot } = useProductSlotIdentifier(); const { getQuickestSlot } = useProductSlotIdentifier();
const { setShouldNavigateToCart } = useFlashNavigationStore(); const { setShouldNavigateToCart } = useFlashNavigationStore();
const { setAddedToCartProduct } = useCartStore(); const { setAddedToCartProduct } = useCartStore();
const { data: slotsData } = useSlots();
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
const productAvailability = useMemo(() => {
if (!productDetail) return null;
return productSlotsMap[productDetail.id];
}, [productDetail, productSlotsMap]);
const sortedDeliverySlots = useMemo(() => { const sortedDeliverySlots = useMemo(() => {
if (!productDetail?.deliverySlots) return [] if (!slotsData?.slots || !productDetail) return []
return [...productDetail.deliverySlots].sort((a, b) => {
// Filter slots that contain this product
const productSlots = slotsData.slots.filter((slot: any) =>
slot.products?.some((p: any) => p.id === productDetail.id)
)
return productSlots.sort((a: any, b: any) => {
const deliveryDiff = new Date(a.deliveryTime).getTime() - new Date(b.deliveryTime).getTime() const deliveryDiff = new Date(a.deliveryTime).getTime() - new Date(b.deliveryTime).getTime()
if (deliveryDiff !== 0) return deliveryDiff if (deliveryDiff !== 0) return deliveryDiff
return new Date(a.freezeTime).getTime() - new Date(b.freezeTime).getTime() return new Date(a.freezeTime).getTime() - new Date(b.freezeTime).getTime()
}) })
}, [productDetail?.deliverySlots]) }, [slotsData, productDetail])
// Find current quantity from cart data // Find current quantity from cart data
const cartItem = productDetail ? cartData?.data?.items?.find((item: any) => item.productId === productDetail.id) : null; const cartItem = productDetail ? cartData?.data?.items?.find((item: any) => item.productId === productDetail.id) : null;
@ -94,7 +109,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
const handleAddToCart = (productId: number) => { const handleAddToCart = (productId: number) => {
if (isFlashDelivery) { if (isFlashDelivery) {
if (!productDetail?.isFlashAvailable) { if (!productAvailability?.isFlashAvailable) {
Alert.alert("Error", "This product is not available for flash delivery"); Alert.alert("Error", "This product is not available for flash delivery");
return; return;
} }
@ -113,7 +128,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
const handleBuyNow = (productId: number) => { const handleBuyNow = (productId: number) => {
if (isFlashDelivery) { if (isFlashDelivery) {
if (!productDetail?.isFlashAvailable) { if (!productAvailability?.isFlashAvailable) {
Alert.alert("Error", "This product is not available for flash delivery"); Alert.alert("Error", "This product is not available for flash delivery");
return; return;
} }
@ -241,13 +256,13 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
<View style={tw`flex-row justify-between items-start mb-2`}> <View style={tw`flex-row justify-between items-start mb-2`}>
<MyText style={tw`text-2xl font-bold text-gray-900 flex-1 mr-2`}>{productDetail.name}</MyText> <MyText style={tw`text-2xl font-bold text-gray-900 flex-1 mr-2`}>{productDetail.name}</MyText>
<View style={tw`flex-row gap-2`}> <View style={tw`flex-row gap-2`}>
{productDetail.isFlashAvailable && ( {productAvailability?.isFlashAvailable && (
<View style={tw`bg-pink-100 px-3 py-1 rounded-full flex-row items-center`}> <View style={tw`bg-pink-100 px-3 py-1 rounded-full flex-row items-center`}>
<MaterialIcons name="bolt" size={12} color="#EC4899" style={tw`mr-1`} /> <MaterialIcons name="bolt" size={12} color="#EC4899" style={tw`mr-1`} />
<MyText style={tw`text-pink-700 text-xs font-bold`}>1 Hr Delivery</MyText> <MyText style={tw`text-pink-700 text-xs font-bold`}>1 Hr Delivery</MyText>
</View> </View>
)} )}
{productDetail.isOutOfStock && ( {productAvailability?.isOutOfStock && (
<View style={tw`bg-red-100 px-3 py-1 rounded-full`}> <View style={tw`bg-red-100 px-3 py-1 rounded-full`}>
<MyText style={tw`text-red-700 text-xs font-bold`}>Out of Stock</MyText> <MyText style={tw`text-red-700 text-xs font-bold`}>Out of Stock</MyText>
</View> </View>
@ -277,7 +292,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
</View> </View>
{/* Flash price on separate line - smaller and less prominent */} {/* Flash price on separate line - smaller and less prominent */}
{productDetail.isFlashAvailable && productDetail.flashPrice && productDetail.flashPrice !== productDetail.price && ( {productAvailability?.isFlashAvailable && productDetail.flashPrice && productDetail.flashPrice !== productDetail.price && (
<View style={tw`mt-1`}> <View style={tw`mt-1`}>
<MyText style={tw`text-pink-600 text-lg font-bold`}> <MyText style={tw`text-pink-600 text-lg font-bold`}>
1 Hr Delivery: {productDetail.flashPrice} / {formatQuantity(productDetail.productQuantity || 1, productDetail.unitNotation).display} 1 Hr Delivery: {productDetail.flashPrice} / {formatQuantity(productDetail.productQuantity || 1, productDetail.unitNotation).display}
@ -304,11 +319,11 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
// Show "Add to Cart" button when not in cart // Show "Add to Cart" button when not in cart
<MyTouchableOpacity <MyTouchableOpacity
style={[tw`flex-1 py-3.5 rounded-xl items-center border`, { style={[tw`flex-1 py-3.5 rounded-xl items-center border`, {
borderColor: (productDetail.isOutOfStock || (isFlashDelivery && !productDetail.isFlashAvailable)) ? '#9ca3af' : theme.colors.brand500, borderColor: (productAvailability?.isOutOfStock || (isFlashDelivery && !productAvailability?.isFlashAvailable)) ? '#9ca3af' : theme.colors.brand500,
backgroundColor: 'white' backgroundColor: 'white'
}]} }]}
onPress={() => { onPress={() => {
if (productDetail.isOutOfStock || (isFlashDelivery && !productDetail.isFlashAvailable)) { if (productAvailability?.isOutOfStock || (isFlashDelivery && !productAvailability?.isFlashAvailable)) {
return; return;
} }
if (isFlashDelivery) { if (isFlashDelivery) {
@ -319,10 +334,10 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
setAddedToCartProduct({ productId: productDetail.id, product: productDetail }); setAddedToCartProduct({ productId: productDetail.id, product: productDetail });
} }
}} }}
disabled={productDetail.isOutOfStock || (isFlashDelivery && !productDetail.isFlashAvailable)} disabled={productAvailability?.isOutOfStock || (isFlashDelivery && !productAvailability?.isFlashAvailable)}
> >
<MyText style={[tw`font-bold text-base`, { color: (productDetail.isOutOfStock || (isFlashDelivery && !productDetail.isFlashAvailable)) ? '#9ca3af' : theme.colors.brand500 }]}> <MyText style={[tw`font-bold text-base`, { color: (productAvailability?.isOutOfStock || (isFlashDelivery && !productAvailability?.isFlashAvailable)) ? '#9ca3af' : theme.colors.brand500 }]}>
{(productDetail.isOutOfStock || (isFlashDelivery && !productDetail.isFlashAvailable)) ? 'Unavailable' : 'Add to Cart'} {(productAvailability?.isOutOfStock || (isFlashDelivery && !productAvailability?.isFlashAvailable)) ? 'Unavailable' : 'Add to Cart'}
</MyText> </MyText>
</MyTouchableOpacity> </MyTouchableOpacity>
)} )}
@ -330,26 +345,26 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
{isFlashDelivery ? ( {isFlashDelivery ? (
<MyTouchableOpacity <MyTouchableOpacity
style={[tw`flex-1 py-3.5 rounded-xl items-center shadow-md`, { style={[tw`flex-1 py-3.5 rounded-xl items-center shadow-md`, {
backgroundColor: (productDetail.isOutOfStock || !productDetail.isFlashAvailable) ? '#9ca3af' : '#FDF2F8' backgroundColor: (productAvailability?.isOutOfStock || !productAvailability?.isFlashAvailable) ? '#9ca3af' : '#FDF2F8'
}]} }]}
onPress={() => !(productDetail.isOutOfStock || !productDetail.isFlashAvailable) && handleBuyNow(productDetail.id)} onPress={() => !(productAvailability?.isOutOfStock || !productAvailability?.isFlashAvailable) && handleBuyNow(productDetail.id)}
disabled={productDetail.isOutOfStock || !productDetail.isFlashAvailable} disabled={productAvailability?.isOutOfStock || !productAvailability?.isFlashAvailable}
> >
<MyText style={tw`text-base font-bold ${productDetail.isOutOfStock || !productDetail.isFlashAvailable ? 'text-gray-400' : 'text-pink-600'}`}> <MyText style={tw`text-base font-bold ${productAvailability?.isOutOfStock || !productAvailability?.isFlashAvailable ? 'text-gray-400' : 'text-pink-600'}`}>
{productDetail.isOutOfStock ? 'Out of Stock' : {productAvailability?.isOutOfStock ? 'Out of Stock' :
(!productDetail.isFlashAvailable ? 'Not Flash Eligible' : 'Get in 1 Hour')} (!productAvailability?.isFlashAvailable ? 'Not Flash Eligible' : 'Get in 1 Hour')}
</MyText> </MyText>
</MyTouchableOpacity> </MyTouchableOpacity>
) : productDetail.isFlashAvailable ? ( ) : productAvailability?.isFlashAvailable ? (
<MyTouchableOpacity <MyTouchableOpacity
style={[tw`flex-1 py-3.5 rounded-xl items-center shadow-md`, { style={[tw`flex-1 py-3.5 rounded-xl items-center shadow-md`, {
backgroundColor: productDetail.deliverySlots.length === 0 ? '#9ca3af' : '#FDF2F8' backgroundColor: sortedDeliverySlots.length === 0 ? '#9ca3af' : '#FDF2F8'
}]} }]}
onPress={() => productDetail.deliverySlots.length > 0 && handleBuyNow(productDetail.id)} onPress={() => sortedDeliverySlots.length > 0 && handleBuyNow(productDetail.id)}
disabled={productDetail.deliverySlots.length === 0} disabled={sortedDeliverySlots.length === 0}
> >
<MyText style={tw`text-base font-bold ${productDetail.deliverySlots.length === 0 ? 'text-gray-400' : 'text-pink-600'}`}> <MyText style={tw`text-base font-bold ${sortedDeliverySlots.length === 0 ? 'text-gray-400' : 'text-pink-600'}`}>
{productDetail.deliverySlots.length === 0 ? 'No Slots' : 'Get in 1 Hour'} {sortedDeliverySlots.length === 0 ? 'No Slots' : 'Get in 1 Hour'}
</MyText> </MyText>
</MyTouchableOpacity> </MyTouchableOpacity>
) : ( ) : (
@ -378,7 +393,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
key={index} key={index}
style={tw`flex-row items-start mb-4 bg-gray-50 p-3 rounded-xl border border-gray-100`} style={tw`flex-row items-start mb-4 bg-gray-50 p-3 rounded-xl border border-gray-100`}
onPress={() => handleSlotAddToCart(productDetail.id, slot.id)} onPress={() => handleSlotAddToCart(productDetail.id, slot.id)}
disabled={productDetail.isOutOfStock} disabled={productAvailability?.isOutOfStock}
activeOpacity={0.7} activeOpacity={0.7}
> >
<MaterialIcons name="local-shipping" size={20} color="#3B82F6" style={tw`mt-0.5`} /> <MaterialIcons name="local-shipping" size={20} color="#3B82F6" style={tw`mt-0.5`} />
@ -590,7 +605,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
key={index} key={index}
style={tw`flex-row items-start mb-4 bg-gray-50 p-4 rounded-xl border border-gray-100`} style={tw`flex-row items-start mb-4 bg-gray-50 p-4 rounded-xl border border-gray-100`}
onPress={() => handleSlotAddToCart(productDetail.id, slot.id)} onPress={() => handleSlotAddToCart(productDetail.id, slot.id)}
disabled={productDetail.isOutOfStock} disabled={productAvailability?.isOutOfStock}
activeOpacity={0.7} activeOpacity={0.7}
> >
<MaterialIcons name="local-shipping" size={20} color="#3B82F6" style={tw`mt-0.5`} /> <MaterialIcons name="local-shipping" size={20} color="#3B82F6" style={tw`mt-0.5`} />

View file

@ -6,6 +6,7 @@ import { BottomDialog, MyTouchableOpacity, MyText, tw, theme } from 'common-ui';
import { useAuth } from '@/src/contexts/AuthContext'; import { useAuth } from '@/src/contexts/AuthContext';
import { trpc } from '@/src/trpc-client'; import { trpc } from '@/src/trpc-client';
import { useAddressStore } from '@/src/store/addressStore'; import { useAddressStore } from '@/src/store/addressStore';
import { useSlots } from '@/src/hooks/prominent-api-hooks';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
interface QuickDeliveryAddressSelectorProps { interface QuickDeliveryAddressSelectorProps {
@ -31,13 +32,13 @@ const QuickDeliveryAddressSelector: React.FC<QuickDeliveryAddressSelectorProps>
const { data: addressesData } = trpc.user.address.getUserAddresses.useQuery(undefined, { const { data: addressesData } = trpc.user.address.getUserAddresses.useQuery(undefined, {
enabled: isAuthenticated, enabled: isAuthenticated,
}); });
const { data: slotsData } = trpc.user.slots.getSlotsWithProducts.useQuery(); const { data: slotsData } = useSlots();
const defaultAddress = defaultAddressData?.data; const defaultAddress = defaultAddressData?.data;
const addresses = addressesData?.data || []; const addresses = addressesData?.data || [];
// Format time range helper // Format time range helper
const formatTimeRange = (deliveryTime: string) => { const formatTimeRange = (deliveryTime: string | Date) => {
const time = dayjs(deliveryTime); const time = dayjs(deliveryTime);
const endTime = time.add(1, 'hour'); const endTime = time.add(1, 'hour');
const startPeriod = time.format('A'); const startPeriod = time.format('A');

View file

@ -7,7 +7,11 @@ import { useRouter, usePathname } from 'expo-router';
import MaterialIcons from '@expo/vector-icons/MaterialIcons'; import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { tw, theme, MyText, MyTouchableOpacity, MyFlatList, AppContainer, MiniQuantifier } from 'common-ui'; import { tw, theme, MyText, MyTouchableOpacity, MyFlatList, AppContainer, MiniQuantifier } from 'common-ui';
import { trpc } from '@/src/trpc-client'; import { trpc } from '@/src/trpc-client';
import { useAllProducts, useStores, useSlots } from '@/src/hooks/prominent-api-hooks';
import { AllProductsApiType } from '@backend/trpc/router';
import { useQuickDeliveryStore } from '@/src/store/quickDeliveryStore'; import { useQuickDeliveryStore } from '@/src/store/quickDeliveryStore';
import { useCentralSlotStore } from '@/src/store/centralSlotStore';
import { useCentralProductStore } from '@/src/store/centralProductStore';
import { useAddToCart, useGetCart, useUpdateCartItem, useRemoveFromCart } from '@/hooks/cart-query-hooks'; import { useAddToCart, useGetCart, useUpdateCartItem, useRemoveFromCart } from '@/hooks/cart-query-hooks';
import { useHideTabNav } from '@/src/hooks/useHideTabNav'; import { useHideTabNav } from '@/src/hooks/useHideTabNav';
import CartIcon from '@/components/icons/CartIcon'; import CartIcon from '@/components/icons/CartIcon';
@ -32,7 +36,7 @@ interface SlotLayoutProps {
function CustomDrawerContent(baseUrl: string, drawerProps: DrawerContentComponentProps, slotIdParent?: number, storeIdParent?: number) { function CustomDrawerContent(baseUrl: string, drawerProps: DrawerContentComponentProps, slotIdParent?: number, storeIdParent?: number) {
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const { data: storesData } = trpc.user.stores.getStores.useQuery(); const { data: storesData } = useStores();
const setStoreId = useSlotStore(state => state.setStoreId); const setStoreId = useSlotStore(state => state.setStoreId);
const { slotId, storeId } = useSlotStore(); const { slotId, storeId } = useSlotStore();
@ -179,17 +183,10 @@ export function SlotLayout({ slotId, storeId, baseUrl, isForFlashDelivery }: Slo
router.replace(`${baseUrl}?slotId=${newSlotId}` as any); router.replace(`${baseUrl}?slotId=${newSlotId}` as any);
}; };
const slotQuery = slotId
? trpc.user.slots.getSlotById.useQuery({ slotId: Number(slotId) })
: trpc.user.slots.nextMajorDelivery.useQuery();
const deliveryTime = dayjs(slotQuery.data?.deliveryTime).format('DD MMM hh:mm A');
return ( return (
<> <>
<View style={tw` w-full flex-row bg-white px-4 py-2 mb-1`}> <View style={tw` w-full flex-row bg-white px-4 py-2 mb-1`}>
<QuickDeliveryAddressSelector <QuickDeliveryAddressSelector
deliveryTime={deliveryTime}
slotId={Number(slotId)} slotId={Number(slotId)}
onSlotChange={handleSlotChange} onSlotChange={handleSlotChange}
isForFlashDelivery={isForFlashDelivery} isForFlashDelivery={isForFlashDelivery}
@ -243,6 +240,7 @@ const CompactProductCard = ({
// Cart management for miniView // Cart management for miniView
const { data: cartData } = useGetCart({}, cartType); const { data: cartData } = useGetCart({}, cartType);
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
const updateCartItem = useUpdateCartItem({ const updateCartItem = useUpdateCartItem({
showSuccessAlert: false, showSuccessAlert: false,
showErrorAlert: false, showErrorAlert: false,
@ -256,6 +254,7 @@ const CompactProductCard = ({
const cartItem = cartData?.items?.find((cartItem: any) => cartItem.productId === item.id); const cartItem = cartData?.items?.find((cartItem: any) => cartItem.productId === item.id);
const quantity = cartItem?.quantity || 0; const quantity = cartItem?.quantity || 0;
const isOutOfStock = productSlotsMap[item.id]?.isOutOfStock;
const handleQuantityChange = (newQuantity: number) => { const handleQuantityChange = (newQuantity: number) => {
if (newQuantity === 0 && cartItem) { if (newQuantity === 0 && cartItem) {
@ -281,7 +280,7 @@ const CompactProductCard = ({
source={{ uri: item.images?.[0] }} source={{ uri: item.images?.[0] }}
style={{ width: "100%", height: itemWidth, resizeMode: "cover" }} style={{ width: "100%", height: itemWidth, resizeMode: "cover" }}
/> />
{item.isOutOfStock && ( {isOutOfStock && (
<View style={tw`absolute inset-0 bg-black/30 items-center justify-center`}> <View style={tw`absolute inset-0 bg-black/30 items-center justify-center`}>
<MyText style={tw`text-white text-xs font-bold`}>Out of Stock</MyText> <MyText style={tw`text-white text-xs font-bold`}>Out of Stock</MyText>
</View> </View>
@ -340,22 +339,20 @@ export function SlotProducts({ slotId:slotIdParent, storeId:storeIdParent, baseU
const slotId = slotIdParent; const slotId = slotIdParent;
const storeId = storeIdParent; const storeId = storeIdParent;
const storeIdNum = storeId; const storeIdNum = storeId;
// const { storeId, slotId: slotIdRaw } = useLocalSearchParams();
// const slotId = Number(slotIdRaw);
const { data: slotsData, isLoading: slotsLoading, error: slotsError } = useSlots();
const { productsById } = useCentralProductStore();
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
// const storeIdNum = storeId ? Number(storeId) : undefined; // Find the specific slot from cached data
const slot = slotsData?.slots?.find(s => s.id === slotId);
const slotQuery = trpc.user.slots.getSlotById.useQuery({ slotId: slotId! }, { enabled: !!slotId });
const productsQuery = trpc.common.product.getAllProductsSummary.useQuery({});
const { addToCart = () => { } } = useAddToCart({ showSuccessAlert: false, showErrorAlert: false, refetchCart: true }, "regular") || {}; const { addToCart = () => { } } = useAddToCart({ showSuccessAlert: false, showErrorAlert: false, refetchCart: true }, "regular") || {};
const handleAddToCart = (productId: number) => { const handleAddToCart = (productId: number) => {
setIsLoadingDialogOpen(true); setIsLoadingDialogOpen(true);
const item = filteredProducts.find((p: any) => p.id === productId); const item = filteredProducts.find((p) => p.id === productId);
const deliveryTime = slotQuery.data?.deliveryTime ? dayjs(slotQuery.data.deliveryTime).format('ddd, DD MMM • h:mm A') : ''; const deliveryTime = slot?.deliveryTime ? dayjs(slot.deliveryTime).format('ddd, DD MMM • h:mm A') : '';
addToCart(productId, 1, slotId || 0, () => { addToCart(productId, 1, slotId || 0, () => {
setIsLoadingDialogOpen(false); setIsLoadingDialogOpen(false);
if (item) { if (item) {
@ -364,7 +361,7 @@ export function SlotProducts({ slotId:slotIdParent, storeId:storeIdParent, baseU
}); });
}; };
if (slotQuery.isLoading || (storeIdNum && productsQuery?.isLoading)) { if (slotsLoading) {
return ( return (
<AppContainer> <AppContainer>
<View style={tw`flex-1 justify-center items-center bg-gray-50`}> <View style={tw`flex-1 justify-center items-center bg-gray-50`}>
@ -374,7 +371,7 @@ export function SlotProducts({ slotId:slotIdParent, storeId:storeIdParent, baseU
); );
} }
if (slotQuery.error || (storeIdNum && productsQuery?.error)) { if (slotsError) {
return ( return (
<AppContainer> <AppContainer>
<View style={tw`flex-1 justify-center items-center bg-gray-50`}> <View style={tw`flex-1 justify-center items-center bg-gray-50`}>
@ -386,7 +383,7 @@ export function SlotProducts({ slotId:slotIdParent, storeId:storeIdParent, baseU
); );
} }
if (!slotQuery.data) { if (!slot) {
return ( return (
<AppContainer> <AppContainer>
<View style={tw`flex-1 justify-center items-center`}> <View style={tw`flex-1 justify-center items-center`}>
@ -397,14 +394,16 @@ export function SlotProducts({ slotId:slotIdParent, storeId:storeIdParent, baseU
); );
} }
// Create a Set of product IDs from slot data for O(1) lookup // Get product details from central store using slot product IDs
const slotProductIds = new Set(slotQuery.data.products?.map((p: any) => p.id) || []); // Filter: 1) Must exist in productsById, 2) Must not be out of stock (from slots data)
const slotProducts = slot.products
?.map(p => productsById[p.id])
?.filter((product): product is NonNullable<typeof product> => product !== null && product !== undefined)
?.filter(product => !productSlotsMap[product.id]?.isOutOfStock) || [];
const filteredProducts: any[] = storeIdNum const filteredProducts = storeIdNum
? productsQuery?.data?.products?.filter(p => ? slotProducts.filter(p => p.storeId === storeIdNum)
p.storeId === storeIdNum && slotProductIds.has(p.id) : slotProducts;
) || []
: slotQuery.data.products;
return ( return (
<View testID="slot-detail-page" style={tw`flex-1`}> <View testID="slot-detail-page" style={tw`flex-1`}>
@ -422,7 +421,7 @@ export function SlotProducts({ slotId:slotIdParent, storeId:storeIdParent, baseU
keyExtractor={(item, index) => index.toString()} keyExtractor={(item, index) => index.toString()}
columnWrapperStyle={{ gap: 16, justifyContent: 'flex-start' }} columnWrapperStyle={{ gap: 16, justifyContent: 'flex-start' }}
contentContainerStyle={[tw`pb-24 px-4`, { gap: 16 }]} contentContainerStyle={[tw`pb-24 px-4`, { gap: 16 }]}
onRefresh={() => slotQuery.refetch()} onRefresh={() => {}}
ListEmptyComponent={ ListEmptyComponent={
storeIdNum ? ( storeIdNum ? (
<View style={tw`items-center justify-center py-10`}> <View style={tw`items-center justify-center py-10`}>
@ -448,7 +447,8 @@ export function FlashDeliveryProducts({ storeId:storeIdParent, baseUrl, onProduc
const storeId = storeIdParent; const storeId = storeIdParent;
const storeIdNum = storeId; const storeIdNum = storeId;
const productsQuery = trpc.common.product.getAllProductsSummary.useQuery({}); const productsQuery = useAllProducts();
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
const { addToCart = () => { } } = useAddToCart({ showSuccessAlert: false, showErrorAlert: false, refetchCart: true }, "flash") || {}; const { addToCart = () => { } } = useAddToCart({ showSuccessAlert: false, showErrorAlert: false, refetchCart: true }, "flash") || {};
@ -486,20 +486,22 @@ export function FlashDeliveryProducts({ storeId:storeIdParent, baseUrl, onProduc
} }
// Filter products to only include those eligible for flash delivery // Filter products to only include those eligible for flash delivery
let flashProducts: any[] = []; let flashProducts: AllProductsApiType['products'][number][] = [];
if (storeIdNum) { if (storeIdNum) {
// Filter by store, flash availability, and stock status // Filter by store, flash availability, and stock status
flashProducts = productsQuery?.data?.products?.filter(p => flashProducts = productsQuery?.data?.products?.filter(p => {
p.storeId === storeIdNum && const productInfo = productSlotsMap[p.id];
p.isFlashAvailable && return p.storeId === storeIdNum &&
!p.isOutOfStock productInfo?.isFlashAvailable &&
) || []; !productInfo?.isOutOfStock;
}) || [];
} else { } else {
// Show all flash-available products that are in stock // Show all flash-available products that are in stock
flashProducts = productsQuery?.data?.products?.filter(p => flashProducts = productsQuery?.data?.products?.filter(p => {
p.isFlashAvailable && const productInfo = productSlotsMap[p.id];
!p.isOutOfStock return productInfo?.isFlashAvailable &&
) || []; !productInfo?.isOutOfStock;
}) || [];
} }
return ( return (

View file

@ -2,7 +2,7 @@ import React, { useState } from 'react';
import { View, ActivityIndicator } from 'react-native'; import { View, ActivityIndicator } from 'react-native';
import { WebView } from 'react-native-webview'; import { WebView } from 'react-native-webview';
import { trpc } from '@/src/trpc-client'; import { trpc } from '@/src/trpc-client';
import { useGetEssentialConsts } from '@/src/api-hooks/essential-consts.api'; import { useGetEssentialConsts } from '@/src/hooks/prominent-api-hooks';
import { theme, MyText, MyTouchableOpacity } from 'common-ui'; import { theme, MyText, MyTouchableOpacity } from 'common-ui';
import MaterialIcons from '@expo/vector-icons/MaterialIcons'; import MaterialIcons from '@expo/vector-icons/MaterialIcons';

View file

@ -24,8 +24,10 @@ import TestingPhaseNote from "@/components/TestingPhaseNote";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { trpc } from "@/src/trpc-client"; import { trpc } from "@/src/trpc-client";
import { useCentralProductStore } from '@/src/store/centralProductStore';
import { useCentralSlotStore } from '@/src/store/centralSlotStore';
import { useGetCart, useUpdateCartItem, useRemoveFromCart } from '@/hooks/cart-query-hooks'; import { useGetCart, useUpdateCartItem, useRemoveFromCart } from '@/hooks/cart-query-hooks';
import { useGetEssentialConsts } from '@/src/api-hooks/essential-consts.api'; import { useGetEssentialConsts } from '@/src/hooks/prominent-api-hooks';
interface CartPageProps { interface CartPageProps {
isFlashDelivery?: boolean; isFlashDelivery?: boolean;
@ -80,33 +82,34 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
const { data: couponsRaw, error: couponsError } = trpc.user.coupon.getEligible.useQuery(); const { data: couponsRaw, error: couponsError } = trpc.user.coupon.getEligible.useQuery();
const { data: constsData } = useGetEssentialConsts(); const { data: constsData } = useGetEssentialConsts();
const { data: productsData } = trpc.common.product.getAllProductsSummary.useQuery({}); const products = useCentralProductStore((state) => state.products);
const productsById = useCentralProductStore((state) => state.productsById);
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
const cartItems = cartData?.items || []; const cartItems = cartData?.items || [];
// Memoized flash-eligible product IDs // Memoized flash-eligible product IDs
const flashEligibleProductIds = useMemo(() => { const flashEligibleProductIds = useMemo(() => {
if (!productsData?.products) return new Set<number>(); if (!products.length) return new Set<number>();
return new Set( return new Set(
productsData.products products
.filter((product: any) => product.isFlashAvailable) .filter((product) => productSlotsMap[product.id]?.isFlashAvailable)
.map((product: any) => product.id) .map((product) => product.id)
); );
}, [productsData]); }, [products, productSlotsMap]);
// Base total price without discounts for coupon eligibility check // Base total price without discounts for coupon eligibility check
const baseTotalPrice = useMemo( const baseTotalPrice = useMemo(
() => () =>
cartItems cartItems
.filter((item) => !item.product?.isOutOfStock) .filter((item) => !productSlotsMap[item.productId]?.isOutOfStock)
.reduce( .reduce((sum, item) => {
(sum, item) => const product = productsById[item.productId];
sum + const price = product?.price || 0;
(item.product?.price || 0) * (quantities[item.id] || item.quantity), return sum + price * (quantities[item.id] || item.quantity);
0 }, 0),
), [cartItems, quantities, productsById]
[cartItems, quantities]
); );
const eligibleCoupons = useMemo(() => { const eligibleCoupons = useMemo(() => {
@ -199,13 +202,14 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
[eligibleCoupons, selectedCouponId] [eligibleCoupons, selectedCouponId]
); );
const totalPrice = cartItems const totalPrice = cartItems
.filter((item) => !item.product?.isOutOfStock) .filter((item) => !productSlotsMap[item.productId]?.isOutOfStock)
.reduce((sum, item) => { .reduce((sum, item) => {
const quantity = quantities[item.id] || item.quantity; const product = productsById[item.productId];
const price = isFlashDelivery ? (item.product?.flashPrice ?? item.product?.price ?? 0) : (item.product?.price || 0); const quantity = quantities[item.id] || item.quantity;
return sum + price * quantity; const price = isFlashDelivery ? (product?.flashPrice ?? product?.price ?? 0) : (product?.price || 0);
}, 0); return sum + price * quantity;
}, 0);
const dropdownData = useMemo( const dropdownData = useMemo(
() => () =>
eligibleCoupons?.map((coupon) => { eligibleCoupons?.map((coupon) => {
@ -273,7 +277,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
const finalTotalWithDelivery = finalTotal + deliveryCharge; const finalTotalWithDelivery = finalTotal + deliveryCharge;
const hasAvailableItems = cartItems.some(item => !item.product?.isOutOfStock); const hasAvailableItems = cartItems.some(item => !productSlotsMap[item.productId]?.isOutOfStock);
useEffect(() => { useEffect(() => {
const initial: Record<number, number> = {}; const initial: Record<number, number> = {};
@ -410,10 +414,12 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
const productSlots = getAvailableSlotsForProduct(item.productId); const productSlots = getAvailableSlotsForProduct(item.productId);
const selectedSlotForItem = selectedSlots[item.id]; const selectedSlotForItem = selectedSlots[item.id];
const isFlashEligible = isFlashDelivery ? flashEligibleProductIds.has(item.productId) : true; const isFlashEligible = isFlashDelivery ? flashEligibleProductIds.has(item.productId) : true;
const product = productsById[item.productId];
const productSlotInfo = productSlotsMap[item.productId];
// const isAvailable = (productSlots.length > 0 || isFlashDelivery) && !item.product?.isOutOfStock && isFlashEligible; // const isAvailable = (productSlots.length > 0 || isFlashDelivery) && !item.product?.isOutOfStock && isFlashEligible;
let isAvailable = true; let isAvailable = true;
if(item.product?.isOutOfStock) { if (productSlotInfo?.isOutOfStock) {
isAvailable = false; isAvailable = false;
} else if(isFlashDelivery) { } else if(isFlashDelivery) {
if(!isFlashEligible) { if(!isFlashEligible) {
@ -430,7 +436,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
// isAvailable = isFlashEligible; // isAvailable = isFlashEligible;
// } // }
const quantity = quantities[item.id] || item.quantity; const quantity = quantities[item.id] || item.quantity;
const price = isFlashDelivery ? (item.product?.flashPrice ?? item.product?.price ?? 0) : (item.product?.price || 0); const price = isFlashDelivery ? (product?.flashPrice ?? product?.price ?? 0) : (product?.price || 0);
const itemPrice = price * quantity; const itemPrice = price * quantity;
return ( return (
@ -438,7 +444,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
<View style={tw`p-4`}> <View style={tw`p-4`}>
<View style={tw`flex-row items-center mb-2`}> <View style={tw`flex-row items-center mb-2`}>
<Image <Image
source={{ uri: item.product.images?.[0] }} source={{ uri: product?.images?.[0] }}
style={tw`w-8 h-8 rounded-lg bg-gray-100 mr-3`} style={tw`w-8 h-8 rounded-lg bg-gray-100 mr-3`}
/> />
@ -446,12 +452,12 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
style={tw`text-sm text-gray-900 flex-1 mr-3`} style={tw`text-sm text-gray-900 flex-1 mr-3`}
numberOfLines={2} numberOfLines={2}
> >
{item.product.name} {product?.name}
</MyText> </MyText>
<MyText style={tw`text-xs text-gray-500 mr-2`}> <MyText style={tw`text-xs text-gray-500 mr-2`}>
{(() => { {(() => {
const qty = item.product?.productQuantity || 1; const qty = product?.productQuantity || 1;
const unit = item.product?.unitNotation || ''; const unit = product?.unitNotation || '';
if (unit?.toLowerCase() === 'kg' && qty < 1) { if (unit?.toLowerCase() === 'kg' && qty < 1) {
return `${Math.round(qty * 1000)}g`; return `${Math.round(qty * 1000)}g`;
} }
@ -512,8 +518,8 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
}); });
} }
}} }}
step={item.product.incrementStep} step={product?.incrementStep}
unit={item.product?.unitNotation} unit={product?.unitNotation}
/> />
</View> </View>
</View> </View>
@ -579,7 +585,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
onPress={() => { onPress={() => {
Alert.alert( Alert.alert(
"Remove Item", "Remove Item",
`Remove ${item.product.name} from cart?`, `Remove ${product?.name} from cart?`,
[ [
{ text: "Cancel", style: "cancel" }, { text: "Cancel", style: "cancel" },
{ {
@ -630,7 +636,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
onPress={() => { onPress={() => {
Alert.alert( Alert.alert(
"Remove Item", "Remove Item",
`Remove ${item.product.name} from cart?`, `Remove ${product?.name} from cart?`,
[ [
{ text: "Cancel", style: "cancel" }, { text: "Cancel", style: "cancel" },
{ {
@ -670,12 +676,12 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
)} )}
{!isAvailable && ( {!isAvailable && (
<View <View
style={tw`bg-red-50 self-start px-2 py-1 rounded-md mt-2`} style={tw`bg-red-50 self-start px-2 py-1 rounded-md mt-2`}
> >
<MyText style={tw`text-xs font-bold text-red-600`}> <MyText style={tw`text-xs font-bold text-red-600`}>
{item.product?.isOutOfStock {productSlotInfo?.isOutOfStock
? "Out of Stock" ? "Out of Stock"
: isFlashDelivery && !flashEligibleProductIds.has(item.productId) : isFlashDelivery && !flashEligibleProductIds.has(item.productId)
? "Not available for flash delivery. Please remove" ? "Not available for flash delivery. Please remove"
: "No delivery slots available"} : "No delivery slots available"}
@ -908,7 +914,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
onPress={() => { onPress={() => {
const availableItems = cartItems const availableItems = cartItems
.filter(item => { .filter(item => {
if (item.product?.isOutOfStock) return false; if (productSlotsMap[item.productId]?.isOutOfStock) return false;
if (isFlashDelivery) { if (isFlashDelivery) {
// Check if product supports flash delivery // Check if product supports flash delivery
return flashEligibleProductIds.has(item.productId); return flashEligibleProductIds.has(item.productId);
@ -917,12 +923,10 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
}) })
.map(item => item.id); .map(item => item.id);
if (availableItems.length === 0) {
// Determine why no items are available
if (availableItems.length === 0) { const outOfStockItems = cartItems.filter(item => productSlotsMap[item.productId]?.isOutOfStock);
// Determine why no items are available const inStockItems = cartItems.filter(item => !productSlotsMap[item.productId]?.isOutOfStock);
const outOfStockItems = cartItems.filter(item => item.product?.isOutOfStock);
const inStockItems = cartItems.filter(item => !item.product?.isOutOfStock);
let errorTitle = "Cannot Proceed"; let errorTitle = "Cannot Proceed";
let errorMessage = ""; let errorMessage = "";
@ -959,9 +963,9 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
return; return;
} }
// Check if there are items without slots (for regular delivery) // Check if there are items without slots (for regular delivery)
if (!isFlashDelivery && availableItems.length < cartItems.length) { if (!isFlashDelivery && availableItems.length < cartItems.length) {
const itemsWithoutSlots = cartItems.filter(item => !selectedSlots[item.id] && !item.product?.isOutOfStock); const itemsWithoutSlots = cartItems.filter(item => !selectedSlots[item.id] && !productSlotsMap[item.productId]?.isOutOfStock);
if (itemsWithoutSlots.length > 0) { if (itemsWithoutSlots.length > 0) {
Alert.alert( Alert.alert(
"Delivery Slot Required", "Delivery Slot Required",

View file

@ -8,8 +8,10 @@ import AddressForm from '@/src/components/AddressForm';
import { useAuthenticatedRoute } from '@/hooks/useAuthenticatedRoute'; import { useAuthenticatedRoute } from '@/hooks/useAuthenticatedRoute';
import { trpc } from '@/src/trpc-client'; import { trpc } from '@/src/trpc-client';
import { useCentralProductStore } from '@/src/store/centralProductStore';
import { useCentralSlotStore } from '@/src/store/centralSlotStore';
import { useGetCart } from '@/hooks/cart-query-hooks'; import { useGetCart } from '@/hooks/cart-query-hooks';
import { useGetEssentialConsts } from '@/src/api-hooks/essential-consts.api'; import { useGetEssentialConsts } from '@/src/hooks/prominent-api-hooks';
import PaymentAndOrderComponent from '@/components/PaymentAndOrderComponent'; import PaymentAndOrderComponent from '@/components/PaymentAndOrderComponent';
import CheckoutAddressSelector from '@/components/CheckoutAddressSelector'; import CheckoutAddressSelector from '@/components/CheckoutAddressSelector';
import { useAddressStore } from '@/src/store/addressStore'; import { useAddressStore } from '@/src/store/addressStore';
@ -35,7 +37,9 @@ const CheckoutPage: React.FC<CheckoutPageProps> = ({ isFlashDelivery = false })
const { data: addresses, refetch: refetchAddresses } = trpc.user.address.getUserAddresses.useQuery(); const { data: addresses, refetch: refetchAddresses } = trpc.user.address.getUserAddresses.useQuery();
const { data: slotsData, refetch: refetchSlots } = trpc.user.slots.getSlots.useQuery(); const { data: slotsData, refetch: refetchSlots } = trpc.user.slots.getSlots.useQuery();
const { data: constsData } = useGetEssentialConsts(); const { data: constsData } = useGetEssentialConsts();
const { data: productsData } = trpc.common.product.getAllProductsSummary.useQuery({}); const products = useCentralProductStore((state) => state.products);
const productsById = useCentralProductStore((state) => state.productsById);
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
useMarkDataFetchers(() => { useMarkDataFetchers(() => {
refetchCart(); refetchCart();
@ -51,15 +55,15 @@ const CheckoutPage: React.FC<CheckoutPageProps> = ({ isFlashDelivery = false })
const cartItems = cartData?.items || []; const cartItems = cartData?.items || [];
// Memoized flash-eligible product IDs // Memoized flash-eligible product IDs
const flashEligibleProductIds = useMemo(() => { const flashEligibleProductIds = useMemo(() => {
if (!productsData?.products) return new Set<number>(); if (!products.length) return new Set<number>();
return new Set( return new Set(
productsData.products products
.filter((product: any) => product.isFlashAvailable) .filter((product) => productSlotsMap[product.id]?.isFlashAvailable)
.map((product: any) => product.id) .map((product) => product.id)
); );
}, [productsData]); }, [products, productSlotsMap]);
// Parse slots parameter from URL (format: "1:1,2,3;2:4,5") // Parse slots parameter from URL (format: "1:1,2,3;2:4,5")
const selectedSlots = useMemo(() => { const selectedSlots = useMemo(() => {
@ -123,10 +127,11 @@ const CheckoutPage: React.FC<CheckoutPageProps> = ({ isFlashDelivery = false })
const totalPrice = selectedItems const totalPrice = selectedItems
.filter((item) => !item.product?.isOutOfStock) .filter((item) => !productSlotsMap[item.productId]?.isOutOfStock)
.reduce( .reduce(
(sum, item) => { (sum, item) => {
const price = isFlashDelivery ? (item.product?.flashPrice ?? item.product?.price ?? 0) : (item.product?.price || 0); const product = productsById[item.productId];
const price = isFlashDelivery ? (product?.flashPrice ?? product?.price ?? 0) : (product?.price || 0);
return sum + price * item.quantity; return sum + price * item.quantity;
}, },
0 0

View file

@ -14,7 +14,6 @@ import {
theme, theme,
updateStatusBarColor, updateStatusBarColor,
} from "common-ui"; } from "common-ui";
import { trpc } from "@/src/trpc-client";
import { import {
useGetCart, useGetCart,
useUpdateCartItem, useUpdateCartItem,
@ -22,8 +21,9 @@ import {
useAddToCart, useAddToCart,
type CartType, type CartType,
} from "@/hooks/cart-query-hooks"; } from "@/hooks/cart-query-hooks";
import { useGetEssentialConsts } from "@/src/api-hooks/essential-consts.api"; import { useGetEssentialConsts, useSlots } from "@/src/hooks/prominent-api-hooks"
import { useProductSlotIdentifier } from "@/hooks/useProductSlotIdentifier"; import { useProductSlotIdentifier } from "@/hooks/useProductSlotIdentifier";
import { useCentralProductStore } from "@/src/store/centralProductStore";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { LinearGradient } from "expo-linear-gradient"; import { LinearGradient } from "expo-linear-gradient";
@ -36,7 +36,7 @@ interface FloatingCartBarProps {
} }
// Smart time window formatting function // Smart time window formatting function
const formatTimeRange = (deliveryTime: string) => { const formatTimeRange = (deliveryTime: string | Date) => {
const time = dayjs(deliveryTime); const time = dayjs(deliveryTime);
const endTime = time.add(1, 'hour'); const endTime = time.add(1, 'hour');
const startPeriod = time.format('A'); const startPeriod = time.format('A');
@ -79,7 +79,8 @@ const FloatingCartBar: React.FC<FloatingCartBarProps> = ({
const setIsExpanded = controlledSetIsExpanded ?? setLocalIsExpanded; const setIsExpanded = controlledSetIsExpanded ?? setLocalIsExpanded;
const { data: cartData, refetch: refetchCart } = useGetCart({}, cartType); const { data: cartData, refetch: refetchCart } = useGetCart({}, cartType);
const { data: constsData } = useGetEssentialConsts(); const { data: constsData } = useGetEssentialConsts();
const { data: slotsData } = trpc.user.slots.getSlotsWithProducts.useQuery(); const { data: slotsData } = useSlots();
const productsById = useCentralProductStore((state) => state.productsById);
const { productSlotsMap } = useProductSlotIdentifier(); const { productSlotsMap } = useProductSlotIdentifier();
const cartItems = cartData?.items || []; const cartItems = cartData?.items || [];
const itemCount = cartItems.length; const itemCount = cartItems.length;
@ -108,21 +109,21 @@ const FloatingCartBar: React.FC<FloatingCartBarProps> = ({
setQuantities(initial); setQuantities(initial);
}, [cartData]); }, [cartData]);
useEffect(() => { useEffect(() => {
if (!cartItems.length || !slotsData?.slots || !productSlotsMap) return; if (!cartItems.length || !slotsData?.slots || !productSlotsMap) return;
const itemsToUpdate = cartItems.filter(item => { const itemsToUpdate = cartItems.filter(item => {
if (isFlashDelivery || !item.slotId) return false; if (isFlashDelivery || !item.slotId) return false;
const availableSlots = productSlotsMap.get(item.productId) || []; const availableSlots = productSlotsMap[item.productId]?.slots || [];
const isSlotAvailable = availableSlots.includes(item.slotId); const isSlotAvailable = availableSlots.some((slot) => slot.id === item.slotId);
return !isSlotAvailable; return !isSlotAvailable;
}); });
itemsToUpdate.forEach((item) => { itemsToUpdate.forEach((item) => {
const availableSlots = productSlotsMap.get(item.productId) || []; const availableSlots = productSlotsMap[item.productId]?.slots || [];
if (availableSlots.length > 0 && !isFlashDelivery) { if (availableSlots.length > 0 && !isFlashDelivery) {
const nearestSlotId = availableSlots[0]; const nearestSlotId = availableSlots[0].id;
removeFromCart.mutate({ itemId: item.id }); removeFromCart.mutate({ itemId: item.id });
addToCartHook.addToCart(item.productId, item.quantity, nearestSlotId); addToCartHook.addToCart(item.productId, item.quantity, nearestSlotId);
} }
@ -135,7 +136,9 @@ useEffect(() => {
// Calculate total cart value and free delivery info // Calculate total cart value and free delivery info
const totalCartValue = cartItems.reduce( const totalCartValue = cartItems.reduce(
(sum, item) => { (sum, item) => {
const price = isFlashDelivery ? (item.product.flashPrice ?? item.product.price) : item.product.price; const product = productsById[item.productId];
const basePrice = product?.price ?? 0;
const price = isFlashDelivery ? (product?.flashPrice ?? basePrice) : basePrice;
return sum + price * item.quantity; return sum + price * item.quantity;
}, },
0 0
@ -256,21 +259,21 @@ useEffect(() => {
<React.Fragment key={item.id}> <React.Fragment key={item.id}>
<View style={tw`py-4`}> <View style={tw`py-4`}>
<View style={tw`flex-row items-center`}> <View style={tw`flex-row items-center`}>
<Image <Image
source={{ uri: item.product.images?.[0] }} source={{ uri: productsById[item.productId]?.images?.[0] }}
style={tw`w-8 h-8 rounded-lg bg-slate-50 border border-slate-100`} style={tw`w-8 h-8 rounded-lg bg-slate-50 border border-slate-100`}
/> />
<View style={tw`flex-1 ml-4`}> <View style={tw`flex-1 ml-4`}>
<View style={tw`flex-row items-center justify-between mb-1`}> <View style={tw`flex-row items-center justify-between mb-1`}>
<ProductNameWithQuantity <ProductNameWithQuantity
name={item.product.name} name={productsById[item.productId]?.name || ''}
productQuantity={item.product.productQuantity} productQuantity={productsById[item.productId]?.productQuantity || 0}
unitNotation={item.product.unitNotation} unitNotation={productsById[item.productId]?.unitNotation || ''}
/> />
<MiniQuantifier <MiniQuantifier
value={quantities[item.id] || item.quantity} value={quantities[item.id] || item.quantity}
onChange={(value) => { onChange={(value) => {
if (value === 0) { if (value === 0) {
removeFromCart.mutate({ itemId: item.id }); removeFromCart.mutate({ itemId: item.id });
} else { } else {
@ -278,21 +281,20 @@ useEffect(() => {
updateCartItem.mutate({ itemId: item.id, quantity: value }); updateCartItem.mutate({ itemId: item.id, quantity: value });
} }
}} }}
step={item.product.incrementStep} step={productsById[item.productId]?.incrementStep || 1}
showUnits={true} showUnits={true}
unit={item.product?.unitNotation} unit={productsById[item.productId]?.unitNotation}
/> />
</View> </View>
<View style={tw`flex-row items-center justify-between`}> <View style={tw`flex-row items-center justify-between`}>
{item.slotId && slotsData && productSlotsMap.has(item.productId) && ( {item.slotId && slotsData && productSlotsMap[item.productId] && (
<BottomDropdown <BottomDropdown
label="Select Delivery Slot" label="Select Delivery Slot"
value={item.slotId} value={item.slotId}
options={(productSlotsMap.get(item.productId) || []).map(slotId => { options={(productSlotsMap[item.productId]?.slots || []).map((slot) => {
const slot = slotsData.slots.find(s => s.id === slotId);
return { return {
label: slot ? formatTimeRange(slot.deliveryTime) : "N/A", label: slot ? formatTimeRange(slot.deliveryTime) : "N/A",
value: slotId, value: slot.id,
}; };
})} })}
onValueChange={async (val) => { onValueChange={async (val) => {
@ -325,7 +327,12 @@ useEffect(() => {
/> />
)} )}
<MyText style={tw`text-slate-900 text-sm font-bold`}> <MyText style={tw`text-slate-900 text-sm font-bold`}>
{(isFlashDelivery ? (item.product.flashPrice ?? item.product.price) : item.product.price) * item.quantity} {(() => {
const product = productsById[item.productId];
const basePrice = product?.price ?? 0;
const price = isFlashDelivery ? (product?.flashPrice ?? basePrice) : basePrice;
return price * item.quantity;
})()}
</MyText> </MyText>
</View> </View>
</View> </View>

View file

@ -1,15 +1,12 @@
import { trpc } from '@/src/trpc-client'; import { useAllProducts } from '@/src/hooks/prominent-api-hooks';
import { useCentralSlotStore } from '@/src/store/centralSlotStore';
import { Alert } from 'react-native'; import { Alert } from 'react-native';
import { useState, useEffect } from 'react'; import { useQuery, useMutation, useQueryClient, UseQueryResult, UseMutationResult } from '@tanstack/react-query';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { StorageServiceCasual } from 'common-ui/src/services/StorageServiceCasual'; import { StorageServiceCasual } from 'common-ui/src/services/StorageServiceCasual';
// Cart type definition // Cart type definition
export type CartType = "regular" | "flash"; export type CartType = "regular" | "flash";
// const CART_MODE: 'remote' | 'local' = 'remote';
const CART_MODE: 'remote' | 'local' = 'local';
const getCartStorageKey = (cartType: CartType = "regular"): string => { const getCartStorageKey = (cartType: CartType = "regular"): string => {
return cartType === "flash" ? "flash_cart_items" : "cart_items"; return cartType === "flash" ? "flash_cart_items" : "cart_items";
}; };
@ -26,15 +23,99 @@ interface ProductSummary {
id: number; id: number;
price: string; price: string;
incrementStep: number; incrementStep: number;
isOutOfStock: boolean;
isFlashAvailable: boolean;
name?: string;
flashPrice?: string | null;
images?: string[];
productQuantity?: number;
unitNotation?: string;
marketPrice?: string | null;
} }
interface CartItem { export interface CartItem {
id: number; id: number;
productId: number; productId: number;
quantity: number; quantity: number;
addedAt: string; addedAt: string;
product: ProductSummary;
subtotal: number; subtotal: number;
slotId: number;
}
interface CartData {
items: CartItem[];
totalItems: number;
totalAmount: number;
}
interface UseGetCartOptions {
refetchOnWindowFocus?: boolean;
enabled?: boolean;
}
interface UseGetCartReturn {
data: CartData | undefined;
isLoading: boolean;
error: Error | null;
refetch: () => Promise<UseQueryResult<CartData, Error>>;
cartItems: CartItem[];
totalItems: number;
totalPrice: number;
isEmpty: boolean;
hasItems: boolean;
}
interface AddToCartVariables {
productId: number;
quantity: number;
slotId: number;
}
interface UpdateCartVariables {
itemId: number;
quantity: number;
}
interface RemoveCartVariables {
itemId: number;
}
interface MutationOptions<TData, TVariables> {
onSuccess?: (data: TData, variables: TVariables) => void;
onError?: (error: Error) => void;
showSuccessAlert?: boolean;
showErrorAlert?: boolean;
refetchCart?: boolean;
}
interface UseAddToCartReturn {
mutate: UseMutationResult<LocalCartItem[], Error, AddToCartVariables>['mutate'];
mutateAsync: UseMutationResult<LocalCartItem[], Error, AddToCartVariables>['mutateAsync'];
isLoading: boolean;
error: Error | null;
data: LocalCartItem[] | undefined;
addToCart: (productId: number, quantity?: number, slotId?: number, onSettled?: (data: LocalCartItem[] | undefined, error: Error | null) => void) => void;
addToCartAsync: (productId: number, quantity?: number, slotId?: number) => Promise<LocalCartItem[]>;
}
interface UseUpdateCartItemReturn {
mutate: UseMutationResult<LocalCartItem[], Error, UpdateCartVariables>['mutate'];
mutateAsync: UseMutationResult<LocalCartItem[], Error, UpdateCartVariables>['mutateAsync'];
isLoading: boolean;
error: Error | null;
data: LocalCartItem[] | undefined;
updateCartItem: (itemId: number, quantity: number) => void;
updateCartItemAsync: (itemId: number, quantity: number) => Promise<LocalCartItem[]>;
}
interface UseRemoveFromCartReturn {
mutate: UseMutationResult<LocalCartItem[], Error, RemoveCartVariables>['mutate'];
mutateAsync: UseMutationResult<LocalCartItem[], Error, RemoveCartVariables>['mutateAsync'];
isLoading: boolean;
error: Error | null;
data: LocalCartItem[] | undefined;
removeFromCart: (itemId: number) => void;
removeFromCartAsync: (itemId: number) => Promise<LocalCartItem[]>;
} }
const getLocalCart = async (cartType: CartType = "regular"): Promise<LocalCartItem[]> => { const getLocalCart = async (cartType: CartType = "regular"): Promise<LocalCartItem[]> => {
@ -46,8 +127,7 @@ const getLocalCart = async (cartType: CartType = "regular"): Promise<LocalCartIt
const saveLocalCart = async (items: LocalCartItem[], cartType: CartType = "regular"): Promise<void> => { const saveLocalCart = async (items: LocalCartItem[], cartType: CartType = "regular"): Promise<void> => {
const key = getCartStorageKey(cartType); const key = getCartStorageKey(cartType);
await StorageServiceCasual.setItem(key, JSON.stringify(items)); await StorageServiceCasual.setItem(key, JSON.stringify(items));
const fetchedItems = await getLocalCart(cartType); await getLocalCart(cartType);
}; };
const getNextCartItemId = (items: LocalCartItem[]): number => { const getNextCartItemId = (items: LocalCartItem[]): number => {
@ -55,8 +135,7 @@ const getNextCartItemId = (items: LocalCartItem[]): number => {
return maxId + 1; return maxId + 1;
}; };
const addToLocalCart = async (productId: number, quantity: number, slotId?: number, cartType: CartType = "regular"): Promise<LocalCartItem[]> => { const addToLocalCart = async (productId: number, quantity: number, slotId: number | undefined, cartType: CartType = "regular"): Promise<LocalCartItem[]> => {
const items = await getLocalCart(cartType); const items = await getLocalCart(cartType);
const existingIndex = items.findIndex(item => item.productId === productId); const existingIndex = items.findIndex(item => item.productId === productId);
@ -67,13 +146,13 @@ const addToLocalCart = async (productId: number, quantity: number, slotId?: numb
} }
} else { } else {
const newId = getNextCartItemId(items); const newId = getNextCartItemId(items);
const cartItem = { const cartItem: LocalCartItem = {
id: newId, id: newId,
productId, productId,
quantity, quantity,
slotId: slotId ?? 0, // Default to 0 if not provided slotId: slotId ?? 0,
addedAt: new Date().toISOString(), addedAt: new Date().toISOString(),
} };
items.push(cartItem); items.push(cartItem);
} }
@ -104,401 +183,192 @@ const clearLocalCart = async (cartType: CartType = "regular"): Promise<void> =>
await StorageServiceCasual.setItem(key, JSON.stringify([])); await StorageServiceCasual.setItem(key, JSON.stringify([]));
}; };
export function useGetCart(options?: { export function useGetCart(options: UseGetCartOptions = {}, cartType: CartType = "regular"): UseGetCartReturn {
refetchOnWindowFocus?: boolean; const { data: products } = useAllProducts();
enabled?: boolean; const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
}, cartType: CartType = "regular") {
if (CART_MODE === 'remote') {
const query = trpc.user.cart.getCart.useQuery(undefined, {
refetchOnWindowFocus: options?.refetchOnWindowFocus ?? true,
enabled: options?.enabled ?? true,
...options
});
return { const query: UseQueryResult<CartData, Error> = useQuery({
// Original tRPC returns queryKey: [`local-cart-${cartType}`],
data: query.data, queryFn: async (): Promise<CartData> => {
isLoading: query.isLoading, const cartItems = await getLocalCart(cartType);
error: query.error,
refetch: query.refetch,
// Computed properties const productMap: Record<number, Omit<ProductSummary, 'isOutOfStock' | 'isFlashAvailable'>> = Object.fromEntries(
cartItems: query.data?.items || [], products?.products?.map((p) => [
totalItems: query.data?.totalItems || 0, p.id,
totalPrice: query.data?.totalAmount || 0, {
id: p.id,
price: String(p.price),
incrementStep: p.incrementStep,
marketPrice: p.marketPrice === null || p.marketPrice === undefined ? null : String(p.marketPrice),
name: p.name,
flashPrice: p.flashPrice,
images: p.images,
productQuantity: p.productQuantity,
unitNotation: p.unitNotation,
},
]) ?? []
);
// Helper methods const items: CartItem[] = cartItems
isEmpty: !query.data?.items?.length, .map((cartItem): CartItem | null => {
hasItems: Boolean(query.data?.items?.length), const productBasic = productMap[cartItem.productId];
}; const productAvailability = productSlotsMap[cartItem.productId];
} else {
const { data: products } = trpc.common.product.getAllProductsSummary.useQuery({}); if (!productBasic || !productAvailability) return null;
const query = useQuery({
queryKey: [`local-cart-${cartType}`],
queryFn: async () => {
const cartItems = await getLocalCart(cartType);
const productMap = Object.fromEntries(
products?.products?.map((p) => [
p.id,
{
...p,
price: String(p.price),
marketPrice: p.marketPrice === null || p.marketPrice === undefined ? null : String(p.marketPrice),
} as ProductSummary,
]) || []
);
const items: CartItem[] = cartItems.map(cartItem => {
const product = productMap[cartItem.productId];
if (!product) return null as any;
return { return {
id: cartItem.id, id: cartItem.id,
productId: cartItem.productId, productId: cartItem.productId,
quantity: cartItem.quantity, quantity: cartItem.quantity,
addedAt: cartItem.addedAt, addedAt: cartItem.addedAt,
product, subtotal: Number(productBasic.price) * cartItem.quantity,
incrementStep: product.incrementStep,
subtotal: Number(product.price) * cartItem.quantity,
slotId: cartItem.slotId, slotId: cartItem.slotId,
}; };
}).filter(Boolean) as CartItem[]; })
const totalAmount = items.reduce((sum, item) => sum + item.subtotal, 0); .filter((item): item is CartItem => item !== null);
return { const totalAmount = items.reduce((sum, item) => sum + item.subtotal, 0);
items,
totalItems: items.length, return {
totalAmount, items,
}; totalItems: items.length,
}, totalAmount,
refetchOnWindowFocus: options?.refetchOnWindowFocus ?? true, };
enabled: (options?.enabled ?? true) && !!products, },
refetchOnWindowFocus: options?.refetchOnWindowFocus ?? true,
enabled: (options?.enabled ?? true) && !!products,
});
return {
data: query.data,
isLoading: query.isLoading,
error: query.error,
refetch: query.refetch,
cartItems: query.data?.items ?? [],
totalItems: query.data?.totalItems ?? 0,
totalPrice: query.data?.totalAmount ?? 0,
isEmpty: !(query.data?.items?.length ?? 0),
hasItems: Boolean(query.data?.items?.length),
};
}
export function useAddToCart(options: MutationOptions<LocalCartItem[], AddToCartVariables> = {}, cartType: CartType = "regular"): UseAddToCartReturn {
const queryClient = useQueryClient();
const mutation: UseMutationResult<LocalCartItem[], Error, AddToCartVariables> = useMutation({
mutationFn: async ({ productId, quantity, slotId }: AddToCartVariables): Promise<LocalCartItem[]> => {
return await addToLocalCart(productId, quantity, slotId, cartType);
},
onSuccess: (data: LocalCartItem[], variables: AddToCartVariables) => {
queryClient.invalidateQueries({ queryKey: [`local-cart-${cartType}`] });
if (options?.showSuccessAlert !== false) {
Alert.alert("Success", "Item added to cart!");
}
options?.onSuccess?.(data, variables);
},
onError: (error: Error) => {
if (options?.showErrorAlert !== false) {
Alert.alert("Error", error.message || "Failed to add item to cart");
}
options?.onError?.(error);
},
});
const addToCart = (productId: number, quantity = 1, slotId?: number, onSettled?: (data: LocalCartItem[] | undefined, error: Error | null) => void): void => {
if (slotId == null) {
throw new Error('slotId is required for adding to cart');
}
mutation.mutate({ productId, quantity, slotId }, {
onSettled: (data: LocalCartItem[] | undefined, error: Error | null) => {
onSettled?.(data, error);
}
}); });
};
return { return {
data: query.data, mutate: mutation.mutate,
isLoading: query.isLoading, mutateAsync: mutation.mutateAsync,
error: query.error, isLoading: mutation.isPending,
refetch: query.refetch, error: mutation.error,
data: mutation.data,
// Computed properties addToCart,
cartItems: query.data?.items || [], addToCartAsync: (productId: number, quantity = 1, slotId?: number): Promise<LocalCartItem[]> => {
totalItems: query.data?.totalItems || 0,
totalPrice: query.data?.totalAmount || 0,
// Helper methods
isEmpty: !query.data?.items?.length,
hasItems: Boolean(query.data?.items?.length),
};
}
}
interface UseAddToCartReturn {
mutate: any;
mutateAsync: any;
isLoading: boolean;
error: any;
data: any;
addToCart: (productId: number, quantity?: number, slotId?: number, onSettled?: (data: any, error: any) => void) => void;
addToCartAsync: (productId: number, quantity?: number, slotId?: number) => Promise<any>;
}
export function useAddToCart(options?: {
onSuccess?: (data: any, variables: any) => void;
onError?: (error: any) => void;
showSuccessAlert?: boolean;
showErrorAlert?: boolean;
refetchCart?: boolean;
}, cartType: CartType = "regular"): UseAddToCartReturn {
if (CART_MODE === 'remote') {
const utils = trpc.useUtils();
const mutation = trpc.user.cart.addToCart.useMutation({
onSuccess: (data, variables) => {
// Default success handling
if (options?.showSuccessAlert !== false) {
Alert.alert("Success", "Item added to cart!");
}
// Auto-refetch cart if requested
if (options?.refetchCart) {
utils.user.cart.getCart.invalidate();
}
// Custom success callback
options?.onSuccess?.(data, variables);
},
onError: (error) => {
// Default error handling
if (options?.showErrorAlert !== false) {
Alert.alert("Error", error.message || "Failed to add item to cart");
}
// Custom error callback
options?.onError?.(error);
},
}) as any;
const addToCart = (productId: number, quantity = 1, slotId?: number, onSettled?: (data: any, error: any) => void) => {
if (slotId == null) { if (slotId == null) {
throw new Error('slotId is required for adding to cart'); throw new Error('slotId is required for adding to cart');
} }
return mutation.mutate({ productId, quantity, slotId }, { return mutation.mutateAsync({ productId, quantity, slotId });
onSettled: (data: any, error: any) => { },
onSettled?.(data, error); };
} }
});
};
return { export function useUpdateCartItem(options: MutationOptions<LocalCartItem[], UpdateCartVariables> = {}, cartType: CartType = "regular"): UseUpdateCartItemReturn {
// Original mutation returns const queryClient = useQueryClient();
mutate: mutation.mutate,
mutateAsync: mutation.mutateAsync,
isLoading: mutation.isPending,
error: mutation.error,
data: mutation.data,
addToCart, const mutation: UseMutationResult<LocalCartItem[], Error, UpdateCartVariables> = useMutation({
mutationFn: async ({ itemId, quantity }: UpdateCartVariables): Promise<LocalCartItem[]> => {
addToCartAsync: (productId: number, quantity = 1, slotId?: number) => { return await updateLocalCartItem(itemId, quantity, cartType);
if (slotId == null) { },
throw new Error('slotId is required for adding to cart'); onSuccess: (data: LocalCartItem[], variables: UpdateCartVariables) => {
} queryClient.invalidateQueries({ queryKey: [`local-cart-${cartType}`] });
return mutation.mutateAsync({ productId, quantity, slotId }); if (options?.showSuccessAlert !== false) {
}, Alert.alert("Success", "Cart item updated!");
};
} else {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: async ({ productId, quantity, slotId }: { productId: number, quantity: number, slotId: number }) => {
return await addToLocalCart(productId, quantity, slotId, cartType);
},
onSuccess: (data, variables) => {
queryClient.invalidateQueries({ queryKey: [`local-cart-${cartType}`] });
if (options?.showSuccessAlert !== false) {
Alert.alert("Success", "Item added to cart!");
}
options?.onSuccess?.(data, variables);
},
onError: (error) => {
if (options?.showErrorAlert !== false) {
Alert.alert("Error", error.message || "Failed to add item to cart");
}
options?.onError?.(error);
},
});
const addToCart = (productId: number, quantity = 1, slotId?: number, onSettled?: (data: any, error: any) => void) => {
if (slotId == null) {
throw new Error('slotId is required for adding to cart');
} }
return mutation.mutate({ productId, quantity, slotId }, { options?.onSuccess?.(data, variables);
onSettled: (data: any, error: any) => { },
onSettled?.(data, error); onError: (error: Error) => {
} if (options?.showErrorAlert !== false) {
}); Alert.alert("Error", error.message || "Failed to update cart item");
}; }
options?.onError?.(error);
},
});
return { return {
mutate: mutation.mutate, mutate: mutation.mutate,
mutateAsync: mutation.mutateAsync, mutateAsync: mutation.mutateAsync,
isLoading: mutation.isPending, isLoading: mutation.isPending,
error: mutation.error, error: mutation.error,
data: mutation.data, data: mutation.data,
addToCart, updateCartItem: (itemId: number, quantity: number): void =>
addToCartAsync: (productId: number, quantity = 1, slotId?: number) => { mutation.mutate({ itemId, quantity }),
if (slotId == null) { updateCartItemAsync: (itemId: number, quantity: number): Promise<LocalCartItem[]> =>
throw new Error('slotId is required for adding to cart'); mutation.mutateAsync({ itemId, quantity }),
} };
return mutation.mutateAsync({ productId, quantity, slotId });
},
};
}
} }
export function useUpdateCartItem(options?: { export function useRemoveFromCart(options: MutationOptions<LocalCartItem[], RemoveCartVariables> = {}, cartType: CartType = "regular"): UseRemoveFromCartReturn {
onSuccess?: (data: any, variables: any) => void; const queryClient = useQueryClient();
onError?: (error: any) => void;
showSuccessAlert?: boolean;
showErrorAlert?: boolean;
refetchCart?: boolean;
}, cartType: CartType = "regular") {
if (CART_MODE === 'remote') {
const utils = trpc.useUtils();
const mutation = trpc.user.cart.updateCartItem.useMutation({ const mutation: UseMutationResult<LocalCartItem[], Error, RemoveCartVariables> = useMutation({
onSuccess: (data, variables) => { mutationFn: async ({ itemId }: RemoveCartVariables): Promise<LocalCartItem[]> => {
// Default success handling return await removeFromLocalCart(itemId, cartType);
if (options?.showSuccessAlert !== false) { },
Alert.alert("Success", "Cart item updated!"); onSuccess: (data: LocalCartItem[], variables: RemoveCartVariables) => {
} queryClient.invalidateQueries({ queryKey: [`local-cart-${cartType}`] });
if (options?.showSuccessAlert !== false) {
Alert.alert("Success", "Item removed from cart!");
}
options?.onSuccess?.(data, variables);
},
onError: (error: Error) => {
if (options?.showErrorAlert !== false) {
Alert.alert("Error", error.message || "Failed to remove item from cart");
}
options?.onError?.(error);
},
});
// Auto-refetch cart if requested return {
if (options?.refetchCart) { mutate: mutation.mutate,
utils.user.cart.getCart.invalidate(); mutateAsync: mutation.mutateAsync,
} isLoading: mutation.isPending,
error: mutation.error,
// Custom success callback data: mutation.data,
options?.onSuccess?.(data, variables); removeFromCart: (itemId: number): void =>
}, mutation.mutate({ itemId }),
onError: (error) => { removeFromCartAsync: (itemId: number): Promise<LocalCartItem[]> =>
// Default error handling mutation.mutateAsync({ itemId }),
if (options?.showErrorAlert !== false) { };
Alert.alert("Error", error.message || "Failed to update cart item");
}
// Custom error callback
options?.onError?.(error);
},
});
return {
// Original mutation returns
mutate: mutation.mutate,
mutateAsync: mutation.mutateAsync,
isLoading: mutation.isPending,
error: mutation.error,
data: mutation.data,
// Helper methods
updateCartItem: (itemId: number, quantity: number) =>
mutation.mutate({ itemId, quantity }),
updateCartItemAsync: (itemId: number, quantity: number) =>
mutation.mutateAsync({ itemId, quantity }),
};
} else {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: async ({ itemId, quantity }: { itemId: number, quantity: number }) => {
return await updateLocalCartItem(itemId, quantity, cartType);
},
onSuccess: (data, variables) => {
queryClient.invalidateQueries({ queryKey: [`local-cart-${cartType}`] });
if (options?.showSuccessAlert !== false) {
Alert.alert("Success", "Cart item updated!");
}
options?.onSuccess?.(data, variables);
},
onError: (error) => {
if (options?.showErrorAlert !== false) {
Alert.alert("Error", error.message || "Failed to update cart item");
}
options?.onError?.(error);
},
});
return {
mutate: mutation.mutate,
mutateAsync: mutation.mutateAsync,
isLoading: mutation.isPending,
error: mutation.error,
data: mutation.data,
updateCartItem: (itemId: number, quantity: number) =>
mutation.mutate({ itemId, quantity }),
updateCartItemAsync: (itemId: number, quantity: number) =>
mutation.mutateAsync({ itemId, quantity }),
};
}
}
export function useRemoveFromCart(options?: {
onSuccess?: (data: any, variables: any) => void;
onError?: (error: any) => void;
showSuccessAlert?: boolean;
showErrorAlert?: boolean;
refetchCart?: boolean;
}, cartType: CartType = "regular") {
if (CART_MODE === 'remote') {
const utils = trpc.useUtils();
const mutation = trpc.user.cart.removeFromCart.useMutation({
onSuccess: (data, variables) => {
// Default success handling
if (options?.showSuccessAlert !== false) {
Alert.alert("Success", "Item removed from cart!");
}
// Auto-refetch cart if requested
if (options?.refetchCart) {
utils.user.cart.getCart.invalidate();
}
// Custom success callback
options?.onSuccess?.(data, variables);
},
onError: (error) => {
// Default error handling
if (options?.showErrorAlert !== false) {
Alert.alert("Error", error.message || "Failed to remove item from cart");
}
// Custom error callback
options?.onError?.(error);
},
});
return {
// Original mutation returns
mutate: mutation.mutate,
mutateAsync: mutation.mutateAsync,
isLoading: mutation.isPending,
error: mutation.error,
data: mutation.data,
// Helper methods
removeFromCart: (itemId: number) =>
mutation.mutate({ itemId }),
removeFromCartAsync: (itemId: number) =>
mutation.mutateAsync({ itemId }),
};
} else {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: async ({ itemId }: { itemId: number }) => {
return await removeFromLocalCart(itemId, cartType);
},
onSuccess: (data, variables) => {
queryClient.invalidateQueries({ queryKey: [`local-cart-${cartType}`] });
if (options?.showSuccessAlert !== false) {
Alert.alert("Success", "Item removed from cart!");
}
options?.onSuccess?.(data, variables);
},
onError: (error) => {
if (options?.showErrorAlert !== false) {
Alert.alert("Error", error.message || "Failed to remove item from cart");
}
options?.onError?.(error);
},
});
return {
mutate: mutation.mutate,
mutateAsync: mutation.mutateAsync,
isLoading: mutation.isPending,
error: mutation.error,
data: mutation.data,
removeFromCart: (itemId: number) =>
mutation.mutate({ itemId }),
removeFromCartAsync: (itemId: number) =>
mutation.mutateAsync({ itemId }),
};
}
} }
// Export clear cart function for direct use // Export clear cart function for direct use

View file

@ -1,46 +1,28 @@
import { trpc } from '@/src/trpc-client';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useCentralSlotStore } from '@/src/store/centralSlotStore';
export function useProductSlotIdentifier() { export function useProductSlotIdentifier() {
// Fetch all slots with products // Get slots data from central store
const { data: slotsData, isLoading: isProductsLoading } = trpc.user.slots.getSlotsWithProducts.useQuery(); const slots = useCentralSlotStore((state) => state.slots);
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
const productSlotsMap = new Map<number, number[]>();
if (slotsData?.slots) {
const now = dayjs();
// Build map of productId to available slot IDs
slotsData.slots.forEach(slot => {
if (dayjs(slot.deliveryTime).isAfter(now)) {
slot.products.forEach(product => {
if (!productSlotsMap.has(product.id)) {
productSlotsMap.set(product.id, []);
}
productSlotsMap.get(product.id)!.push(slot.id);
});
}
});
}
const getQuickestSlot = (productId: number): number | null => { const getQuickestSlot = (productId: number): number | null => {
if (!slots?.length) return null;
if (!slotsData?.slots) return null;
const now = dayjs(); const now = dayjs();
const productInfo = productSlotsMap[productId];
if (!productInfo?.slots?.length) return null;
// Find slots that contain this product and have future delivery time // Find slots that contain this product and have future delivery time
const availableSlots = slotsData.slots.filter(slot => const availableSlots = productInfo.slots.filter((slot: any) =>
slot.products.some(product => product.id === productId) &&
dayjs(slot.deliveryTime).isAfter(now) dayjs(slot.deliveryTime).isAfter(now)
); );
// if(productId === 98)
// console.log(JSON.stringify(slotsData))
if (availableSlots.length === 0) return null; if (availableSlots.length === 0) return null;
// Return earliest slot ID (sorted by delivery time) // Return earliest slot ID (sorted by delivery time)
const earliestSlot = availableSlots.sort((a, b) => const earliestSlot = availableSlots.sort((a: any, b: any) =>
dayjs(a.deliveryTime).diff(dayjs(b.deliveryTime)) dayjs(a.deliveryTime).diff(dayjs(b.deliveryTime))
)[0]; )[0];

View file

@ -1,6 +1,20 @@
// Learn more on how to setup config for the app: https://docs.expo.dev/guides/config-plugins/#metro-config // Learn more on how to setup config for the app: https://docs.expo.dev/guides/config-plugins/#metro-config
const { getDefaultConfig } = require('expo/metro-config'); const { getDefaultConfig } = require('expo/metro-config');
const path = require('path');
const config = getDefaultConfig(__dirname); const config = getDefaultConfig(__dirname);
// Add the packages directory to watch folders
config.watchFolders = [
...config.watchFolders || [],
path.resolve(__dirname, '../../packages/shared'),
];
// Configure module resolution for @packages/*
config.resolver.extraNodeModules = {
...config.resolver.extraNodeModules,
'@packages/shared': path.resolve(__dirname, '../../packages/shared'),
'global-shared': path.resolve(__dirname, '../../packages/shared'),
};
module.exports = config; module.exports = config;

Binary file not shown.

View file

@ -48,6 +48,7 @@
"expo-updates": "~0.28.17", "expo-updates": "~0.28.17",
"expo-web-browser": "~14.2.0", "expo-web-browser": "~14.2.0",
"formik": "^2.4.6", "formik": "^2.4.6",
"fuse.js": "^7.1.0",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"react": "19.0.0", "react": "19.0.0",
"react-dom": "19.0.0", "react-dom": "19.0.0",

View file

@ -1,8 +0,0 @@
import { trpc } from '@/src/trpc-client';
export const useGetEssentialConsts = () => {
const query = trpc.common.essentialConsts.useQuery(undefined, {
refetchInterval: 60000,
});
return { ...query, refetch: query.refetch };
};

View file

@ -5,9 +5,9 @@ import { tw, BottomDialog, MyText, MyTouchableOpacity, Quantifier } from 'common
import MaterialIcons from '@expo/vector-icons/MaterialIcons'; import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { useCartStore } from '@/src/store/cartStore'; import { useCartStore } from '@/src/store/cartStore';
import { useFlashCartStore } from '@/src/store/flashCartStore'; import { useFlashCartStore } from '@/src/store/flashCartStore';
import { trpc } from '@/src/trpc-client'; import { useCentralSlotStore } from '@/src/store/centralSlotStore';
import { useAddToCart, useGetCart, useUpdateCartItem, useRemoveFromCart } from '@/hooks/cart-query-hooks'; import { useAddToCart, useGetCart, useUpdateCartItem, useRemoveFromCart } from '@/hooks/cart-query-hooks';
import { useGetEssentialConsts } from '@/src/api-hooks/essential-consts.api'; import { useGetEssentialConsts, useSlots } from '@/src/hooks/prominent-api-hooks';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
@ -31,9 +31,10 @@ export default function AddToCartDialog() {
const [selectedSlotId, setSelectedSlotId] = useState<number | null>(null); const [selectedSlotId, setSelectedSlotId] = useState<number | null>(null);
const [selectedFlashDelivery, setSelectedFlashDelivery] = useState(false); const [selectedFlashDelivery, setSelectedFlashDelivery] = useState(false);
const { data: slotsData } = trpc.user.slots.getSlotsWithProducts.useQuery(); const { data: slotsData } = useSlots();
const { data: cartData } = useGetCart(); const { data: cartData } = useGetCart();
const { data: constsData } = useGetEssentialConsts(); const { data: constsData } = useGetEssentialConsts();
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
// const isFlashDeliveryEnabled = constsData?.isFlashDeliveryEnabled === true; // const isFlashDeliveryEnabled = constsData?.isFlashDeliveryEnabled === true;
const isFlashDeliveryEnabled = true; const isFlashDeliveryEnabled = true;
@ -113,7 +114,7 @@ export default function AddToCartDialog() {
const isUpdate = (cartItem?.quantity || 0) >= 1; const isUpdate = (cartItem?.quantity || 0) >= 1;
// Check if flash delivery option should be shown // Check if flash delivery option should be shown
const showFlashOption = product?.isFlashAvailable === true && isFlashDeliveryEnabled; const showFlashOption = productSlotsMap[product?.id]?.isFlashAvailable === true && isFlashDeliveryEnabled;
const handleAddToCart = () => { const handleAddToCart = () => {
if (selectedFlashDelivery) { if (selectedFlashDelivery) {

View file

@ -0,0 +1,14 @@
import React from 'react';
import { useInitializeCentralSlotStore } from '@/src/store/centralSlotStore';
import { useInitializeCentralProductStore } from '@/src/store/centralProductStore';
interface CentralStoreInitializerProps {
children: React.ReactNode;
}
export default function CentralStoreInitializer({ children }: CentralStoreInitializerProps) {
useInitializeCentralSlotStore();
useInitializeCentralProductStore();
return <>{children}</>;
}

View file

@ -0,0 +1,125 @@
import { useQuery } from '@tanstack/react-query'
import axios from 'axios'
import { trpc } from '@/src/trpc-client'
import { AllProductsApiType, StoresApiType, SlotsApiType, EssentialConstsApiType, BannersApiType, StoreWithProductsApiType } from "@backend/trpc/router";
import { CACHE_FILENAMES } from "@packages/shared";
// Local useGetEssentialConsts hook
export const useGetEssentialConsts = () => {
const query = trpc.common.essentialConsts.useQuery(undefined, {
refetchInterval: 60000,
})
return { ...query, refetch: query.refetch }
}
type ProductsResponse = AllProductsApiType;
type StoresResponse = StoresApiType;
type SlotsResponse = SlotsApiType;
type EssentialConstsResponse = EssentialConstsApiType;
type BannersResponse = BannersApiType;
type StoreWithProductsResponse = StoreWithProductsApiType;
function useCacheUrl(filename: string): string | null {
const { data: essentialConsts } = useGetEssentialConsts()
const assetsDomain = essentialConsts?.assetsDomain
const apiCacheKey = essentialConsts?.apiCacheKey
return assetsDomain && apiCacheKey
? `${assetsDomain}${apiCacheKey}/${filename}`
: null
}
export function useAllProducts() {
const cacheUrl = useCacheUrl(CACHE_FILENAMES.products)
return useQuery<ProductsResponse>({
queryKey: ['all-products', cacheUrl],
queryFn: async () => {
if (!cacheUrl) {
throw new Error('Cache URL not available')
}
const response = await axios.get<ProductsResponse>(cacheUrl)
return response.data
},
staleTime: 60000, // 1 minute
enabled: !!cacheUrl,
})
}
export function useStores() {
const cacheUrl = useCacheUrl(CACHE_FILENAMES.stores)
return useQuery<StoresResponse>({
queryKey: ['stores', cacheUrl],
queryFn: async () => {
if (!cacheUrl) {
throw new Error('Cache URL not available')
}
const response = await axios.get<StoresResponse>(cacheUrl)
return response.data
},
staleTime: 60000, // 1 minute
enabled: !!cacheUrl,
})
}
export function useSlots() {
const cacheUrl = useCacheUrl(CACHE_FILENAMES.slots)
return useQuery<SlotsResponse>({
queryKey: ['slots', cacheUrl],
queryFn: async () => {
if (!cacheUrl) {
throw new Error('Cache URL not available')
}
const response = await axios.get<SlotsResponse>(cacheUrl)
return response.data
},
staleTime: 60000, // 1 minute
enabled: !!cacheUrl,
})
}
export function useBanners() {
const cacheUrl = useCacheUrl(CACHE_FILENAMES.banners)
return useQuery<BannersResponse>({
queryKey: ['banners', cacheUrl],
queryFn: async () => {
if (!cacheUrl) {
throw new Error('Cache URL not available')
}
const response = await axios.get<BannersResponse>(cacheUrl)
return response.data
},
staleTime: 60000, // 1 minute
enabled: !!cacheUrl,
})
}
export function useStoreWithProducts(storeId: number) {
const { data: essentialConsts } = useGetEssentialConsts()
const assetsDomain = essentialConsts?.assetsDomain
const apiCacheKey = essentialConsts?.apiCacheKey
const cacheUrl = assetsDomain && apiCacheKey
? `${assetsDomain}${apiCacheKey}/stores/${storeId}.json`
: null
return useQuery<StoreWithProductsResponse>({
queryKey: ['store-with-products', storeId, cacheUrl],
queryFn: async () => {
if (!cacheUrl) {
throw new Error('Cache URL not available')
}
const response = await axios.get<StoreWithProductsResponse>(cacheUrl)
return response.data
},
staleTime: 60000, // 1 minute
enabled: !!cacheUrl,
})
}

View file

@ -0,0 +1,50 @@
import { create } from 'zustand'
import { useEffect } from 'react'
import { useAllProducts } from '@/src/hooks/prominent-api-hooks'
import { AllProductsApiType } from '@backend/trpc/router'
type Product = AllProductsApiType['products'][number]
interface CentralProductState {
products: Product[]
productsById: Record<number, Product>
refetchProducts: (() => Promise<void>) | null
setProducts: (products: Product[]) => void
clearProducts: () => void
setRefetchProducts: (refetch: () => Promise<void>) => void
}
export const useCentralProductStore = create<CentralProductState>((set) => ({
products: [],
productsById: {},
refetchProducts: null,
setProducts: (products) => {
const productsById: Record<number, Product> = {}
products.forEach((product) => {
productsById[product.id] = product
})
set({ products, productsById })
},
clearProducts: () => set({ products: [], productsById: {} }),
setRefetchProducts: (refetchProducts) => set({ refetchProducts }),
}))
export function useInitializeCentralProductStore() {
const { data: productsData, refetch } = useAllProducts()
const setProducts = useCentralProductStore((state) => state.setProducts)
const setRefetchProducts = useCentralProductStore((state) => state.setRefetchProducts)
useEffect(() => {
if (productsData?.products) {
setProducts(productsData.products)
}
}, [productsData, setProducts])
useEffect(() => {
setRefetchProducts(async () => {
await refetch()
})
}, [refetch, setRefetchProducts])
}

View file

@ -0,0 +1,71 @@
import { create } from 'zustand';
import { useSlots } from '@/src/hooks/prominent-api-hooks';
import { useEffect } from 'react';
import { SlotsApiType } from "@backend/trpc/router";
type Slot = SlotsApiType['slots'][number];
type ProductAvailability = SlotsApiType['productAvailability'][number];
interface ProductSlotInfo {
slots: Slot[];
isOutOfStock: boolean;
isFlashAvailable: boolean;
}
interface CentralSlotState {
slots: Slot[];
productSlotsMap: Record<number, ProductSlotInfo>;
refetchSlots: (() => Promise<void>) | null;
setSlotsData: (slots: Slot[], productAvailability: ProductAvailability[]) => void;
clearSlotsData: () => void;
setRefetchSlots: (refetch: () => Promise<void>) => void;
}
export const useCentralSlotStore = create<CentralSlotState>((set) => ({
slots: [],
productSlotsMap: {},
refetchSlots: null,
setSlotsData: (slots, productAvailability) => {
const productSlotsMap: Record<number, ProductSlotInfo> = {};
// First, create entries for ALL products from productAvailability
productAvailability.forEach((product) => {
productSlotsMap[product.id] = {
slots: [],
isOutOfStock: product.isOutOfStock,
isFlashAvailable: product.isFlashAvailable,
};
});
// Then, populate slots for products that appear in delivery slots
slots.forEach((slot) => {
slot.products?.forEach((product) => {
if (productSlotsMap[product.id]) {
productSlotsMap[product.id].slots.push(slot);
}
});
});
set({ slots, productSlotsMap });
},
clearSlotsData: () => set({ slots: [], productSlotsMap: {} }),
setRefetchSlots: (refetchSlots) => set({ refetchSlots }),
}));
export function useInitializeCentralSlotStore() {
const { data: slotsData, refetch } = useSlots();
const setSlotsData = useCentralSlotStore((state) => state.setSlotsData);
const setRefetchSlots = useCentralSlotStore((state) => state.setRefetchSlots);
useEffect(() => {
if (slotsData?.slots) {
setSlotsData(slotsData.slots, slotsData.productAvailability || []);
}
}, [slotsData, setSlotsData]);
useEffect(() => {
setRefetchSlots(async () => {
await refetch();
});
}, [refetch, setRefetchSlots]);
}

View file

@ -18,6 +18,18 @@
], ],
"common-ui/*": [ "common-ui/*": [
"../../packages/ui/*" "../../packages/ui/*"
],
"@packages/shared": [
"../../packages/shared"
],
"@packages/shared/*": [
"../../packages/shared/*"
],
"global-shared": [
"../../packages/shared"
],
"global-shared/*": [
"../../packages/shared/*"
] ]
}, },
"moduleSuffixes": [ "moduleSuffixes": [
@ -34,5 +46,6 @@
"**/*.tsx", "**/*.tsx",
".expo/types/**/*.ts", ".expo/types/**/*.ts",
"expo-env.d.ts", "expo-env.d.ts",
"../../packages/shared"
] ]
} }

4028
bun.lock Normal file

File diff suppressed because it is too large Load diff

30
ios/.gitignore vendored
View file

@ -1,30 +0,0 @@
# OSX
#
.DS_Store
# Xcode
#
build/
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xccheckout
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate
project.xcworkspace
.xcode.env.local
# Bundle artifacts
*.jsbundle
# CocoaPods
/Pods/

View file

@ -1,11 +0,0 @@
# This `.xcode.env` file is versioned and is used to source the environment
# used when running script phases inside Xcode.
# To customize your local environment, you can create an `.xcode.env.local`
# file that is not versioned.
# NODE_BINARY variable contains the PATH to the node executable.
#
# Customize the NODE_BINARY variable here.
# For example, to use nvm with brew, add the following line
# . "$(brew --prefix nvm)/nvm.sh" --no-use
export NODE_BINARY=$(command -v node)

View file

@ -1,64 +0,0 @@
require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking")
require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods")
require 'json'
podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {}
ENV['RCT_NEW_ARCH_ENABLED'] = '0' if podfile_properties['newArchEnabled'] == 'false'
ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] = podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR']
platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1'
install! 'cocoapods',
:deterministic_uuids => false
prepare_react_native_project!
target 'meatfarmermonorepo' do
use_expo_modules!
if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1'
config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"];
else
config_command = [
'npx',
'expo-modules-autolinking',
'react-native-config',
'--json',
'--platform',
'ios'
]
end
config = use_native_modules!(config_command)
use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks']
use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS']
use_react_native!(
:path => config[:reactNativePath],
:hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes',
# An absolute path to your application root.
:app_path => "#{Pod::Config.instance.installation_root}/..",
:privacy_file_aggregation_enabled => podfile_properties['apple.privacyManifestAggregationEnabled'] != 'false',
)
post_install do |installer|
react_native_post_install(
installer,
config[:reactNativePath],
:mac_catalyst_enabled => false,
:ccache_enabled => podfile_properties['apple.ccacheEnabled'] == 'true',
)
# This is necessary for Xcode 14, because it signs resource bundles by default
# when building for devices.
installer.target_installation_results.pod_target_installation_results
.each do |pod_name, target_installation_result|
target_installation_result.resource_bundle_targets.each do |resource_bundle_target|
resource_bundle_target.build_configurations.each do |config|
config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO'
end
end
end
end
end

File diff suppressed because it is too large Load diff

View file

@ -1,4 +0,0 @@
{
"expo.jsEngine": "hermes",
"EX_DEV_CLIENT_NETWORK_INSPECTOR": "true"
}

View file

@ -1,567 +0,0 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
2C4FDEE95846910CE44B063B /* libPods-meatfarmermonorepo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 37BA16CD265ED7F72409550D /* libPods-meatfarmermonorepo.a */; };
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
D20664F10ED8090F4983CFB0 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EB8BC3528ACACFE6A3CFB5D /* ExpoModulesProvider.swift */; };
E5B462C85E4DB34FCCD3D04E /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = F66C424AA0E1347CF8253669 /* PrivacyInfo.xcprivacy */; };
F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11748412D0307B40044C1D9 /* AppDelegate.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
13B07F961A680F5B00A75B9A /* meatfarmermonorepo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = meatfarmermonorepo.app; sourceTree = BUILT_PRODUCTS_DIR; };
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = meatfarmermonorepo/Images.xcassets; sourceTree = "<group>"; };
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = meatfarmermonorepo/Info.plist; sourceTree = "<group>"; };
338F04D2CAE7495C1D7CD233 /* Pods-meatfarmermonorepo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-meatfarmermonorepo.debug.xcconfig"; path = "Target Support Files/Pods-meatfarmermonorepo/Pods-meatfarmermonorepo.debug.xcconfig"; sourceTree = "<group>"; };
37BA16CD265ED7F72409550D /* libPods-meatfarmermonorepo.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-meatfarmermonorepo.a"; sourceTree = BUILT_PRODUCTS_DIR; };
3EB8BC3528ACACFE6A3CFB5D /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-meatfarmermonorepo/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
533CB49887DAADE200BAF1EB /* Pods-meatfarmermonorepo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-meatfarmermonorepo.release.xcconfig"; path = "Target Support Files/Pods-meatfarmermonorepo/Pods-meatfarmermonorepo.release.xcconfig"; sourceTree = "<group>"; };
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = meatfarmermonorepo/SplashScreen.storyboard; sourceTree = "<group>"; };
BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = "<group>"; };
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
F11748412D0307B40044C1D9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = meatfarmermonorepo/AppDelegate.swift; sourceTree = "<group>"; };
F11748442D0722820044C1D9 /* meatfarmermonorepo-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "meatfarmermonorepo-Bridging-Header.h"; path = "meatfarmermonorepo/meatfarmermonorepo-Bridging-Header.h"; sourceTree = "<group>"; };
F66C424AA0E1347CF8253669 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = meatfarmermonorepo/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
13B07F8C1A680F5B00A75B9A /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
2C4FDEE95846910CE44B063B /* libPods-meatfarmermonorepo.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
13B07FAE1A68108700A75B9A /* meatfarmermonorepo */ = {
isa = PBXGroup;
children = (
F11748412D0307B40044C1D9 /* AppDelegate.swift */,
F11748442D0722820044C1D9 /* meatfarmermonorepo-Bridging-Header.h */,
BB2F792B24A3F905000567C9 /* Supporting */,
13B07FB51A68108700A75B9A /* Images.xcassets */,
13B07FB61A68108700A75B9A /* Info.plist */,
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */,
F66C424AA0E1347CF8253669 /* PrivacyInfo.xcprivacy */,
);
name = meatfarmermonorepo;
sourceTree = "<group>";
};
2D16E6871FA4F8E400B85C8A /* Frameworks */ = {
isa = PBXGroup;
children = (
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
37BA16CD265ED7F72409550D /* libPods-meatfarmermonorepo.a */,
);
name = Frameworks;
sourceTree = "<group>";
};
3742C5BD2E66753B5F39026B /* ExpoModulesProviders */ = {
isa = PBXGroup;
children = (
4DACB6B6CCC8B1A8CBF98D1D /* meatfarmermonorepo */,
);
name = ExpoModulesProviders;
sourceTree = "<group>";
};
4DACB6B6CCC8B1A8CBF98D1D /* meatfarmermonorepo */ = {
isa = PBXGroup;
children = (
3EB8BC3528ACACFE6A3CFB5D /* ExpoModulesProvider.swift */,
);
name = meatfarmermonorepo;
sourceTree = "<group>";
};
832341AE1AAA6A7D00B99B32 /* Libraries */ = {
isa = PBXGroup;
children = (
);
name = Libraries;
sourceTree = "<group>";
};
83CBB9F61A601CBA00E9B192 = {
isa = PBXGroup;
children = (
13B07FAE1A68108700A75B9A /* meatfarmermonorepo */,
832341AE1AAA6A7D00B99B32 /* Libraries */,
83CBBA001A601CBA00E9B192 /* Products */,
2D16E6871FA4F8E400B85C8A /* Frameworks */,
D7B1D84C142A04E96F6A3A3D /* Pods */,
3742C5BD2E66753B5F39026B /* ExpoModulesProviders */,
);
indentWidth = 2;
sourceTree = "<group>";
tabWidth = 2;
usesTabs = 0;
};
83CBBA001A601CBA00E9B192 /* Products */ = {
isa = PBXGroup;
children = (
13B07F961A680F5B00A75B9A /* meatfarmermonorepo.app */,
);
name = Products;
sourceTree = "<group>";
};
BB2F792B24A3F905000567C9 /* Supporting */ = {
isa = PBXGroup;
children = (
BB2F792C24A3F905000567C9 /* Expo.plist */,
);
name = Supporting;
path = meatfarmermonorepo/Supporting;
sourceTree = "<group>";
};
D7B1D84C142A04E96F6A3A3D /* Pods */ = {
isa = PBXGroup;
children = (
338F04D2CAE7495C1D7CD233 /* Pods-meatfarmermonorepo.debug.xcconfig */,
533CB49887DAADE200BAF1EB /* Pods-meatfarmermonorepo.release.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
13B07F861A680F5B00A75B9A /* meatfarmermonorepo */ = {
isa = PBXNativeTarget;
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "meatfarmermonorepo" */;
buildPhases = (
08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */,
5BC667EA2551B13AA89D9A66 /* [Expo] Configure project */,
13B07F871A680F5B00A75B9A /* Sources */,
13B07F8C1A680F5B00A75B9A /* Frameworks */,
13B07F8E1A680F5B00A75B9A /* Resources */,
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */,
DAF2E4EA15761DC5FE4C4A8B /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = meatfarmermonorepo;
productName = meatfarmermonorepo;
productReference = 13B07F961A680F5B00A75B9A /* meatfarmermonorepo.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
83CBB9F71A601CBA00E9B192 /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1130;
TargetAttributes = {
13B07F861A680F5B00A75B9A = {
LastSwiftMigration = 1250;
};
};
};
buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "meatfarmermonorepo" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 83CBB9F61A601CBA00E9B192;
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
13B07F861A680F5B00A75B9A /* meatfarmermonorepo */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
13B07F8E1A680F5B00A75B9A /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */,
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */,
E5B462C85E4DB34FCCD3D04E /* PrivacyInfo.xcprivacy in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Bundle React Native code and images";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\nif [[ \"$CONFIGURATION\" = *Debug* ]]; then\n export SKIP_BUNDLING=1\nfi\nif [[ -z \"$ENTRY_FILE\" ]]; then\n # Set the entry JS file using the bundler's entry resolution.\n export ENTRY_FILE=\"$(\"$NODE_BINARY\" -e \"require('expo/scripts/resolveAppEntry')\" \"$PROJECT_ROOT\" ios absolute | tail -n 1)\"\nfi\n\nif [[ -z \"$CLI_PATH\" ]]; then\n # Use Expo CLI\n export CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })\")\"\nfi\nif [[ -z \"$BUNDLE_COMMAND\" ]]; then\n # Default Expo CLI command for bundling\n export BUNDLE_COMMAND=\"export:embed\"\nfi\n\n# Source .xcode.env.updates if it exists to allow\n# SKIP_BUNDLING to be unset if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.updates\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.updates\"\nfi\n# Source local changes to allow overrides\n# if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n`\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n\n";
};
08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-meatfarmermonorepo-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
5BC667EA2551B13AA89D9A66 /* [Expo] Configure project */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "[Expo] Configure project";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-meatfarmermonorepo/expo-configure-project.sh\"\n";
};
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-meatfarmermonorepo/Pods-meatfarmermonorepo-resources.sh",
"${PODS_CONFIGURATION_BUILD_DIR}/AppAuth/AppAuthCore_Privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/EXApplication/ExpoApplication_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/EXNotifications/ExpoNotifications_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/EXUpdates/EXUpdates.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoDevice/ExpoDevice_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoSystemUI/ExpoSystemUI_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/GTMAppAuth/GTMAppAuth_Privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/GTMSessionFetcher/GTMSessionFetcher_Core_Privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/GoogleSignIn/GoogleSignIn.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/GoogleUtilities/GoogleUtilities_Privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/PromisesObjC/FBLPromises_Privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ReachabilitySwift/ReachabilitySwift.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle",
);
name = "[CP] Copy Pods Resources";
outputPaths = (
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AppAuthCore_Privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoApplication_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoNotifications_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXUpdates.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoDevice_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoSystemUI_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GTMAppAuth_Privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GTMSessionFetcher_Core_Privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleSignIn.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleUtilities_Privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FBLPromises_Privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ReachabilitySwift.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SDWebImage.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-meatfarmermonorepo/Pods-meatfarmermonorepo-resources.sh\"\n";
showEnvVarsInLog = 0;
};
DAF2E4EA15761DC5FE4C4A8B /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-meatfarmermonorepo/Pods-meatfarmermonorepo-frameworks.sh",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes",
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-meatfarmermonorepo/Pods-meatfarmermonorepo-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
13B07F871A680F5B00A75B9A /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */,
D20664F10ED8090F4983CFB0 /* ExpoModulesProvider.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
13B07F941A680F5B00A75B9A /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 338F04D2CAE7495C1D7CD233 /* Pods-meatfarmermonorepo.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = meatfarmermonorepo/meatfarmermonorepo.entitlements;
CURRENT_PROJECT_VERSION = 1;
ENABLE_BITCODE = NO;
GCC_PREPROCESSOR_DEFINITIONS = (
"$(inherited)",
"FB_SONARKIT_ENABLED=1",
);
INFOPLIST_FILE = meatfarmermonorepo/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
"-lc++",
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = "com.mohammedshafiuddin54.meat-farmer-monorepo";
PRODUCT_NAME = meatfarmermonorepo;
SWIFT_OBJC_BRIDGING_HEADER = "meatfarmermonorepo/meatfarmermonorepo-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
13B07F951A680F5B00A75B9A /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 533CB49887DAADE200BAF1EB /* Pods-meatfarmermonorepo.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = meatfarmermonorepo/meatfarmermonorepo.entitlements;
CURRENT_PROJECT_VERSION = 1;
INFOPLIST_FILE = meatfarmermonorepo/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
"-lc++",
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = "com.mohammedshafiuddin54.meat-farmer-monorepo";
PRODUCT_NAME = meatfarmermonorepo;
SWIFT_OBJC_BRIDGING_HEADER = "meatfarmermonorepo/meatfarmermonorepo-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
83CBBA201A601CBA00E9B192 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_CXX_LANGUAGE_STANDARD = "c++20";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_SYMBOLS_PRIVATE_EXTERN = NO;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = (
/usr/lib/swift,
"$(inherited)",
);
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
USE_HERMES = true;
};
name = Debug;
};
83CBBA211A601CBA00E9B192 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_CXX_LANGUAGE_STANDARD = "c++20";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = YES;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = (
/usr/lib/swift,
"$(inherited)",
);
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
MTL_ENABLE_DEBUG_INFO = NO;
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
USE_HERMES = true;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "meatfarmermonorepo" */ = {
isa = XCConfigurationList;
buildConfigurations = (
13B07F941A680F5B00A75B9A /* Debug */,
13B07F951A680F5B00A75B9A /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "meatfarmermonorepo" */ = {
isa = XCConfigurationList;
buildConfigurations = (
83CBBA201A601CBA00E9B192 /* Debug */,
83CBBA211A601CBA00E9B192 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */;
}

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