Compare commits

...

2 commits

Author SHA1 Message Date
shafi54
3d7e023965 enh 2026-02-07 10:17:33 +05:30
shafi54
b812992419 enh 2026-02-07 01:53:31 +05:30
25 changed files with 190 additions and 72 deletions

File diff suppressed because one or more lines are too long

View file

@ -201,9 +201,7 @@ export default function Layout() {
<Drawer.Screen name="dashboard" options={{ title: "Dashboard" }} /> <Drawer.Screen name="dashboard" options={{ title: "Dashboard" }} />
<Drawer.Screen name="products" options={{ title: "Products" }} /> <Drawer.Screen name="products" options={{ title: "Products" }} />
<Drawer.Screen name="prices-overview" options={{ title: "Prices Overview" }} /> <Drawer.Screen name="prices-overview" options={{ title: "Prices Overview" }} />
<Drawer.Screen name="product-groupings" options={{ title: "Product Groupings" }} /> <Drawer.Screen name="product-groupings" options={{ title: "Product Groupings" }} />
<Drawer.Screen name="create-product-group" options={{ title: "Create Product Group" }} />
<Drawer.Screen name="edit-product-group/[id]" options={{ title: "Edit Product Group" }} />
<Drawer.Screen <Drawer.Screen
@ -213,14 +211,12 @@ export default function Layout() {
<Drawer.Screen name="complaints" options={{ title: "Complaints" }} /> <Drawer.Screen name="complaints" options={{ title: "Complaints" }} />
<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="delivery-sequences" options={{ title: "Delivery Sequences", headerShown: false }} /> <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="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="order-details/[id]" options={{ title: "Order Details" }} /> <Drawer.Screen name="order-details/[id]" options={{ title: "Order Details" }} />
<Drawer.Screen name="orders" options={{ title: "Orders" }} /> <Drawer.Screen name="rebalance-orders" options={{ title: "Rebalance Orders" }} />
<Drawer.Screen name="rebalance-orders" options={{ title: "Rebalance Orders" }} />
</Drawer> </Drawer>
); );
} }

View file

@ -4,8 +4,8 @@ export default function Layout() {
return ( return (
<Stack screenOptions={{ headerShown: false }}> <Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="index" options={{ title: 'Dashboard Banners' }} /> <Stack.Screen name="index" options={{ title: 'Dashboard Banners' }} />
<Stack.Screen name="create-banner" options={{ title: 'Create Banner' }} /> <Stack.Screen name="create" options={{ title: 'Create Banner' }} />
<Stack.Screen name="edit-banner" options={{ title: 'Edit Banner' }} /> <Stack.Screen name="edit/[id]" options={{ title: 'Edit Banner' }} />
</Stack> </Stack>
); );
} }

View file

@ -1,9 +0,0 @@
import { Stack } from 'expo-router';
export default function Layout() {
return (
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="index" options={{ title: 'Create Banner' }} />
</Stack>
);
}

View file

@ -5,7 +5,7 @@ import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import { FormikHelpers } from 'formik'; import { FormikHelpers } from 'formik';
import BannerForm, { BannerFormData } from '@/components/BannerForm'; import BannerForm, { BannerFormData } from '@/components/BannerForm';
import { trpc } from '../../../../src/trpc-client'; import { trpc } from '@/src/trpc-client';
export default function CreateBanner() { export default function CreateBanner() {
const router = useRouter(); const router = useRouter();

View file

@ -1,9 +0,0 @@
import { Stack } from 'expo-router';
export default function Layout() {
return (
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="[id]" options={{ title: 'Edit Banner' }} />
</Stack>
);
}

View file

@ -5,7 +5,7 @@ import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { useRouter, useLocalSearchParams } from 'expo-router'; import { useRouter, useLocalSearchParams } from 'expo-router';
import { FormikHelpers } from 'formik'; import { FormikHelpers } from 'formik';
import BannerForm, { BannerFormData } from '@/components/BannerForm'; import BannerForm, { BannerFormData } from '@/components/BannerForm';
import { trpc } from '../../../../src/trpc-client'; import { trpc } from '@/src/trpc-client';
interface Banner { interface Banner {
id: number; id: number;

View file

@ -180,7 +180,7 @@ export default function DashboardBanners() {
}; };
const handleEdit = (banner: Banner) => { const handleEdit = (banner: Banner) => {
router.push(`/dashboard-banners/edit-banner/${banner.id}` as any); router.push(`/dashboard-banners/edit/${banner.id}` as any);
}; };
const handleDelete = (id: number) => { const handleDelete = (id: number) => {
@ -207,7 +207,7 @@ export default function DashboardBanners() {
}; };
const handleCreate = () => { const handleCreate = () => {
router.push('/(drawer)/dashboard-banners/create-banner' as any); router.push('/dashboard-banners/create' as any);
}; };
if (isLoading) { if (isLoading) {

View file

@ -6,6 +6,7 @@ import { MyText, tw } from 'common-ui';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import { theme } from 'common-ui/src/theme'; import { theme } from 'common-ui/src/theme';
import { trpc } from '@/src/trpc-client'; import { trpc } from '@/src/trpc-client';
import { useNavigationTarget } from 'common-ui/hooks/useNavigationTarget';
interface MenuItem { interface MenuItem {
title: string; title: string;
@ -16,6 +17,7 @@ interface MenuItem {
iconColor?: string; iconColor?: string;
iconBg?: string; iconBg?: string;
badgeCount?: number; badgeCount?: number;
onPress?: () => void;
} }
interface MenuItemComponentProps { interface MenuItemComponentProps {
@ -27,8 +29,7 @@ const MenuItemComponent: React.FC<MenuItemComponentProps> = ({ item, router }) =
return ( return (
<Pressable <Pressable
key={item.route} onPress={() => item.onPress ? item.onPress() : router.push(item.route as any)}
onPress={() => router.push(item.route as any)}
style={({ pressed }) => [ style={({ pressed }) => [
tw`flex-row items-center p-4 bg-white border border-gray-100 rounded-xl mb-3 shadow-sm`, tw`flex-row items-center p-4 bg-white border border-gray-100 rounded-xl mb-3 shadow-sm`,
pressed && tw`bg-gray-50`, pressed && tw`bg-gray-50`,
@ -55,28 +56,41 @@ const MenuItemComponent: React.FC<MenuItemComponentProps> = ({ item, router }) =
export default function Dashboard() { export default function Dashboard() {
const router = useRouter(); const router = useRouter();
const { setNavigationTarget } = useNavigationTarget();
const { data: essentialsData } = trpc.admin.user.getEssentials.useQuery(); const { data: essentialsData } = trpc.admin.user.getEssentials.useQuery();
const handleManageOrdersPress = () => {
setNavigationTarget('/manage-orders/orders');
router.push('/(drawer)/manage-orders');
};
const handleDeliverySequencesPress = () => {
setNavigationTarget('/manage-orders/delivery-sequences');
router.push('/(drawer)/manage-orders');
};
const menuItems: MenuItem[] = [ const menuItems: MenuItem[] = [
{ {
title: 'Manage Orders', title: 'Manage Orders',
icon: 'shopping-bag', icon: 'shopping-bag',
description: 'View and manage customer orders', description: 'View and manage customer orders',
route: '/(drawer)/orders', route: '/(drawer)/manage-orders',
category: 'orders', category: 'orders',
iconColor: '#10B981', iconColor: '#10B981',
iconBg: '#D1FAE5', iconBg: '#D1FAE5',
onPress: handleManageOrdersPress,
}, },
{ {
title: 'Delivery Sequences', title: 'Delivery Sequences',
icon: 'alt-route', icon: 'alt-route',
description: 'Plan and optimize delivery routes', description: 'Plan and optimize delivery routes',
route: '/(drawer)/delivery-sequences', route: '/manage-orders/delivery-sequences',
category: 'orders', category: 'orders',
iconColor: '#8B5CF6', iconColor: '#8B5CF6',
iconBg: '#EDE9FE', iconBg: '#EDE9FE',
onPress: handleDeliverySequencesPress,
}, },
{ {
title: 'Delivery Slots', title: 'Delivery Slots',
@ -211,8 +225,8 @@ export default function Dashboard() {
<View style={tw`flex-row flex-wrap gap-3`}> <View style={tw`flex-row flex-wrap gap-3`}>
{quickActions.map((item) => ( {quickActions.map((item) => (
<Pressable <Pressable
key={item.route} key={`quick-${item.route}`}
onPress={() => router.push(item.route as any)} onPress={() => item.onPress ? item.onPress() : router.push(item.route as any)}
style={({ pressed }) => [ style={({ pressed }) => [
tw`bg-white rounded-xl p-3 shadow-sm border border-gray-100 items-center`, tw`bg-white rounded-xl p-3 shadow-sm border border-gray-100 items-center`,
{ width: 'calc(25% - 9px)' }, { width: 'calc(25% - 9px)' },
@ -249,7 +263,7 @@ export default function Dashboard() {
</View> </View>
<MyText style={tw`text-gray-700 font-bold text-base`}>{category.title}</MyText> <MyText style={tw`text-gray-700 font-bold text-base`}>{category.title}</MyText>
</View> </View>
{categoryItems.map(item => <MenuItemComponent key={item.route} item={item} router={router} />)} {categoryItems.map(item => <MenuItemComponent key={`menu-${item.route}`} item={item} router={router} />)}
</View> </View>
); );
})} })}

View file

@ -4,6 +4,8 @@ export default function Layout() {
return ( return (
<Stack screenOptions={{ headerShown: false }}> <Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="index" options={{ title: 'Manage Orders' }} /> <Stack.Screen name="index" options={{ title: 'Manage Orders' }} />
<Stack.Screen name="delivery-sequences" options={{ title: 'Delivery Sequences' }} />
<Stack.Screen name="orders" options={{ title: 'Orders' }} />
</Stack> </Stack>
); );
} }

View file

@ -2,8 +2,8 @@ import { Stack } from 'expo-router';
export default function Layout() { export default function Layout() {
return ( return (
<Stack screenOptions={{ headerShown: false }}> <Stack>
<Stack.Screen name="index" options={{ title: 'Delivery Sequences' }} /> <Stack.Screen name="index" options={{ title: 'Delivery Sequences', headerShown: false }} />
</Stack> </Stack>
); );
} }

View file

@ -302,7 +302,7 @@ export default function DeliverySequences() {
// Auto-select first slot if no slotId provided // Auto-select first slot if no slotId provided
useEffect(() => { useEffect(() => {
if (!slotId && slotsData?.slots && slotsData.slots.length > 0) { if (!slotId && slotsData?.slots && slotsData.slots.length > 0) {
router.replace(`/delivery-sequences?slotId=${slotsData.slots[0].id}`); router.replace(`/manage-orders/delivery-sequences?slotId=${slotsData.slots[0].id}`);
} }
}, [slotId, slotsData, router]); }, [slotId, slotsData, router]);
@ -505,7 +505,7 @@ export default function DeliverySequences() {
value={selectedSlotId || ""} value={selectedSlotId || ""}
onValueChange={(val) => { onValueChange={(val) => {
if (val) { if (val) {
router.replace(`/delivery-sequences?slotId=${val}`); router.replace(`/manage-orders/delivery-sequences?slotId=${val}`);
} }
}} }}
placeholder="Select slot" placeholder="Select slot"

View file

@ -1,16 +1,28 @@
import { View, TouchableOpacity, Alert } from 'react-native'; import { View, TouchableOpacity, Alert } from 'react-native';
import { MyText, BottomDropdown, tw, MyFlatList, useMarkDataFetchers } from 'common-ui'; import { MyText, BottomDropdown, tw, MyFlatList, useMarkDataFetchers } from 'common-ui';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import { useFocusEffect } from '@react-navigation/native';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useState } from 'react'; import { useState, useCallback } from 'react';
import MaterialIcons from '@expo/vector-icons/MaterialIcons'; import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { trpc } from '@/src/trpc-client'; import { trpc } from '@/src/trpc-client';
import { useNavigationTarget } from 'common-ui/hooks/useNavigationTarget';
export default function ManageOrders() { export default function ManageOrders() {
const router = useRouter(); const router = useRouter();
const { getNavigationTarget } = useNavigationTarget();
const [selectedSlotId, setSelectedSlotId] = useState<string | null>(null); const [selectedSlotId, setSelectedSlotId] = useState<string | null>(null);
const { data: slotsData, refetch } = trpc.admin.slots.getAll.useQuery(); const { data: slotsData, refetch } = trpc.admin.slots.getAll.useQuery();
useFocusEffect(
useCallback(() => {
const target = getNavigationTarget();
if (target) {
router.replace(target as any);
}
}, [router, getNavigationTarget])
);
useMarkDataFetchers(() => { useMarkDataFetchers(() => {
refetch(); refetch();
}); });
@ -34,7 +46,7 @@ export default function ManageOrders() {
Alert.alert('Flash Deliveries', 'Flash deliveries do not have delivery sequences. Use the Orders menu to manage flash deliveries.'); Alert.alert('Flash Deliveries', 'Flash deliveries do not have delivery sequences. Use the Orders menu to manage flash deliveries.');
return; return;
} }
router.push(`/(drawer)/delivery-sequences?slotId=${selectedSlotId}`); router.push(`/manage-orders/delivery-sequences?slotId=${selectedSlotId}`);
}, },
}, },
{ {
@ -43,9 +55,9 @@ export default function ManageOrders() {
color: 'bg-cyan-500', color: 'bg-cyan-500',
onPress: () => { onPress: () => {
if (selectedSlotId === 'flash') { if (selectedSlotId === 'flash') {
router.push('/(drawer)/orders?filter=flash'); router.push('/manage-orders/orders?filter=flash');
} else { } else {
router.push('/(drawer)/orders'); router.push('/manage-orders/orders');
} }
}, },
}, },

View file

@ -0,0 +1,9 @@
import { Stack } from 'expo-router';
export default function Layout() {
return (
<Stack>
<Stack.Screen name="index" options={{ title: 'Orders', headerShown: false }} />
</Stack>
);
}

View file

@ -1,7 +1,7 @@
import React, { useState , useEffect } from 'react'; import React, { useState , useEffect } from 'react';
import { View, TouchableOpacity, Alert, TextInput, ActivityIndicator, Linking } from 'react-native'; import { View, TouchableOpacity, Alert, TextInput, ActivityIndicator, Linking } from 'react-native';
import { AppContainer, MyText, tw, MyFlatList, BottomDialog, BottomDropdown, Checkbox, theme, MyTextInput } from 'common-ui'; import { AppContainer, MyText, tw, MyFlatList, BottomDialog, BottomDropdown, Checkbox, theme, MyTextInput } from 'common-ui';
import { trpc } from '../../../src/trpc-client'; import { trpc } from '@/src/trpc-client';
import { useRouter, useLocalSearchParams } from 'expo-router'; import { useRouter, useLocalSearchParams } from 'expo-router';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import MaterialIcons from '@expo/vector-icons/MaterialIcons'; import MaterialIcons from '@expo/vector-icons/MaterialIcons';

View file

@ -1,9 +0,0 @@
import { Stack } from 'expo-router';
export default function Layout() {
return (
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="index" options={{ title: 'Orders' }} />
</Stack>
);
}

View file

@ -4,6 +4,8 @@ export default function Layout() {
return ( return (
<Stack screenOptions={{ headerShown: false }}> <Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="index" options={{ title: 'Product Groupings' }} /> <Stack.Screen name="index" options={{ title: 'Product Groupings' }} />
<Stack.Screen name="create" options={{ title: 'Create Product Group' }} />
<Stack.Screen name="edit/[id]" options={{ title: 'Edit Product Group' }} />
</Stack> </Stack>
); );
} }

View file

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { View } from 'react-native'; import { View } from 'react-native';
import { AppContainer } from 'common-ui'; import { AppContainer } from 'common-ui';
import ProductGroupForm from '../../components/ProductGroupForm'; import ProductGroupForm from '../../../components/ProductGroupForm';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
export default function CreateProductGroup() { export default function CreateProductGroup() {

View file

@ -1,9 +1,9 @@
import React from 'react'; import React from 'react';
import { View } from 'react-native'; import { View } from 'react-native';
import { AppContainer, MyText } from 'common-ui'; import { AppContainer, MyText } from 'common-ui';
import ProductGroupForm from '../../../components/ProductGroupForm'; import ProductGroupForm from '../../../../components/ProductGroupForm';
import { useRouter, useLocalSearchParams } from 'expo-router'; import { useRouter, useLocalSearchParams } from 'expo-router';
import { trpc } from '../../../src/trpc-client'; import { trpc } from '@/src/trpc-client';
export default function EditProductGroup() { export default function EditProductGroup() {
const router = useRouter(); const router = useRouter();

View file

@ -143,11 +143,11 @@ export default function ProductGroupings() {
}); });
const handleCreate = () => { const handleCreate = () => {
router.push("/(drawer)/create-product-group"); router.push("/product-groupings/create");
}; };
const handleEdit = (group: ProductGroup) => { const handleEdit = (group: ProductGroup) => {
router.push(`/(drawer)/edit-product-group/${group.id}`); router.push(`/product-groupings/edit/${group.id}`);
}; };
const handleDelete = (id: number) => { const handleDelete = (id: number) => {

View file

@ -198,7 +198,7 @@ export default function SlotDetails() {
{/* FAB for Edit Slot */} {/* FAB for Edit Slot */}
<MyTouchableOpacity <MyTouchableOpacity
onPress={() => router.push(`/edit-slot/${slot.id}` as any)} onPress={() => router.push(`/slots/edit/${slot.id}` as any)}
activeOpacity={0.95} activeOpacity={0.95}
style={{ position: 'absolute', bottom: 32, right: 24, zIndex: 100 }} style={{ position: 'absolute', bottom: 32, right: 24, zIndex: 100 }}
> >

View file

@ -1,6 +1,6 @@
import './notif-job'; import './notif-job';
import { initializeAllStores } from '../stores/store-initializer'; import { initializeAllStores } from '../stores/store-initializer';
import { startOrderHandler, publishOrder } from './post-order-handler'; import { startOrderHandler, startCancellationHandler, publishOrder } from './post-order-handler';
import { deleteOrders } from './delete-orders'; import { deleteOrders } from './delete-orders';
/** /**
@ -9,6 +9,7 @@ import { deleteOrders } from './delete-orders';
* - Role Manager (fetches and caches all roles) * - Role Manager (fetches and caches all roles)
* - Const Store (syncs constants from DB to Redis) * - Const Store (syncs constants from DB to Redis)
* - Post Order Handler (Redis Pub/Sub subscriber) * - Post Order Handler (Redis Pub/Sub subscriber)
* - Cancellation Handler (Redis Pub/Sub subscriber for order cancellations)
* - Other services can be added here in the future * - Other services can be added here in the future
*/ */
export const initFunc = async (): Promise<void> => { export const initFunc = async (): Promise<void> => {
@ -18,6 +19,7 @@ export const initFunc = async (): Promise<void> => {
await Promise.all([ await Promise.all([
initializeAllStores(), initializeAllStores(),
startOrderHandler(), startOrderHandler(),
startCancellationHandler(),
]); ]);
console.log('Application initialization completed successfully'); console.log('Application initialization completed successfully');

View file

@ -1,15 +1,23 @@
import { db } from '../db/db_index'; import { db } from '../db/db_index';
import { orders } from '../db/schema'; import { orders, orderStatus } from '../db/schema';
import redisClient from './redis-client'; import redisClient from './redis-client';
import { sendTelegramMessage } from './telegram-service'; import { sendTelegramMessage } from './telegram-service';
import { inArray } from 'drizzle-orm'; import { inArray, eq } from 'drizzle-orm';
const ORDER_CHANNEL = 'orders:placed'; const ORDER_CHANNEL = 'orders:placed';
const CANCELLED_CHANNEL = 'orders:cancelled';
interface OrderIdMessage { interface OrderIdMessage {
orderIds: number[]; orderIds: number[];
} }
interface CancellationMessage {
orderId: number;
cancelledBy: 'user' | 'admin';
reason: string;
cancelledAt: string;
}
const formatDateTime = (dateStr: string | null | undefined): string => { const formatDateTime = (dateStr: string | null | undefined): string => {
if (!dateStr) return 'N/A'; if (!dateStr) return 'N/A';
return new Date(dateStr).toLocaleString('en-IN', { return new Date(dateStr).toLocaleString('en-IN', {
@ -55,6 +63,28 @@ const formatOrderMessageWithFullData = (ordersData: any[]): string => {
return message; return message;
}; };
const formatCancellationMessage = (orderData: any, cancellationData: CancellationMessage): string => {
const message = `❌ <b>Order Cancelled</b>
<b>Order #${orderData.id}</b>
👤 <b>Name:</b> ${orderData.address?.name || 'N/A'}
📞 <b>Phone:</b> ${orderData.address?.phone || 'N/A'}
📦 <b>Items:</b>
${orderData.orderItems?.map((item: any) => `${item.product?.name || 'Unknown'} x${item.quantity}`).join('\n') || ' N/A'}
💰 <b>Total:</b> ${orderData.totalAmount}
💳 <b>Refund:</b> ${orderData.refundStatus === 'na' ? 'N/A (COD)' : orderData.refundStatus || 'Pending'}
<b>Reason:</b> ${cancellationData.reason}
👤 <b>Cancelled by:</b> ${cancellationData.cancelledBy === 'admin' ? 'Admin' : 'User'}
<b>Time:</b> ${formatDateTime(cancellationData.cancelledAt)}
`;
return message;
};
/** /**
* Start the post order handler * Start the post order handler
* Subscribes to the orders:placed channel and sends to Telegram * Subscribes to the orders:placed channel and sends to Telegram
@ -104,6 +134,56 @@ export const stopOrderHandler = async (): Promise<void> => {
} }
}; };
export const startCancellationHandler = async (): Promise<void> => {
try {
console.log('Starting cancellation handler...');
await redisClient.subscribe(CANCELLED_CHANNEL, async (message: string) => {
try {
const cancellationData: CancellationMessage = JSON.parse(message);
console.log('Order cancellation received, sending to Telegram...');
const orderData = await db.query.orders.findFirst({
where: eq(orders.id, cancellationData.orderId),
with: {
address: true,
orderItems: { with: { product: true } },
refunds: true,
},
});
if (!orderData) {
console.error('Order not found for cancellation:', cancellationData.orderId);
await sendTelegramMessage(`⚠️ Order ${cancellationData.orderId} was cancelled but could not be found in database`);
return;
}
const refundStatus = orderData.refunds?.[0]?.refundStatus || 'pending';
const telegramMessage = formatCancellationMessage({ ...orderData, refundStatus }, cancellationData);
await sendTelegramMessage(telegramMessage);
} catch (error) {
console.error('Failed to process cancellation message:', error);
await sendTelegramMessage(`⚠️ Error processing cancellation: ${message}`);
}
});
console.log('Cancellation handler started successfully');
} catch (error) {
console.error('Failed to start cancellation handler:', error);
throw error;
}
};
export const stopCancellationHandler = async (): Promise<void> => {
try {
await redisClient.unsubscribe(CANCELLED_CHANNEL);
console.log('Cancellation handler stopped');
} catch (error) {
console.error('Error stopping cancellation handler:', error);
}
};
export const publishOrder = async (orderDetails: OrderIdMessage): Promise<boolean> => { export const publishOrder = async (orderDetails: OrderIdMessage): Promise<boolean> => {
try { try {
const message = JSON.stringify(orderDetails); const message = JSON.stringify(orderDetails);
@ -127,3 +207,24 @@ export const publishFormattedOrder = async (
return false; return false;
} }
}; };
export const publishCancellation = async (
orderId: number,
cancelledBy: 'user' | 'admin',
reason: string
): Promise<boolean> => {
try {
const message: CancellationMessage = {
orderId,
cancelledBy,
reason,
cancelledAt: new Date().toISOString(),
};
await redisClient.publish(CANCELLED_CHANNEL, JSON.stringify(message));
console.log('Cancellation published to Redis:', orderId);
return true;
} catch (error) {
console.error('Failed to publish cancellation:', error);
return false;
}
};

View file

@ -19,6 +19,7 @@ import {
sendOrderPackagedNotification, sendOrderPackagedNotification,
sendOrderDeliveredNotification, sendOrderDeliveredNotification,
} from "../../lib/notif-job"; } from "../../lib/notif-job";
import { publishCancellation } from "../../lib/post-order-handler";
const updateOrderNotesSchema = z.object({ const updateOrderNotesSchema = z.object({
orderId: z.number(), orderId: z.number(),
@ -955,6 +956,9 @@ export const orderRouter = router({
return { orderId: order.id, userId: order.userId }; return { orderId: order.id, userId: order.userId };
}); });
// Publish to Redis for Telegram notification
await publishCancellation(result.orderId, 'admin', reason);
return { success: true, message: "Order cancelled successfully" }; return { success: true, message: "Order cancelled successfully" };
}), }),
}); });

View file

@ -25,7 +25,7 @@ import {
import { RazorpayPaymentService } from "../../lib/payments-utils"; import { RazorpayPaymentService } from "../../lib/payments-utils";
import { getNextDeliveryDate } from "../common-apis/common"; import { getNextDeliveryDate } from "../common-apis/common";
import { CONST_KEYS, getConstant, getConstants } from "../../lib/const-store"; import { CONST_KEYS, getConstant, getConstants } from "../../lib/const-store";
import { publishFormattedOrder } from "../../lib/post-order-handler"; import { publishFormattedOrder, publishCancellation } from "../../lib/post-order-handler";
const validateAndGetCoupon = async ( const validateAndGetCoupon = async (
@ -785,6 +785,9 @@ export const orderRouter = router({
result.orderId.toString() 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) {
console.log(e); console.log(e);