freshyo/verifier/user-apis/apis/order.ts
2026-03-22 20:20:18 +05:30

979 lines
29 KiB
TypeScript

import { router, protectedProcedure } from "@/src/trpc/trpc-index";
import { z } from "zod";
import { db } from "@/src/db/db_index";
import {
orders,
orderItems,
orderStatus,
addresses,
productInfo,
paymentInfoTable,
coupons,
couponUsage,
cartItems,
refunds,
units,
userDetails,
} from "@/src/db/schema";
import { eq, and, inArray, desc, gte, lte } from "drizzle-orm";
import { scaffoldAssetUrl } from "@/src/lib/s3-client";
import { ApiError } from "@/src/lib/api-error";
import {
sendOrderPlacedNotification,
sendOrderCancelledNotification,
} from "@/src/lib/notif-job";
import { getNextDeliveryDate } from "@/src/trpc/apis/common-apis/common";
import { CONST_KEYS, getConstant, getConstants } from "@/src/lib/const-store";
import { publishFormattedOrder, publishCancellation } from "@/src/lib/post-order-handler";
import { getSlotById } from "@/src/stores/slot-store";
const validateAndGetCoupon = async (
couponId: number | undefined,
userId: number,
totalAmount: number
) => {
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: {
userId: number;
selectedItems: Array<{
productId: number;
quantity: number;
slotId: number | null;
}>;
addressId: number;
paymentMethod: "online" | "cod";
couponId?: number;
userNotes?: string;
isFlash?: boolean;
}) => {
const {
userId,
selectedItems,
addressId,
paymentMethod,
couponId,
userNotes,
} = params;
const constants = await getConstants<number>([
CONST_KEYS.minRegularOrderValue,
CONST_KEYS.deliveryCharge,
CONST_KEYS.flashFreeDeliveryThreshold,
CONST_KEYS.flashDeliveryCharge,
]);
const isFlashDelivery = params.isFlash;
const minOrderValue = (isFlashDelivery ? constants[CONST_KEYS.flashFreeDeliveryThreshold] : constants[CONST_KEYS.minRegularOrderValue]) || 0;
const deliveryCharge = (isFlashDelivery ? constants[CONST_KEYS.flashDeliveryCharge] : constants[CONST_KEYS.deliveryCharge]) || 0;
const orderGroupId = `${Date.now()}-${userId}`;
const address = await db.query.addresses.findFirst({
where: and(eq(addresses.userId, userId), eq(addresses.id, addressId)),
});
if (!address) {
throw new ApiError("Invalid address", 400);
}
const ordersBySlot = new Map<
number | null,
Array<{
productId: number;
quantity: number;
slotId: number | null;
product: any;
}>
>();
for (const item of selectedItems) {
const product = await db.query.productInfo.findFirst({
where: eq(productInfo.id, item.productId),
});
if (!product) {
throw new ApiError(`Product ${item.productId} not found`, 400);
}
if (!ordersBySlot.has(item.slotId)) {
ordersBySlot.set(item.slotId, []);
}
ordersBySlot.get(item.slotId)!.push({ ...item, product });
}
if (params.isFlash) {
for (const item of selectedItems) {
const product = await db.query.productInfo.findFirst({
where: eq(productInfo.id, item.productId),
});
if (!product?.isFlashAvailable) {
throw new ApiError(`Product ${item.productId} is not available for flash delivery`, 400);
}
}
}
let totalAmount = 0;
for (const [slotId, items] of ordersBySlot) {
const orderTotal = items.reduce(
(sum, item) => {
const itemPrice = params.isFlash
? parseFloat((item.product.flashPrice || item.product.price).toString())
: parseFloat(item.product.price.toString());
return sum + itemPrice * item.quantity;
},
0
);
totalAmount += orderTotal;
}
const appliedCoupon = await validateAndGetCoupon(couponId, userId, totalAmount);
const expectedDeliveryCharge =
totalAmount < minOrderValue ? deliveryCharge : 0;
const totalWithDelivery = totalAmount + expectedDeliveryCharge;
type OrderData = {
order: Omit<typeof orders.$inferInsert, "id">;
orderItems: Omit<typeof orderItems.$inferInsert, "id">[];
orderStatus: Omit<typeof orderStatus.$inferInsert, "id">;
};
const ordersData: OrderData[] = [];
let isFirstOrder = true;
for (const [slotId, items] of ordersBySlot) {
const subOrderTotal = items.reduce(
(sum, item) => {
const itemPrice = params.isFlash
? parseFloat((item.product.flashPrice || item.product.price).toString())
: parseFloat(item.product.price.toString());
return sum + itemPrice * item.quantity;
},
0
);
const subOrderTotalWithDelivery = subOrderTotal + expectedDeliveryCharge;
const orderGroupProportion = subOrderTotal / totalAmount;
const orderTotalAmount = isFirstOrder ? subOrderTotalWithDelivery : subOrderTotal;
const { finalOrderTotal: finalOrderAmount } = applyDiscountToOrder(
orderTotalAmount,
appliedCoupon,
orderGroupProportion
);
const order: Omit<typeof orders.$inferInsert, "id"> = {
userId,
addressId,
slotId: params.isFlash ? null : slotId,
isCod: paymentMethod === "cod",
isOnlinePayment: paymentMethod === "online",
paymentInfoId: null,
totalAmount: finalOrderAmount.toString(),
deliveryCharge: isFirstOrder ? expectedDeliveryCharge.toString() : "0",
readableId: -1,
userNotes: userNotes || null,
orderGroupId,
orderGroupProportion: orderGroupProportion.toString(),
isFlashDelivery: params.isFlash,
};
const orderItemsData: Omit<typeof orderItems.$inferInsert, "id">[] = items.map(
(item) => ({
orderId: 0,
productId: item.productId,
quantity: item.quantity.toString(),
price: params.isFlash
? item.product.flashPrice || item.product.price
: item.product.price,
discountedPrice: (
params.isFlash
? item.product.flashPrice || item.product.price
: item.product.price
).toString(),
})
);
const orderStatusData: Omit<typeof orderStatus.$inferInsert, "id"> = {
userId,
orderId: 0,
paymentStatus: paymentMethod === "cod" ? "cod" : "pending",
};
ordersData.push({ order, orderItems: orderItemsData, orderStatus: orderStatusData });
isFirstOrder = false;
}
const createdOrders = await db.transaction(async (tx) => {
let sharedPaymentInfoId: number | null = null;
if (paymentMethod === "online") {
const [paymentInfo] = await tx
.insert(paymentInfoTable)
.values({
status: "pending",
gateway: "razorpay",
merchantOrderId: `multi_order_${Date.now()}`,
})
.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) {
}
return insertedOrders;
});
await db.delete(cartItems).where(
and(
eq(cartItems.userId, userId),
inArray(
cartItems.productId,
selectedItems.map((item) => item.productId)
)
)
);
if (appliedCoupon && createdOrders.length > 0) {
await db.insert(couponUsage).values({
userId,
couponId: appliedCoupon.id,
orderId: createdOrders[0].id as number,
orderItemId: null,
usedAt: new Date(),
});
}
for (const order of createdOrders) {
sendOrderPlacedNotification(userId, order.id.toString());
}
await publishFormattedOrder(createdOrders, ordersBySlot);
return { success: true, data: createdOrders };
};
export const orderRouter = router({
placeOrder: protectedProcedure
.input(
z.object({
selectedItems: z.array(
z.object({
productId: z.number().int().positive(),
quantity: z.number().int().positive(),
slotId: z.union([z.number().int(), z.null()]),
})
),
addressId: z.number().int().positive(),
paymentMethod: z.enum(["online", "cod"]),
couponId: z.number().int().positive().optional(),
userNotes: z.string().optional(),
isFlashDelivery: z.boolean().optional().default(false),
})
)
.mutation(async ({ input, ctx }) => {
const userId = ctx.user.userId;
// Check if user is suspended from placing orders
const userDetail = await db.query.userDetails.findFirst({
where: eq(userDetails.userId, userId),
});
if (userDetail?.isSuspended) {
throw new ApiError("Unable to place order", 403);
}
const {
selectedItems,
addressId,
paymentMethod,
couponId,
userNotes,
isFlashDelivery,
} = input;
// Check if flash delivery is enabled when placing a flash delivery order
if (isFlashDelivery) {
const isFlashDeliveryEnabled = await getConstant<boolean>(CONST_KEYS.isFlashDeliveryEnabled);
if (!isFlashDeliveryEnabled) {
throw new ApiError("Flash delivery is currently unavailable. Please opt for scheduled delivery.", 403);
}
}
// Check if any selected slot is at full capacity (only for regular delivery)
if (!isFlashDelivery) {
const slotIds = [...new Set(selectedItems.filter(i => i.slotId !== null).map(i => i.slotId as number))];
for (const slotId of slotIds) {
const slot = await getSlotById(slotId);
if (slot?.isCapacityFull) {
throw new ApiError("Selected delivery slot is at full capacity. Please choose another slot.", 403);
}
}
}
let processedItems = selectedItems;
// Handle flash delivery slot resolution
if (isFlashDelivery) {
// For flash delivery, set slotId to null (no specific slot assigned)
processedItems = selectedItems.map(item => ({
...item,
slotId: null as any, // Type override for flash delivery
}));
}
return await placeOrderUtil({
userId,
selectedItems: processedItems,
addressId,
paymentMethod,
couponId,
userNotes,
isFlash: isFlashDelivery,
});
}),
getOrders: protectedProcedure
.input(
z
.object({
page: z.number().min(1).default(1),
pageSize: z.number().min(1).max(50).default(10),
})
.optional()
)
.query(async ({ input, ctx }) => {
const { page = 1, pageSize = 10 } = input || {};
const userId = ctx.user.userId;
const offset = (page - 1) * pageSize;
// Get total count for pagination
const totalCountResult = await db.$count(
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(
userOrders.map(async (order) => {
const status = order.orderStatus[0];
const refund = order.refunds[0];
type DeliveryStatus = "cancelled" | "success" | "pending" | "packaged";
type OrderStatus = "cancelled" | "success";
let deliveryStatus: DeliveryStatus;
let orderStatus: OrderStatus;
const allItemsPackaged = order.orderItems.every(
(item) => item.is_packaged
);
if (status?.isCancelled) {
deliveryStatus = "cancelled";
orderStatus = "cancelled";
} else if (status?.isDelivered) {
deliveryStatus = "success";
orderStatus = "success";
} else if (allItemsPackaged) {
deliveryStatus = "packaged";
orderStatus = "success";
} else {
deliveryStatus = "pending";
orderStatus = "success";
}
const paymentMode = order.isCod ? "CoD" : "Online";
const paymentStatus = status?.paymentStatus || "pending";
const refundStatus = refund?.refundStatus || "none";
const refundAmount = refund?.refundAmount
? parseFloat(refund.refundAmount.toString())
: null;
const items = await Promise.all(
order.orderItems.map(async (item) => {
const signedImages = item.product.images
? scaffoldAssetUrl(
item.product.images as string[]
)
: [];
return {
productName: item.product.name,
quantity: parseFloat(item.quantity),
price: parseFloat(item.price.toString()),
discountedPrice: parseFloat(
item.discountedPrice?.toString() || item.price.toString()
),
amount:
parseFloat(item.price.toString()) * parseFloat(item.quantity),
image: signedImages[0] || null,
};
})
);
return {
id: order.id,
orderId: `ORD${order.id}`,
orderDate: order.createdAt.toISOString(),
deliveryStatus,
deliveryDate: order.slot?.deliveryTime.toISOString(),
orderStatus,
cancelReason: status?.cancelReason || null,
paymentMode,
totalAmount: Number(order.totalAmount),
deliveryCharge: Number(order.deliveryCharge),
paymentStatus,
refundStatus,
refundAmount,
userNotes: order.userNotes || null,
items,
isFlashDelivery: order.isFlashDelivery,
createdAt: order.createdAt.toISOString(),
};
})
);
return {
success: true,
data: mappedOrders,
pagination: {
page,
pageSize,
totalCount,
totalPages: Math.ceil(totalCount / pageSize),
},
};
}),
getOrderById: protectedProcedure
.input(z.object({ orderId: z.string() }))
.query(async ({ input, ctx }) => {
const { orderId } = input;
const userId = ctx.user.userId;
const order = await db.query.orders.findFirst({
where: and(eq(orders.id, parseInt(orderId)), eq(orders.userId, userId)),
with: {
orderItems: {
with: {
product: true,
},
},
slot: true,
paymentInfo: true,
orderStatus: {
with: {
refundCoupon: true,
},
},
refunds: true,
},
});
if (!order) {
throw new Error("Order not found");
}
// Get coupon usage for this specific order using new orderId field
const couponUsageData = await db.query.couponUsage.findMany({
where: eq(couponUsage.orderId, order.id), // Use new orderId field
with: {
coupon: true,
},
});
let couponData = null;
if (couponUsageData.length > 0) {
// Calculate total discount from multiple coupons
let totalDiscountAmount = 0;
const orderTotal = parseFloat(order.totalAmount.toString());
for (const usage of couponUsageData) {
let discountAmount = 0;
if (usage.coupon.discountPercent) {
discountAmount =
(orderTotal *
parseFloat(usage.coupon.discountPercent.toString())) /
100;
} else if (usage.coupon.flatDiscount) {
discountAmount = parseFloat(usage.coupon.flatDiscount.toString());
}
// Apply max value limit if set
if (
usage.coupon.maxValue &&
discountAmount > parseFloat(usage.coupon.maxValue.toString())
) {
discountAmount = parseFloat(usage.coupon.maxValue.toString());
}
totalDiscountAmount += discountAmount;
}
couponData = {
couponCode: couponUsageData
.map((u) => u.coupon.couponCode)
.join(", "),
couponDescription: `${couponUsageData.length} coupons applied`,
discountAmount: totalDiscountAmount,
};
}
const status = order.orderStatus[0];
const refund = order.refunds[0];
type DeliveryStatus = "cancelled" | "success" | "pending" | "packaged";
type OrderStatus = "cancelled" | "success";
let deliveryStatus: DeliveryStatus;
let orderStatus: OrderStatus;
const allItemsPackaged = order.orderItems.every(
(item) => item.is_packaged
);
if (status?.isCancelled) {
deliveryStatus = "cancelled";
orderStatus = "cancelled";
} else if (status?.isDelivered) {
deliveryStatus = "success";
orderStatus = "success";
} else if (allItemsPackaged) {
deliveryStatus = "packaged";
orderStatus = "success";
} else {
deliveryStatus = "pending";
orderStatus = "success";
}
const paymentMode = order.isCod ? "CoD" : "Online";
const paymentStatus = status?.paymentStatus || "pending";
const refundStatus = refund?.refundStatus || "none";
const refundAmount = refund?.refundAmount
? parseFloat(refund.refundAmount.toString())
: null;
const items = await Promise.all(
order.orderItems.map(async (item) => {
const signedImages = item.product.images
? scaffoldAssetUrl(
item.product.images as string[]
)
: [];
return {
productName: item.product.name,
quantity: parseFloat(item.quantity),
price: parseFloat(item.price.toString()),
discountedPrice: parseFloat(
item.discountedPrice?.toString() || item.price.toString()
),
amount:
parseFloat(item.price.toString()) * parseFloat(item.quantity),
image: signedImages[0] || null,
};
})
);
return {
id: order.id,
orderId: `ORD${order.id}`,
orderDate: order.createdAt.toISOString(),
deliveryStatus,
deliveryDate: order.slot?.deliveryTime.toISOString(),
orderStatus: order.orderStatus,
cancellationStatus: orderStatus,
cancelReason: status?.cancelReason || null,
paymentMode,
paymentStatus,
refundStatus,
refundAmount,
userNotes: order.userNotes || null,
items,
couponCode: couponData?.couponCode || null,
couponDescription: couponData?.couponDescription || null,
discountAmount: couponData?.discountAmount || null,
orderAmount: parseFloat(order.totalAmount.toString()),
isFlashDelivery: order.isFlashDelivery,
createdAt: order.createdAt.toISOString(),
};
}),
cancelOrder: protectedProcedure
.input(
z.object({
// id: z.string().regex(/^ORD\d+$/, "Invalid order ID format"),
id: z.number(),
reason: z.string().min(1, "Cancellation reason is required"),
})
)
.mutation(async ({ input, ctx }) => {
try {
const userId = ctx.user.userId;
const { id, reason } = input;
// Check if order exists and belongs to user
const order = await db.query.orders.findFirst({
where: eq(orders.id, Number(id)),
with: {
orderStatus: true,
},
});
if (!order) {
console.error("Order not found:", id);
throw new ApiError("Order not found", 404);
}
if (order.userId !== userId) {
console.error("Order does not belong to user:", {
orderId: id,
orderUserId: order.userId,
requestUserId: userId,
});
throw new ApiError("Order not found", 404);
}
const status = order.orderStatus[0];
if (!status) {
console.error("Order status not found for order:", id);
throw new ApiError("Order status not found", 400);
}
if (status.isCancelled) {
console.error("Order is already cancelled:", id);
throw new ApiError("Order is already cancelled", 400);
}
if (status.isDelivered) {
console.error("Cannot cancel delivered order:", id);
throw new ApiError("Cannot cancel delivered order", 400);
}
// Perform database operations in transaction
const result = await db.transaction(async (tx) => {
// Update order status
await tx
.update(orderStatus)
.set({
isCancelled: true,
cancelReason: reason,
cancellationUserNotes: reason,
cancellationReviewed: false,
})
.where(eq(orderStatus.id, status.id));
// Determine refund status based on payment method
const refundStatus = order.isCod ? "na" : "pending";
// Insert refund record
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" };
} catch (e) {
console.log(e);
throw new ApiError("failed to cancel order");
}
}),
updateUserNotes: protectedProcedure
.input(
z.object({
id: z.number(),
userNotes: z.string(),
})
)
.mutation(async ({ input, ctx }) => {
const userId = ctx.user.userId;
const { id, userNotes } = input;
// Extract readable ID from orderId (e.g., ORD001 -> 1)
// 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) {
console.error("Order not found:", id);
throw new ApiError("Order not found", 404);
}
if (order.userId !== userId) {
console.error("Order does not belong to user:", {
orderId: id,
orderUserId: order.userId,
requestUserId: userId,
});
throw new ApiError("Order not found", 404);
}
const status = order.orderStatus[0];
if (!status) {
console.error("Order status not found for order:", id);
throw new ApiError("Order status not found", 400);
}
// Only allow updating notes for orders that are not delivered or cancelled
if (status.isDelivered) {
console.error("Cannot update notes for delivered order:", id);
throw new ApiError("Cannot update notes for delivered order", 400);
}
if (status.isCancelled) {
console.error("Cannot update notes for cancelled order:", id);
throw new ApiError("Cannot update notes for cancelled order", 400);
}
// Update user notes
await db
.update(orders)
.set({
userNotes: userNotes || null,
})
.where(eq(orders.id, order.id));
return { success: true, message: "Notes updated successfully" };
}),
getRecentlyOrderedProducts: protectedProcedure
.input(
z
.object({
limit: z.number().min(1).max(50).default(20),
})
.optional()
)
.query(async ({ input, ctx }) => {
const { limit = 20 } = input || {};
const userId = ctx.user.userId;
// Get user's recent delivered orders (last 30 days)
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const recentOrders = await db
.select({ id: orders.id })
.from(orders)
.innerJoin(orderStatus, eq(orders.id, orderStatus.orderId))
.where(
and(
eq(orders.userId, userId),
eq(orderStatus.isDelivered, true),
gte(orders.createdAt, thirtyDaysAgo)
)
)
.orderBy(desc(orders.createdAt))
.limit(10); // Get last 10 orders
if (recentOrders.length === 0) {
return { success: true, products: [] };
}
const orderIds = recentOrders.map((order) => order.id);
// Get unique product IDs from recent orders
const orderItemsResult = await db
.select({ productId: orderItems.productId })
.from(orderItems)
.where(inArray(orderItems.orderId, orderIds));
const productIds = [
...new Set(orderItemsResult.map((item) => item.productId)),
];
if (productIds.length === 0) {
return { success: true, products: [] };
}
// Get product details
const productsWithUnits = await db
.select({
id: productInfo.id,
name: productInfo.name,
shortDescription: productInfo.shortDescription,
price: productInfo.price,
images: productInfo.images,
isOutOfStock: productInfo.isOutOfStock,
unitShortNotation: units.shortNotation,
incrementStep: productInfo.incrementStep,
})
.from(productInfo)
.innerJoin(units, eq(productInfo.unitId, units.id))
.where(
and(
inArray(productInfo.id, productIds),
eq(productInfo.isSuspended, false)
)
)
.orderBy(desc(productInfo.createdAt))
.limit(limit);
// Generate signed URLs for product images
const formattedProducts = await Promise.all(
productsWithUnits.map(async (product) => {
const nextDeliveryDate = await getNextDeliveryDate(product.id);
return {
id: product.id,
name: product.name,
shortDescription: product.shortDescription,
price: product.price,
unit: product.unitShortNotation,
incrementStep: product.incrementStep,
isOutOfStock: product.isOutOfStock,
nextDeliveryDate: nextDeliveryDate
? nextDeliveryDate.toISOString()
: null,
images: scaffoldAssetUrl(
(product.images as string[]) || []
),
};
})
);
return {
success: true,
products: formattedProducts,
};
}),
});