758 lines
22 KiB
TypeScript
758 lines
22 KiB
TypeScript
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
|
|
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 { imageUploadS3, scaffoldAssetUrl, getOriginalUrlFromSignedUrl, claimUploadUrl } from '@/src/lib/s3-client'
|
|
import { deleteS3Image } from '@/src/lib/delete-image'
|
|
import type { SpecialDeal } from '@/src/db/types'
|
|
import { scheduleStoreInitialization } from '@/src/stores/store-initializer'
|
|
|
|
|
|
type CreateDeal = {
|
|
quantity: number;
|
|
price: number;
|
|
validTill: string;
|
|
};
|
|
|
|
export const productRouter = router({
|
|
getProducts: protectedProcedure
|
|
.query(async ({ ctx }) => {
|
|
const products = await db.query.productInfo.findMany({
|
|
orderBy: productInfo.name,
|
|
with: {
|
|
unit: true,
|
|
store: true,
|
|
},
|
|
});
|
|
|
|
// Generate signed URLs for all product images
|
|
const productsWithSignedUrls = await Promise.all(
|
|
products.map(async (product) => ({
|
|
...product,
|
|
images: scaffoldAssetUrl((product.images as string[]) || []),
|
|
}))
|
|
);
|
|
|
|
return {
|
|
products: productsWithSignedUrls,
|
|
count: productsWithSignedUrls.length,
|
|
};
|
|
}),
|
|
|
|
getProductById: protectedProcedure
|
|
.input(z.object({
|
|
id: z.number(),
|
|
}))
|
|
.query(async ({ input, ctx }) => {
|
|
const { id } = input;
|
|
|
|
const product = await db.query.productInfo.findFirst({
|
|
where: eq(productInfo.id, id),
|
|
with: {
|
|
unit: true,
|
|
},
|
|
});
|
|
|
|
if (!product) {
|
|
throw new ApiError("Product not found", 404);
|
|
}
|
|
|
|
// Fetch special deals for this product
|
|
const deals = await db.query.specialDeals.findMany({
|
|
where: eq(specialDeals.productId, id),
|
|
orderBy: specialDeals.quantity,
|
|
});
|
|
|
|
// Fetch associated tags for this product
|
|
const productTagsData = await db.query.productTags.findMany({
|
|
where: eq(productTags.productId, id),
|
|
with: {
|
|
tag: true,
|
|
},
|
|
});
|
|
|
|
// Generate signed URLs for product images
|
|
const productWithSignedUrls = {
|
|
...product,
|
|
images: scaffoldAssetUrl((product.images as string[]) || []),
|
|
deals,
|
|
tags: productTagsData.map(pt => pt.tag),
|
|
};
|
|
|
|
return {
|
|
product: productWithSignedUrls,
|
|
};
|
|
}),
|
|
|
|
deleteProduct: protectedProcedure
|
|
.input(z.object({
|
|
id: z.number(),
|
|
}))
|
|
.mutation(async ({ input, ctx }) => {
|
|
const { id } = input;
|
|
|
|
const [deletedProduct] = await db
|
|
.delete(productInfo)
|
|
.where(eq(productInfo.id, id))
|
|
.returning();
|
|
|
|
if (!deletedProduct) {
|
|
throw new ApiError("Product not found", 404);
|
|
}
|
|
|
|
// Reinitialize stores to reflect changes
|
|
scheduleStoreInitialization()
|
|
|
|
return {
|
|
message: "Product deleted successfully",
|
|
};
|
|
}),
|
|
|
|
createProduct: protectedProcedure
|
|
.input(z.object({
|
|
name: z.string().min(1),
|
|
shortDescription: z.string().optional(),
|
|
longDescription: z.string().optional(),
|
|
unitId: z.number(),
|
|
storeId: z.number(),
|
|
price: z.number(),
|
|
marketPrice: z.number().optional(),
|
|
incrementStep: z.number().default(1),
|
|
productQuantity: z.number().default(1),
|
|
isSuspended: z.boolean().default(false),
|
|
isFlashAvailable: z.boolean().default(false),
|
|
flashPrice: z.number().optional(),
|
|
deals: z.array(z.object({
|
|
quantity: z.number(),
|
|
price: z.number(),
|
|
validTill: z.string(),
|
|
})).optional(),
|
|
tagIds: z.array(z.number()).optional(),
|
|
imageKeys: z.array(z.string()).optional(),
|
|
}))
|
|
.mutation(async ({ input }) => {
|
|
const {
|
|
name, shortDescription, longDescription, unitId, storeId,
|
|
price, marketPrice, incrementStep, productQuantity,
|
|
isSuspended, isFlashAvailable, flashPrice,
|
|
deals, tagIds, imageKeys
|
|
} = input;
|
|
|
|
// Validation
|
|
if (!name || !unitId || !storeId || !price) {
|
|
throw new ApiError("Name, unitId, storeId, and price are required", 400);
|
|
}
|
|
|
|
// Check for duplicate name
|
|
const existingProduct = await db.query.productInfo.findFirst({
|
|
where: eq(productInfo.name, name.trim()),
|
|
});
|
|
if (existingProduct) {
|
|
throw new ApiError("A product with this name already exists", 400);
|
|
}
|
|
|
|
// Check if unit exists
|
|
const unit = await db.query.units.findFirst({
|
|
where: eq(units.id, unitId),
|
|
});
|
|
if (!unit) {
|
|
throw new ApiError("Invalid unit ID", 400);
|
|
}
|
|
|
|
console.log(imageKeys)
|
|
const [newProduct] = await db
|
|
.insert(productInfo)
|
|
.values({
|
|
name: name.trim(),
|
|
shortDescription,
|
|
longDescription,
|
|
unitId,
|
|
storeId,
|
|
price: price.toString(),
|
|
marketPrice: marketPrice?.toString(),
|
|
incrementStep,
|
|
productQuantity,
|
|
isSuspended,
|
|
isFlashAvailable,
|
|
flashPrice: flashPrice?.toString(),
|
|
images: imageKeys || [],
|
|
})
|
|
.returning();
|
|
|
|
// Handle deals
|
|
if (deals && deals.length > 0) {
|
|
const dealInserts = deals.map(deal => ({
|
|
productId: newProduct.id,
|
|
quantity: deal.quantity.toString(),
|
|
price: deal.price.toString(),
|
|
validTill: new Date(deal.validTill),
|
|
}));
|
|
await db.insert(specialDeals).values(dealInserts);
|
|
}
|
|
|
|
// Handle tags
|
|
if (tagIds && tagIds.length > 0) {
|
|
const tagAssociations = tagIds.map(tagId => ({
|
|
productId: newProduct.id,
|
|
tagId,
|
|
}));
|
|
await db.insert(productTags).values(tagAssociations);
|
|
}
|
|
|
|
// Claim upload URLs
|
|
if (imageKeys && imageKeys.length > 0) {
|
|
for (const key of imageKeys) {
|
|
try {
|
|
await claimUploadUrl(key);
|
|
} catch (e) {
|
|
console.warn(`Failed to claim upload URL for key: ${key}`, e);
|
|
}
|
|
}
|
|
}
|
|
|
|
scheduleStoreInitialization();
|
|
|
|
return {
|
|
product: newProduct,
|
|
message: "Product created successfully",
|
|
};
|
|
}),
|
|
|
|
updateProduct: protectedProcedure
|
|
.input(z.object({
|
|
id: z.number(),
|
|
name: z.string().min(1).optional(),
|
|
shortDescription: z.string().optional(),
|
|
longDescription: z.string().optional(),
|
|
unitId: z.number().optional(),
|
|
storeId: z.number().optional(),
|
|
price: z.number().optional(),
|
|
marketPrice: z.number().optional(),
|
|
incrementStep: z.number().optional(),
|
|
productQuantity: z.number().optional(),
|
|
isSuspended: z.boolean().optional(),
|
|
isFlashAvailable: z.boolean().optional(),
|
|
flashPrice: z.number().optional(),
|
|
deals: z.array(z.object({
|
|
quantity: z.number(),
|
|
price: z.number(),
|
|
validTill: z.string(),
|
|
})).optional(),
|
|
tagIds: z.array(z.number()).optional(),
|
|
newImageKeys: z.array(z.string()).optional(),
|
|
imagesToDelete: z.array(z.string()).optional(),
|
|
}))
|
|
.mutation(async ({ input }) => {
|
|
const { id, newImageKeys, imagesToDelete, deals, tagIds, ...updateData } = input;
|
|
|
|
// Get current product
|
|
const currentProduct = await db.query.productInfo.findFirst({
|
|
where: eq(productInfo.id, id),
|
|
});
|
|
if (!currentProduct) {
|
|
throw new ApiError("Product not found", 404);
|
|
}
|
|
|
|
// Handle image deletions
|
|
let currentImages = (currentProduct.images as string[]) || [];
|
|
if (imagesToDelete && imagesToDelete.length > 0) {
|
|
for (const imageUrl of imagesToDelete) {
|
|
try {
|
|
await deleteS3Image(imageUrl);
|
|
} catch (e) {
|
|
console.error(`Failed to delete image: ${imageUrl}`, e);
|
|
}
|
|
}
|
|
currentImages = currentImages.filter(img => {
|
|
//!imagesToDelete.includes(img)
|
|
const isRemoved = imagesToDelete.some(item => item.includes(img));
|
|
return !isRemoved;
|
|
});
|
|
}
|
|
|
|
// Add new images
|
|
if (newImageKeys && newImageKeys.length > 0) {
|
|
currentImages = [...currentImages, ...newImageKeys];
|
|
|
|
for (const key of newImageKeys) {
|
|
try {
|
|
await claimUploadUrl(key);
|
|
} catch (e) {
|
|
console.warn(`Failed to claim upload URL for key: ${key}`, e);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update product - convert numeric fields to strings for PostgreSQL numeric type
|
|
const { price, marketPrice, flashPrice, ...otherData } = updateData;
|
|
const [updatedProduct] = await db
|
|
.update(productInfo)
|
|
.set({
|
|
...otherData,
|
|
...(price !== undefined && { price: price.toString() }),
|
|
...(marketPrice !== undefined && { marketPrice: marketPrice.toString() }),
|
|
...(flashPrice !== undefined && { flashPrice: flashPrice.toString() }),
|
|
images: currentImages,
|
|
})
|
|
.where(eq(productInfo.id, id))
|
|
.returning();
|
|
|
|
// Handle deals update
|
|
if (deals !== undefined) {
|
|
await db.delete(specialDeals).where(eq(specialDeals.productId, id));
|
|
if (deals.length > 0) {
|
|
const dealInserts = deals.map(deal => ({
|
|
productId: id,
|
|
quantity: deal.quantity.toString(),
|
|
price: deal.price.toString(),
|
|
validTill: new Date(deal.validTill),
|
|
}));
|
|
await db.insert(specialDeals).values(dealInserts);
|
|
}
|
|
}
|
|
|
|
// Handle tags update
|
|
if (tagIds !== undefined) {
|
|
await db.delete(productTags).where(eq(productTags.productId, id));
|
|
if (tagIds.length > 0) {
|
|
const tagAssociations = tagIds.map(tagId => ({
|
|
productId: id,
|
|
tagId,
|
|
}));
|
|
await db.insert(productTags).values(tagAssociations);
|
|
}
|
|
}
|
|
|
|
scheduleStoreInitialization();
|
|
|
|
return {
|
|
product: updatedProduct,
|
|
message: "Product updated successfully",
|
|
};
|
|
}),
|
|
|
|
toggleOutOfStock: protectedProcedure
|
|
.input(z.object({
|
|
id: z.number(),
|
|
}))
|
|
.mutation(async ({ input, ctx }) => {
|
|
const { id } = input;
|
|
|
|
const product = await db.query.productInfo.findFirst({
|
|
where: eq(productInfo.id, id),
|
|
});
|
|
|
|
if (!product) {
|
|
throw new ApiError("Product not found", 404);
|
|
}
|
|
|
|
const [updatedProduct] = await db
|
|
.update(productInfo)
|
|
.set({
|
|
isOutOfStock: !product.isOutOfStock,
|
|
})
|
|
.where(eq(productInfo.id, id))
|
|
.returning();
|
|
|
|
// Reinitialize stores to reflect changes
|
|
scheduleStoreInitialization()
|
|
|
|
return {
|
|
product: updatedProduct,
|
|
message: `Product marked as ${updatedProduct.isOutOfStock ? 'out of stock' : 'in stock'}`,
|
|
};
|
|
}),
|
|
|
|
updateSlotProducts: protectedProcedure
|
|
.input(z.object({
|
|
slotId: z.string(),
|
|
productIds: z.array(z.string()),
|
|
}))
|
|
.mutation(async ({ input, ctx }) => {
|
|
const { slotId, productIds } = input;
|
|
|
|
if (!Array.isArray(productIds)) {
|
|
throw new ApiError("productIds must be an array", 400);
|
|
}
|
|
|
|
// Get current associations
|
|
const currentAssociations = await db.query.productSlots.findMany({
|
|
where: eq(productSlots.slotId, parseInt(slotId)),
|
|
columns: {
|
|
productId: true,
|
|
},
|
|
});
|
|
|
|
const currentProductIds = currentAssociations.map(assoc => assoc.productId);
|
|
const newProductIds = productIds.map((id: string) => parseInt(id));
|
|
|
|
// Find products to add and remove
|
|
const productsToAdd = newProductIds.filter(id => !currentProductIds.includes(id));
|
|
const productsToRemove = currentProductIds.filter(id => !newProductIds.includes(id));
|
|
|
|
// Remove associations for products that are no longer selected
|
|
if (productsToRemove.length > 0) {
|
|
await db.delete(productSlots).where(
|
|
and(
|
|
eq(productSlots.slotId, parseInt(slotId)),
|
|
inArray(productSlots.productId, productsToRemove)
|
|
)
|
|
);
|
|
}
|
|
|
|
// Add associations for newly selected products
|
|
if (productsToAdd.length > 0) {
|
|
const newAssociations = productsToAdd.map(productId => ({
|
|
productId,
|
|
slotId: parseInt(slotId),
|
|
}));
|
|
|
|
await db.insert(productSlots).values(newAssociations);
|
|
}
|
|
|
|
// Reinitialize stores to reflect changes
|
|
scheduleStoreInitialization()
|
|
|
|
return {
|
|
message: "Slot products updated successfully",
|
|
added: productsToAdd.length,
|
|
removed: productsToRemove.length,
|
|
};
|
|
}),
|
|
|
|
getSlotProductIds: protectedProcedure
|
|
.input(z.object({
|
|
slotId: z.string(),
|
|
}))
|
|
.query(async ({ input, ctx }) => {
|
|
const { slotId } = input;
|
|
|
|
const associations = await db.query.productSlots.findMany({
|
|
where: eq(productSlots.slotId, parseInt(slotId)),
|
|
columns: {
|
|
productId: true,
|
|
},
|
|
});
|
|
|
|
const productIds = associations.map(assoc => assoc.productId);
|
|
|
|
return {
|
|
productIds,
|
|
};
|
|
}),
|
|
|
|
getSlotsProductIds: protectedProcedure
|
|
.input(z.object({
|
|
slotIds: z.array(z.number()),
|
|
}))
|
|
.query(async ({ input, ctx }) => {
|
|
const { slotIds } = input;
|
|
|
|
if (!Array.isArray(slotIds)) {
|
|
throw new ApiError("slotIds must be an array", 400);
|
|
}
|
|
|
|
if (slotIds.length === 0) {
|
|
return {};
|
|
}
|
|
|
|
// Fetch all associations for the requested slots
|
|
const associations = await db.query.productSlots.findMany({
|
|
where: inArray(productSlots.slotId, slotIds),
|
|
columns: {
|
|
slotId: true,
|
|
productId: true,
|
|
},
|
|
});
|
|
|
|
// Group by slotId
|
|
const result = associations.reduce((acc, assoc) => {
|
|
if (!acc[assoc.slotId]) {
|
|
acc[assoc.slotId] = [];
|
|
}
|
|
acc[assoc.slotId].push(assoc.productId);
|
|
return acc;
|
|
}, {} as Record<number, number[]>);
|
|
|
|
// Ensure all requested slots have entries (even if empty)
|
|
slotIds.forEach(slotId => {
|
|
if (!result[slotId]) {
|
|
result[slotId] = [];
|
|
}
|
|
});
|
|
|
|
return result;
|
|
}),
|
|
|
|
getProductReviews: protectedProcedure
|
|
.input(z.object({
|
|
productId: z.number().int().positive(),
|
|
limit: z.number().int().min(1).max(50).optional().default(10),
|
|
offset: z.number().int().min(0).optional().default(0),
|
|
}))
|
|
.query(async ({ input }) => {
|
|
const { productId, limit, offset } = input;
|
|
|
|
const reviews = await db
|
|
.select({
|
|
id: productReviews.id,
|
|
reviewBody: productReviews.reviewBody,
|
|
ratings: productReviews.ratings,
|
|
imageUrls: productReviews.imageUrls,
|
|
reviewTime: productReviews.reviewTime,
|
|
adminResponse: productReviews.adminResponse,
|
|
adminResponseImages: productReviews.adminResponseImages,
|
|
userName: users.name,
|
|
})
|
|
.from(productReviews)
|
|
.innerJoin(users, eq(productReviews.userId, users.id))
|
|
.where(eq(productReviews.productId, productId))
|
|
.orderBy(desc(productReviews.reviewTime))
|
|
.limit(limit)
|
|
.offset(offset);
|
|
|
|
// Generate signed URLs for images
|
|
const reviewsWithSignedUrls = await Promise.all(
|
|
reviews.map(async (review) => ({
|
|
...review,
|
|
signedImageUrls: scaffoldAssetUrl((review.imageUrls as string[]) || []),
|
|
signedAdminImageUrls: scaffoldAssetUrl((review.adminResponseImages as string[]) || []),
|
|
}))
|
|
);
|
|
|
|
// Check if more reviews exist
|
|
const totalCountResult = await db
|
|
.select({ count: sql`count(*)` })
|
|
.from(productReviews)
|
|
.where(eq(productReviews.productId, productId));
|
|
|
|
const totalCount = Number(totalCountResult[0].count);
|
|
const hasMore = offset + limit < totalCount;
|
|
|
|
return { reviews: reviewsWithSignedUrls, hasMore };
|
|
}),
|
|
|
|
respondToReview: protectedProcedure
|
|
.input(z.object({
|
|
reviewId: z.number().int().positive(),
|
|
adminResponse: z.string().optional(),
|
|
adminResponseImages: z.array(z.string()).optional().default([]),
|
|
uploadUrls: z.array(z.string()).optional().default([]),
|
|
}))
|
|
.mutation(async ({ input }) => {
|
|
const { reviewId, adminResponse, adminResponseImages, uploadUrls } = input;
|
|
|
|
const [updatedReview] = await db
|
|
.update(productReviews)
|
|
.set({
|
|
adminResponse,
|
|
adminResponseImages,
|
|
})
|
|
.where(eq(productReviews.id, reviewId))
|
|
.returning();
|
|
|
|
if (!updatedReview) {
|
|
throw new ApiError('Review not found', 404);
|
|
}
|
|
|
|
// Claim upload URLs
|
|
if (uploadUrls && uploadUrls.length > 0) {
|
|
// const { claimUploadUrl } = await import('@/src/lib/s3-client');
|
|
await Promise.all(uploadUrls.map(url => claimUploadUrl(url)));
|
|
}
|
|
|
|
return { success: true, review: updatedReview };
|
|
}),
|
|
|
|
getGroups: protectedProcedure
|
|
.query(async ({ ctx }) => {
|
|
const groups = await db.query.productGroupInfo.findMany({
|
|
with: {
|
|
memberships: {
|
|
with: {
|
|
product: true,
|
|
},
|
|
},
|
|
},
|
|
orderBy: desc(productGroupInfo.createdAt),
|
|
});
|
|
|
|
return {
|
|
groups: groups.map(group => ({
|
|
...group,
|
|
products: group.memberships.map(m => m.product),
|
|
productCount: group.memberships.length,
|
|
})),
|
|
};
|
|
}),
|
|
|
|
createGroup: protectedProcedure
|
|
.input(z.object({
|
|
group_name: z.string().min(1),
|
|
description: z.string().optional(),
|
|
product_ids: z.array(z.number()).default([]),
|
|
}))
|
|
.mutation(async ({ input, ctx }) => {
|
|
const { group_name, description, product_ids } = input;
|
|
|
|
const [newGroup] = await db
|
|
.insert(productGroupInfo)
|
|
.values({
|
|
groupName: group_name,
|
|
description,
|
|
})
|
|
.returning();
|
|
|
|
if (product_ids.length > 0) {
|
|
const memberships = product_ids.map(productId => ({
|
|
productId,
|
|
groupId: newGroup.id,
|
|
}));
|
|
|
|
await db.insert(productGroupMembership).values(memberships);
|
|
}
|
|
|
|
// Reinitialize stores to reflect changes
|
|
scheduleStoreInitialization()
|
|
|
|
return {
|
|
group: newGroup,
|
|
message: 'Group created successfully',
|
|
};
|
|
}),
|
|
|
|
updateGroup: protectedProcedure
|
|
.input(z.object({
|
|
id: z.number(),
|
|
group_name: z.string().optional(),
|
|
description: z.string().optional(),
|
|
product_ids: z.array(z.number()).optional(),
|
|
}))
|
|
.mutation(async ({ input, ctx }) => {
|
|
const { id, group_name, description, product_ids } = input;
|
|
|
|
const updateData: any = {};
|
|
if (group_name !== undefined) updateData.groupName = group_name;
|
|
if (description !== undefined) updateData.description = description;
|
|
|
|
const [updatedGroup] = await db
|
|
.update(productGroupInfo)
|
|
.set(updateData)
|
|
.where(eq(productGroupInfo.id, id))
|
|
.returning();
|
|
|
|
if (!updatedGroup) {
|
|
throw new ApiError('Group not found', 404);
|
|
}
|
|
|
|
if (product_ids !== undefined) {
|
|
// Delete existing memberships
|
|
await db.delete(productGroupMembership).where(eq(productGroupMembership.groupId, id));
|
|
|
|
// Insert new memberships
|
|
if (product_ids.length > 0) {
|
|
const memberships = product_ids.map(productId => ({
|
|
productId,
|
|
groupId: id,
|
|
}));
|
|
|
|
await db.insert(productGroupMembership).values(memberships);
|
|
}
|
|
}
|
|
|
|
// Reinitialize stores to reflect changes
|
|
scheduleStoreInitialization()
|
|
|
|
return {
|
|
group: updatedGroup,
|
|
message: 'Group updated successfully',
|
|
};
|
|
}),
|
|
|
|
deleteGroup: protectedProcedure
|
|
.input(z.object({
|
|
id: z.number(),
|
|
}))
|
|
.mutation(async ({ input, ctx }) => {
|
|
const { id } = input;
|
|
|
|
// Delete memberships first
|
|
await db.delete(productGroupMembership).where(eq(productGroupMembership.groupId, id));
|
|
|
|
// Delete group
|
|
const [deletedGroup] = await db
|
|
.delete(productGroupInfo)
|
|
.where(eq(productGroupInfo.id, id))
|
|
.returning();
|
|
|
|
if (!deletedGroup) {
|
|
throw new ApiError('Group not found', 404);
|
|
}
|
|
|
|
// Reinitialize stores to reflect changes
|
|
scheduleStoreInitialization()
|
|
|
|
return {
|
|
message: 'Group deleted successfully',
|
|
};
|
|
}),
|
|
|
|
updateProductPrices: protectedProcedure
|
|
.input(z.object({
|
|
updates: z.array(z.object({
|
|
productId: z.number(),
|
|
price: z.number().optional(),
|
|
marketPrice: z.number().nullable().optional(),
|
|
flashPrice: z.number().nullable().optional(),
|
|
isFlashAvailable: z.boolean().optional(),
|
|
})),
|
|
}))
|
|
.mutation(async ({ input, ctx }) => {
|
|
const { updates } = input;
|
|
|
|
if (updates.length === 0) {
|
|
throw new ApiError('No updates provided', 400);
|
|
}
|
|
|
|
// Validate that all productIds exist
|
|
const productIds = updates.map(u => u.productId);
|
|
const existingProducts = await db.query.productInfo.findMany({
|
|
where: inArray(productInfo.id, productIds),
|
|
columns: { id: true },
|
|
});
|
|
|
|
const existingIds = new Set(existingProducts.map(p => p.id));
|
|
const invalidIds = productIds.filter(id => !existingIds.has(id));
|
|
|
|
if (invalidIds.length > 0) {
|
|
throw new ApiError(`Invalid product IDs: ${invalidIds.join(', ')}`, 400);
|
|
}
|
|
|
|
// Perform batch update
|
|
const updatePromises = updates.map(async (update) => {
|
|
const { productId, price, marketPrice, flashPrice, isFlashAvailable } = update;
|
|
const updateData: any = {};
|
|
if (price !== undefined) updateData.price = price;
|
|
if (marketPrice !== undefined) updateData.marketPrice = marketPrice;
|
|
if (flashPrice !== undefined) updateData.flashPrice = flashPrice;
|
|
if (isFlashAvailable !== undefined) updateData.isFlashAvailable = isFlashAvailable;
|
|
|
|
return db
|
|
.update(productInfo)
|
|
.set(updateData)
|
|
.where(eq(productInfo.id, productId));
|
|
});
|
|
|
|
await Promise.all(updatePromises);
|
|
|
|
// Reinitialize stores to reflect changes
|
|
scheduleStoreInitialization()
|
|
|
|
return {
|
|
message: `Updated prices for ${updates.length} product(s)`,
|
|
updatedCount: updates.length,
|
|
};
|
|
}),
|
|
});
|