fully functional

This commit is contained in:
shafi54 2026-07-31 20:47:35 +05:30
parent 5c2b3aaa67
commit f6bcc23ca2
39 changed files with 436 additions and 177 deletions

View file

@ -84,7 +84,7 @@ export default function EditCoupon() {
maxValue: coupon.maxValue ? parseFloat(coupon.maxValue) : undefined,
validTill: coupon.validTill ? dayjs(coupon.validTill).format('YYYY-MM-DD') : undefined,
maxLimitForUser: coupon.maxLimitForUser || undefined,
productIds: coupon.productIds,
skuIds: coupon.skuIds,
isReservedCoupon: false, // Normal coupons
};

View file

@ -15,7 +15,7 @@ export default function CreateBanner() {
name: '',
imageUrl: '',
description: '',
productIds: [],
skuIds: [],
redirectUrl: '',
// serialNum removed - assigned automatically by backend
};
@ -38,7 +38,7 @@ export default function CreateBanner() {
name: values.name,
imageUrl,
description: values.description || undefined,
productIds: values.productIds.length > 0 ? values.productIds : [],
skuIds: values.skuIds.length > 0 ? values.skuIds : [],
redirectUrl: values.redirectUrl || undefined,
});

View file

@ -12,7 +12,7 @@ interface Banner {
name: string;
imageUrl: string;
description?: string;
productIds?: number[];
skuIds?: number[];
redirectUrl?: string;
serialNum: number;
isActive: boolean;
@ -45,12 +45,12 @@ export default function EditBanner() {
if (bannerData) {
// Handle data format compatibility (productId -> productIds migration)
// Handle data format compatibility (productId -> skuIds migration)
const processedBanner = {
...bannerData,
productIds: Array.isArray(bannerData.productIds)
? bannerData.productIds
: (bannerData.productIds ? [bannerData.productIds] : [])
skuIds: Array.isArray(bannerData.skuIds)
? bannerData.skuIds
: (bannerData.skuIds ? [bannerData.skuIds] : [])
};
setBanner(processedBanner);
@ -74,14 +74,14 @@ export default function EditBanner() {
name: banner.name,
imageUrl: banner.imageUrl,
description: banner.description || '',
productIds: banner.productIds || [],
skuIds: banner.skuIds || [],
redirectUrl: banner.redirectUrl || '',
// serialNum removed - handled automatically by backend
} : {
name: '',
imageUrl: '',
description: '',
productIds: [],
skuIds: [],
redirectUrl: '',
// serialNum removed - handled automatically by backend
};
@ -99,7 +99,7 @@ export default function EditBanner() {
name: values.name,
imageUrl: imageUrl || banner.imageUrl,
description: values.description || undefined,
productIds: values.productIds.length > 0 ? values.productIds : [],
skuIds: values.skuIds.length > 0 ? values.skuIds : [],
redirectUrl: values.redirectUrl || undefined,
});

View file

@ -10,7 +10,7 @@ interface Banner {
name: string;
imageUrl: string;
description: string | null;
productIds: number[] | null;
skuIds: number[] | null;
redirectUrl: string | null;
serialNum: number | null;
isActive: boolean;

View file

@ -113,7 +113,7 @@ export default function SlotDetails() {
<View style={tw`flex-row items-center`}>
<TouchableOpacity
onPress={() => {
const snippetProducts = products.filter(p => snippet.productIds.includes(p.id));
const snippetProducts = products.filter(p => snippet.skuIds.includes(p.id));
setDialogProducts(snippetProducts);
setDialogOpen(true);
}}
@ -121,7 +121,7 @@ export default function SlotDetails() {
>
<MaterialIcons name="shopping-bag" size={14} color="#94A3B8" />
<MyText style={tw`text-xs font-medium text-blue-600 ml-1 underline`}>
{snippet.productIds.length} products
{snippet.skuIds.length} products
</MyText>
</TouchableOpacity>

View file

@ -192,7 +192,7 @@ const SnippetItem = ({
>
<MaterialIcons name="shopping-bag" size={14} color="#94A3B8" />
<MyText style={tw`text-xs font-bold text-slate-500 ml-1.5`}>
{snippet.productIds.length} Items
{snippet.skuIds.length} Items
</MyText>
<MaterialIcons name="chevron-right" size={14} color="#94A3B8" />
</MyTouchableOpacity>
@ -279,7 +279,7 @@ const handleViewProducts = (products: VendorSnippetProduct[]) => {
snippetCode: snippet.snippetCode,
slotId: snippet.slotId || 0, // Convert null to number for form
isPermanent: snippet.isPermanent,
productIds: snippet.productIds,
skuIds: snippet.skuIds,
validTill: snippet.validTill,
createdAt: snippet.createdAt,
};

View file

@ -14,7 +14,7 @@ export interface BannerFormData {
name: string;
imageUrl: string;
description: string;
productIds: number[];
skuIds: number[];
redirectUrl: string;
// serialNum removed - will be assigned automatically by backend
}
@ -32,7 +32,7 @@ interface BannerFormProps {
const validationSchema = Yup.object().shape({
name: Yup.string().trim().required('Banner name is required').max(255),
description: Yup.string().max(500),
productIds: Yup.array()
skuIds: Yup.array()
.of(Yup.number())
.optional(),
redirectUrl: Yup.string()
@ -177,10 +177,10 @@ export default function BannerForm({
<View style={{ marginBottom: 16 }}>
<ProductsSelector
value={values.productIds}
value={values.skuIds}
onChange={(value) => {
const selectedValues = Array.isArray(value) ? value : [value];
setFieldValue('productIds', selectedValues.map(v => Number(v)));
setFieldValue('skuIds', selectedValues.map(v => Number(v)));
}}
multiple={true}
label="Select Products"

View file

@ -34,7 +34,7 @@ const VendorSnippetForm: React.FC<VendorSnippetFormProps> = ({
snippetCode: snippet?.snippetCode || '',
slotId: snippet?.slotId?.toString() || '',
isPermanent: snippet?.isPermanent || false,
productIds: snippet?.productIds?.map(id => id.toString()) || [],
skuIds: snippet?.skuIds?.map(id => id.toString()) || [],
validTill: snippet?.validTill ? new Date(snippet.validTill) : null,
},
validate: (values) => {
@ -55,8 +55,8 @@ const VendorSnippetForm: React.FC<VendorSnippetFormProps> = ({
errors.slotId = 'Slot selection is required';
}
if (values.productIds.length === 0) {
errors.productIds = 'At least one product must be selected';
if (values.skuIds.length === 0) {
errors.skuIds = 'At least one product must be selected';
}
return errors;
@ -68,7 +68,7 @@ const VendorSnippetForm: React.FC<VendorSnippetFormProps> = ({
snippetCode: values.snippetCode,
slotId: values.isPermanent ? undefined : parseInt(values.slotId || '0'),
isPermanent: values.isPermanent,
productIds: values.productIds.map(id => parseInt(id)),
skuIds: values.skuIds.map(id => parseInt(id)),
validTill: values.validTill ? values.validTill.toISOString() : undefined,
};
@ -180,15 +180,15 @@ const VendorSnippetForm: React.FC<VendorSnippetFormProps> = ({
{/* Product Selection */}
<View>
<ProductsSelector
value={formik.values.productIds.map(id => parseInt(id))}
onChange={(selectedProductIds) => formik.setFieldValue('productIds', (selectedProductIds as number[]).map(id => id.toString()))}
value={formik.values.skuIds.map(id => parseInt(id))}
onChange={(selectedProductIds) => formik.setFieldValue('skuIds', (selectedProductIds as number[]).map(id => id.toString()))}
multiple={true}
label="Select Products"
placeholder="Select products"
labelFormat={(product) => `${product.name} (${product.unit})`}
/>
{formik.errors.productIds && formik.touched.productIds && (
<MyText style={tw`text-red-500 text-sm mt-1`}>{formik.errors.productIds}</MyText>
{formik.errors.skuIds && formik.touched.skuIds && (
<MyText style={tw`text-red-500 text-sm mt-1`}>{formik.errors.skuIds}</MyText>
)}
</View>

View file

@ -4,7 +4,7 @@ export interface Banner {
name: string;
imageUrl: string;
description?: string;
productId?: number;
skuIds?: number[];
redirectUrl?: string;
serialNum: number;
isActive: boolean;
@ -16,7 +16,7 @@ export interface CreateBannerPayload {
name: string;
imageUrl: string;
description?: string;
productId?: number;
skuIds?: number[];
redirectUrl?: string;
serialNum: number;
}

View file

@ -105,7 +105,7 @@ export default function CouponForm({ onSubmit, isLoading, initialValues }: Coupo
maxValue: undefined,
validTill: undefined,
maxLimitForUser: undefined,
productIds: undefined,
skuIds: undefined,
applicableUsers: [],
applicableProducts: [],
exclusiveApply: false,

View file

@ -8,7 +8,7 @@ export interface VendorSnippet {
snippetCode: string;
slotId: number | null;
isPermanent: boolean;
productIds: number[];
skuIds: number[];
products: VendorSnippetProduct[];
validTill: string | null;
createdAt: string;
@ -28,7 +28,7 @@ export interface VendorSnippetForm {
snippetCode: string;
slotId: number;
isPermanent: boolean;
productIds: number[];
skuIds: number[];
validTill: string | null;
createdAt: string;
}

View file

@ -15,7 +15,7 @@
"deploy:dev": "wrangler deploy --config wrangler.dev.toml",
"wrangler:dev": "wrangler dev worker.ts --config wrangler.toml",
"wrangler:deploy": "wrangler deploy worker.ts --config wrangler.toml",
"pull_db": "wrangler d1 export freshyo-dev --config wrangler.prod.toml --remote --output ./dumps/latest.sql && bash ./scripts/populate_localdb.sh",
"pull_db": "rm -rf .wrangler/state/v3/d1/* && wrangler d1 export freshyo-dev --config wrangler.prod.toml --remote --output ./dumps/latest.sql && bash ./scripts/populate_localdb.sh",
"docker:build": "cd .. && docker buildx build --platform linux/amd64 -t mohdshafiuddin54/health_petal:latest --progress=plain -f backend/Dockerfile .",
"docker:push": "docker push mohdshafiuddin54/health_petal:latest"
},

View file

@ -72,6 +72,7 @@ export {
createSpecialDealsForProduct,
updateProductDeals,
replaceProductTags,
mergeSkus,
toggleProductOutOfStock,
updateSlotProducts,
getSlotProductIds,

View file

@ -13,7 +13,7 @@ interface Banner {
name: string
imageUrl: string | null
serialNum: number | null
productIds: number[] | null
skuIds: number[] | null
createdAt: Date
}
@ -46,7 +46,7 @@ export async function initializeBannerStore(): Promise<void> {
// name: banner.name,
// imageUrl: signedImageUrl,
// serialNum: banner.serialNum,
// productIds: banner.productIds,
// skuIds: banner.skuIds,
// createdAt: banner.createdAt,
// }
//
@ -78,7 +78,7 @@ export async function getBannerById(id: number): Promise<Banner | null> {
name: banner.name,
imageUrl: signedImageUrl,
serialNum: banner.serialNum,
productIds: banner.productIds,
skuIds: banner.skuIds,
createdAt: banner.createdAt,
}
} catch (error) {
@ -121,7 +121,7 @@ export async function getAllBanners(): Promise<Banner[]> {
name: banner.name,
imageUrl: signedImageUrl,
serialNum: banner.serialNum,
productIds: banner.productIds,
skuIds: banner.skuIds,
createdAt: banner.createdAt,
}
})

View file

@ -26,7 +26,7 @@ export const bannerRouter = router({
// 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 skuIds array
// });
@ -37,16 +37,16 @@ export const bannerRouter = router({
return {
...banner,
imageUrl: banner.imageUrl ? scaffoldAssetUrl(banner.imageUrl) : banner.imageUrl,
// Ensure productIds is always an array
productIds: banner.productIds || [],
// Ensure skuIds is always an array
skuIds: banner.skuIds || [],
};
} catch (error) {
console.error(`Failed to generate signed URL for banner ${banner.id}:`, error);
return {
...banner,
imageUrl: banner.imageUrl, // Keep original on error
// Ensure productIds is always an array
productIds: banner.productIds || [],
// Ensure skuIds is always an array
skuIds: banner.skuIds || [],
};
}
})
@ -74,7 +74,7 @@ export const bannerRouter = router({
// Old implementation - direct DB query:
const banner = await db.query.homeBanners.findFirst({
where: eq(homeBanners.id, input.id),
// Removed product relationship since we now use productIds array
// Removed product relationship since we now use skuIds array
});
*/
@ -89,9 +89,9 @@ export const bannerRouter = router({
// Keep original imageUrl on error
}
// Ensure productIds is always an array (handle migration compatibility)
if (!banner.productIds) {
banner.productIds = [];
// Ensure skuIds is always an array (handle migration compatibility)
if (!banner.skuIds) {
banner.skuIds = [];
}
}
@ -104,7 +104,7 @@ export const bannerRouter = router({
name: z.string().min(1),
imageUrl: z.string().url(),
description: z.string().optional(),
productIds: z.array(z.number()).optional(),
skuIds: z.array(z.number()).optional(),
redirectUrl: z.string().url().optional(),
// serialNum removed completely
}))
@ -116,7 +116,7 @@ export const bannerRouter = router({
name: input.name,
imageUrl: imageUrl,
description: input.description ?? null,
productIds: input.productIds || [],
skuIds: input.skuIds || [],
redirectUrl: input.redirectUrl ?? null,
serialNum: 999, // Default value, not used
isActive: false, // Default to inactive
@ -129,7 +129,7 @@ export const bannerRouter = router({
name: input.name,
imageUrl: imageUrl,
description: input.description,
productIds: input.productIds || [],
skuIds: input.skuIds || [],
redirectUrl: input.redirectUrl,
serialNum: 999, // Default value, not used
isActive: false, // Default to inactive
@ -153,7 +153,7 @@ export const bannerRouter = router({
name: z.string().min(1).optional(),
imageUrl: z.string().url().optional(),
description: z.string().optional(),
productIds: z.array(z.number()).optional(),
skuIds: z.array(z.number()).optional(),
redirectUrl: z.string().url().optional(),
serialNum: z.number().nullable().optional(),
isActive: z.boolean().optional(),
@ -181,7 +181,7 @@ export const bannerRouter = router({
/*
// Old implementation - direct DB query:
const { id, ...updateData } = input;
const incomingProductIds = input.productIds;
const incomingProductIds = input.skuIds;
// Extract S3 key from presigned URL if imageUrl is provided
const processedData = {
...updateData,

View file

@ -29,7 +29,7 @@ const createCouponBodySchema = z.object({
flatDiscount: z.number().optional(),
minOrder: z.number().optional(),
targetUser: z.number().optional(),
productIds: z.array(z.number()).optional().nullable(),
skuIds: z.array(z.number()).optional().nullable(),
applicableUsers: z.array(z.number()).optional(),
applicableProducts: z.array(z.number()).optional(),
maxValue: z.number().optional(),
@ -49,7 +49,7 @@ export const couponRouter = router({
create: protectedProcedure
.input(createCouponBodySchema)
.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, skuIds, applicableUsers, applicableProducts, maxValue, isApplyForAll, validTill, maxLimitForUser, exclusiveApply } = input;
// Validation: ensure at least one discount type is provided
if ((!discountPercent && !flatDiscount) || (discountPercent && flatDiscount)) {
@ -101,7 +101,7 @@ export const couponRouter = router({
discountPercent: discountPercent?.toString(),
flatDiscount: flatDiscount?.toString(),
minOrder: minOrder?.toString(),
productIds: productIds || null,
skuIds: skuIds || null,
createdBy: staffUserId,
maxValue: maxValue?.toString(),
isApplyForAll: isApplyForAll || false,
@ -121,7 +121,7 @@ export const couponRouter = router({
discountPercent: discountPercent?.toString(),
flatDiscount: flatDiscount?.toString(),
minOrder: minOrder?.toString(),
productIds: productIds || null,
skuIds: skuIds || null,
createdBy: staffUserId,
maxValue: maxValue?.toString(),
isApplyForAll: isApplyForAll || false,
@ -145,9 +145,9 @@ export const couponRouter = router({
// Insert applicable products
if (applicableProducts && applicableProducts.length > 0) {
await db.insert(couponApplicableProducts).values(
applicableProducts.map(productId => ({
applicableProducts.map(skuId => ({
couponId: coupon.id,
productId,
skuId,
}))
);
}
@ -185,7 +185,7 @@ export const couponRouter = router({
return {
...result,
productIds: (result.productIds as number[]) || undefined,
skuIds: (result.skuIds as number[]) || undefined,
applicableUsers: result.applicableUsers.map((au: any) => au.user),
applicableProducts: result.applicableProducts.map((ap: any) => ap.product),
};
@ -221,7 +221,7 @@ export const couponRouter = router({
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 (updates.skuIds !== undefined) updateData.skuIds = updates.skuIds;
// Using dbService helper (new implementation)
const coupon = await updateCouponWithRelations(
@ -260,9 +260,9 @@ export const couponRouter = router({
await db.delete(couponApplicableProducts).where(eq(couponApplicableProducts.couponId, id));
if (updates.applicableProducts.length > 0) {
await db.insert(couponApplicableProducts).values(
updates.applicableProducts.map(productId => ({
updates.applicableProducts.map(skuId => ({
couponId: id,
productId,
skuId,
}))
);
}
@ -405,7 +405,7 @@ export const couponRouter = router({
createReservedCoupon: protectedProcedure
.input(createCouponBodySchema)
.mutation(async ({ input, ctx }): Promise<any> => {
const { couponCode, isUserBased, discountPercent, flatDiscount, minOrder, productIds, applicableProducts, maxValue, validTill, maxLimitForUser, exclusiveApply } = input;
const { couponCode, isUserBased, discountPercent, flatDiscount, minOrder, skuIds, applicableProducts, maxValue, validTill, maxLimitForUser, exclusiveApply } = input;
// Validation: ensure at least one discount type is provided
if ((!discountPercent && !flatDiscount) || (discountPercent && flatDiscount)) {
@ -434,7 +434,7 @@ export const couponRouter = router({
discountPercent: discountPercent?.toString(),
flatDiscount: flatDiscount?.toString(),
minOrder: minOrder?.toString(),
productIds,
skuIds,
maxValue: maxValue?.toString(),
validTill: validTill ? dayjs(validTill).toDate() : undefined,
maxLimitForUser,
@ -452,7 +452,7 @@ export const couponRouter = router({
discountPercent: discountPercent?.toString(),
flatDiscount: flatDiscount?.toString(),
minOrder: minOrder?.toString(),
productIds,
skuIds,
maxValue: maxValue?.toString(),
validTill: validTill ? dayjs(validTill).toDate() : undefined,
maxLimitForUser,
@ -465,9 +465,9 @@ export const couponRouter = router({
// Insert applicable products if provided
if (applicableProducts && applicableProducts.length > 0) {
await db.insert(couponApplicableProducts).values(
applicableProducts.map(productId => ({
applicableProducts.map(skuId => ({
couponId: coupon.id,
productId,
skuId,
}))
);
}

View file

@ -32,7 +32,7 @@ import type {
const createSnippetSchema = z.object({
snippetCode: z.string().min(1, "Snippet code is required"),
slotId: z.number().optional(),
productIds: z.array(z.number().int().positive()).min(1, "At least one product is required"),
skuIds: z.array(z.number().int().positive()).min(1, "At least one product is required"),
validTill: z.string().optional(),
isPermanent: z.boolean().default(false)
});
@ -41,7 +41,7 @@ const updateSnippetSchema = z.object({
id: z.number().int().positive(),
updates: createSnippetSchema.partial().extend({
snippetCode: z.string().min(1).optional(),
productIds: z.array(z.number().int().positive()).optional(),
skuIds: z.array(z.number().int().positive()).optional(),
isPermanent: z.boolean().default(false)
}),
});
@ -50,7 +50,7 @@ export const vendorSnippetsRouter = router({
create: protectedProcedure
.input(createSnippetSchema)
.mutation(async ({ input, ctx }): Promise<AdminVendorSnippet> => {
const { snippetCode, slotId, productIds, validTill, isPermanent } = input;
const { snippetCode, slotId, skuIds, validTill, isPermanent } = input;
// Get staff user ID from auth middleware
const staffUserId = ctx.staffUser?.id;
@ -65,8 +65,8 @@ export const vendorSnippetsRouter = router({
}
}
const products = await getProductsByIdsInDb(productIds)
if (products.length !== productIds.length) {
const products = await getProductsByIdsInDb(skuIds)
if (products.length !== skuIds.length) {
throw new Error("One or more invalid product IDs")
}
@ -78,7 +78,7 @@ export const vendorSnippetsRouter = router({
const result = await createVendorSnippetInDb({
snippetCode,
slotId,
productIds,
skuIds,
isPermanent,
validTill: validTill ? new Date(validTill) : undefined,
})
@ -97,9 +97,9 @@ export const vendorSnippetsRouter = router({
// Validate products exist
const products = await db.query.productInfo.findMany({
where: inArray(productInfo.id, productIds),
where: inArray(productInfo.id, skuIds),
});
if (products.length !== productIds.length) {
if (products.length !== skuIds.length) {
throw new Error("One or more invalid product IDs");
}
@ -114,7 +114,7 @@ export const vendorSnippetsRouter = router({
const result = await db.insert(vendorSnippets).values({
snippetCode,
slotId,
productIds,
skuIds,
isPermanent,
validTill: validTill ? new Date(validTill) : undefined,
}).returning();
@ -134,7 +134,7 @@ export const vendorSnippetsRouter = router({
const snippetsWithProducts = await Promise.all(
result.map(async (snippet) => {
const products = await getProductsByIdsInDb(snippet.productIds)
const products = await getProductsByIdsInDb(snippet.skuIds)
return {
...snippet,
@ -156,7 +156,7 @@ export const vendorSnippetsRouter = router({
const snippetsWithProducts = await Promise.all(
result.map(async (snippet) => {
const products = await db.query.productInfo.findMany({
where: inArray(productInfo.id, snippet.productIds),
where: inArray(productInfo.id, snippet.skuIds),
columns: { id: true, name: true },
});
@ -226,9 +226,9 @@ export const vendorSnippetsRouter = router({
}
}
if (updates.productIds) {
const products = await getProductsByIdsInDb(updates.productIds)
if (products.length !== updates.productIds.length) {
if (updates.skuIds) {
const products = await getProductsByIdsInDb(updates.skuIds)
if (products.length !== updates.skuIds.length) {
throw new Error('One or more invalid product IDs')
}
}
@ -269,11 +269,11 @@ export const vendorSnippetsRouter = router({
}
// Validate products if being updated
if (updates.productIds) {
if (updates.skuIds) {
const products = await db.query.productInfo.findMany({
where: inArray(productInfo.id, updates.productIds),
where: inArray(productInfo.id, updates.skuIds),
});
if (products.length !== updates.productIds.length) {
if (products.length !== updates.skuIds.length) {
throw new Error("One or more invalid product IDs");
}
}
@ -404,14 +404,14 @@ export const vendorSnippetsRouter = router({
const status = order.orderStatus;
if (status[0].isCancelled) return false;
const orderProductIds = order.orderItems.map(item => item.productId);
return snippet.productIds.some(productId => orderProductIds.includes(productId));
return snippet.skuIds.some(productId => orderProductIds.includes(productId));
});
// Format the response
const formattedOrders = filteredOrders.map(order => {
// Filter orderItems to only include products attached to the snippet
const attachedOrderItems = order.orderItems.filter(item =>
snippet.productIds.includes(item.productId)
snippet.skuIds.includes(item.productId)
);
const products = attachedOrderItems.map(item => ({
@ -439,7 +439,7 @@ export const vendorSnippetsRouter = router({
sequence: order.slot.deliverySequence,
} : null,
products,
matchedProducts: snippet.productIds, // All snippet products are considered matched
matchedProducts: snippet.skuIds, // All snippet products are considered matched
snippetCode: snippet.snippetCode,
};
});
@ -451,7 +451,7 @@ export const vendorSnippetsRouter = router({
id: snippet.id,
snippetCode: snippet.snippetCode,
slotId: snippet.slotId,
productIds: snippet.productIds,
skuIds: snippet.skuIds,
validTill: snippet.validTill?.toISOString(),
createdAt: snippet.createdAt.toISOString(),
isPermanent: snippet.isPermanent,
@ -589,14 +589,14 @@ export const vendorSnippetsRouter = router({
const status = order.orderStatus;
if (status[0]?.isCancelled) return false;
const orderProductIds = order.orderItems.map(item => item.productId);
return snippet.productIds.some(productId => orderProductIds.includes(productId));
return snippet.skuIds.some(productId => orderProductIds.includes(productId));
});
// Format the response
const formattedOrders = filteredOrders.map(order => {
// Filter orderItems to only include products attached to the snippet
const attachedOrderItems = order.orderItems.filter(item =>
snippet.productIds.includes(item.productId)
snippet.skuIds.includes(item.productId)
);
const products = attachedOrderItems.map(item => ({
@ -624,7 +624,7 @@ export const vendorSnippetsRouter = router({
sequence: order.slot.deliverySequence,
} : null,
products,
matchedProducts: snippet.productIds,
matchedProducts: snippet.skuIds,
snippetCode: snippet.snippetCode,
};
});
@ -636,7 +636,7 @@ export const vendorSnippetsRouter = router({
id: snippet.id,
snippetCode: snippet.snippetCode,
slotId: snippet.slotId,
productIds: snippet.productIds,
skuIds: snippet.skuIds,
validTill: snippet.validTill?.toISOString(),
createdAt: snippet.createdAt ? snippet.createdAt.toISOString() : new Date(0).toISOString(),
isPermanent: snippet.isPermanent,

View file

@ -87,10 +87,10 @@ export const userCouponRouter = router({
}),
getProductCoupons: protectedProcedure
.input(z.object({ productId: z.number().int().positive() }))
.input(z.object({ skuId: z.number().int().positive() }))
.query(async ({ input, ctx }): Promise<UserEligibleCouponsResponse> => {
const userId = ctx.user.userId;
const { productId } = input;
const { skuId } = input;
// Get all active, non-expired coupons
const allCoupons = await getUserActiveCouponsWithRelationsInDb(userId)
@ -129,7 +129,7 @@ export const userCouponRouter = router({
const userApplicable = !coupon.isUserBased || applicableUsers.some(au => au.userId === userId);
const applicableProducts = coupon.applicableProducts || [];
const productApplicable = applicableProducts.length === 0 || applicableProducts.some(ap => ap.productId === productId);
const productApplicable = applicableProducts.length === 0 || applicableProducts.some(ap => ap.skuId === skuId);
return userApplicable && productApplicable;
});
@ -248,7 +248,7 @@ export const userCouponRouter = router({
discountPercent: reservedCoupon.discountPercent,
flatDiscount: reservedCoupon.flatDiscount,
minOrder: reservedCoupon.minOrder,
productIds: reservedCoupon.productIds,
skuIds: reservedCoupon.skuIds,
maxValue: reservedCoupon.maxValue,
isApplyForAll: false,
validTill: reservedCoupon.validTill,

View file

@ -5,3 +5,6 @@
--file dumps/latest.sql --remote
# run a single file
wrangler d1 execute freshyo-dev \
--config wrangler.dev.toml \
--file ../../packages/db_helper_sqlite/drizzle/0002_sku_split.sql

View file

@ -222,7 +222,7 @@ const ProductCard: React.FC<ProductCardProps> = ({
)}
</View>
<View style={tw`flex-row items-center mb-2`}>
<MyText style={tw`text-gray-500 text-xs font-medium`}>Quantity: <MyText style={tw`text-[#f81260] font-semibold`}>{formatQuantity(item.productQuantity || 1, item.unitNotation).display}</MyText></MyText>
<MyText style={tw`text-gray-500 text-xs font-medium`}>Quantity: <MyText style={tw`text-[#f81260] font-semibold`}>{item.unitNotation}</MyText></MyText>
</View>
{showDeliveryInfo && displayDeliveryDate && (

View file

@ -278,7 +278,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
<MyText style={tw`text-3xl font-bold text-gray-900`}>
{productDetail.price}
</MyText>
<MyText style={tw`text-gray-500 text-lg mb-1 ml-1`}>/ {formatQuantity(productDetail.productQuantity || 1, productDetail.unitNotation).display}</MyText>
<MyText style={tw`text-gray-500 text-lg mb-1 ml-1`}>/ {productDetail.unitNotation}</MyText>
{/* Show market price discount if available */}
{productDetail.marketPrice && (
@ -295,7 +295,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
{productAvailability?.isFlashAvailable && productDetail.flashPrice && productDetail.flashPrice !== productDetail.price && (
<View style={tw`mt-1`}>
<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} / {productDetail.unitNotation}
</MyText>
</View>
)}
@ -462,7 +462,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
{productDetail.specialDeals.map((deal: { quantity: string; price: string; validTill: string }, index: number) => (
<View key={index} style={tw`flex-row justify-between items-center p-3 bg-amber-50 rounded-xl border border-amber-100 mb-2`}>
<MyText style={tw`text-amber-900 font-medium`}>Buy {deal.quantity} {formatQuantity(parseFloat(deal.quantity), productDetail.unitNotation).display}</MyText>
<MyText style={tw`text-amber-900 font-medium`}>Buy {deal.quantity} {productDetail.unitNotation}</MyText>
<MyText style={tw`text-amber-900 font-bold text-lg`}>{deal.price}</MyText>
</View>
))}

View file

@ -317,7 +317,7 @@ const CompactProductCard = ({
{item.marketPrice && Number(item.marketPrice) > Number(item.price) && (
<MyText style={tw`text-gray-400 text-xs ml-1 line-through`}>{item.marketPrice}</MyText>
)}
<MyText style={tw`text-gray-600 text-xs ml-1`}>Quantity: <MyText style={tw`text-[#f81260] font-semibold`}>{formatQuantity(item.productQuantity || 1, item.unit || item.unitNotation).display}</MyText></MyText>
<MyText style={tw`text-gray-600 text-xs ml-1`}>Quantity: <MyText style={tw`text-[#f81260] font-semibold`}>{item.unit || item.unitNotation}</MyText></MyText>
</View>
</View>
</View>

View file

@ -461,12 +461,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
</MyText>
<MyText style={tw`text-xs text-gray-500 mr-2`}>
{(() => {
const qty = product?.productQuantity || 1;
const unit = product?.unitNotation || '';
if (unit?.toLowerCase() === 'kg' && qty < 1) {
return `${Math.round(qty * 1000)}g`;
}
return `${qty}${unit}`;
return unit;
})()}
</MyText>

View file

@ -60,12 +60,12 @@ const formatTimeRange = (deliveryTime: string | Date) => {
};
// Product name component with quantity
const ProductNameWithQuantity = ({ name, productQuantity, unitNotation }: { name: string; productQuantity: number; unitNotation: string }) => {
const ProductNameWithQuantity = ({ name, unitNotation }: { name: string; unitNotation: string }) => {
const truncatedName = name.length > 25 ? name.substring(0, 25) + '...' : name;
const unit = unitNotation ? ` ${unitNotation}` : '';
return (
<MyText style={tw`text-slate-900 font-extrabold text-sm flex-1`} numberOfLines={1}>
{truncatedName} <MyText style={tw`text-slate-500 font-medium text-xs`}>({productQuantity}{unit})</MyText>
{truncatedName} <MyText style={tw`text-slate-500 font-medium text-xs`}>({unit})</MyText>
</MyText>
);
};
@ -272,7 +272,7 @@ const FloatingCartBar: React.FC<FloatingCartBarProps> = ({
style={tw`w-8 h-8 rounded-lg bg-slate-50 border border-slate-100`}
/>
<MyText style={tw`text-gray-500 text-[9px] font-medium mt-1`}>
<MyText style={tw`text-[#f81260] font-semibold`}>{formatQuantity(productsById[item.skuId]?.productQuantity || 1, productsById[item.skuId]?.unitNotation || '').display}</MyText>
<MyText style={tw`text-[#f81260] font-semibold`}>{productsById[item.skuId]?.unitNotation || ''}</MyText>
</MyText>
</View>
@ -280,7 +280,6 @@ const FloatingCartBar: React.FC<FloatingCartBarProps> = ({
<View style={tw`flex-row items-center justify-between mb-1`}>
<ProductNameWithQuantity
name={productsById[item.skuId]?.name || ''}
productQuantity={productsById[item.skuId]?.productQuantity || 0}
unitNotation={productsById[item.skuId]?.unitNotation || ''}
/>
<MiniQuantifier

View file

@ -152,7 +152,7 @@ export default function AddToCartDialog() {
<MyText style={tw`text-xl font-bold text-gray-900`}>Select Delivery Slot</MyText>
{product?.name && (
<MyText style={tw`text-sm text-gray-500`}>
{product.name} ({product.productQuantity}{product.unitNotation ? ` ${product.unitNotation}` : ''})
{product.name} ({product.unitNotation ? ` ${product.unitNotation}` : ''})
</MyText>
)}
</View>

View file

@ -52,11 +52,7 @@ INSERT INTO `sku_features` (`sku_id`, `feature_name`, `feature_value`)
SELECT
`ps`.`id`,
'quantity',
CASE
WHEN CAST(`pi`.`product_quantity` AS INTEGER) = `pi`.`product_quantity`
THEN CAST(CAST(`pi`.`product_quantity` AS INTEGER) AS TEXT)
ELSE CAST(`pi`.`product_quantity` AS TEXT)
END || COALESCE(`u`.`short_notation`, '')
(CAST(`pi`.`product_quantity` AS TEXT)) || COALESCE(`u`.`short_notation`, '')
FROM `product_info` `pi`
JOIN `product_skus` `ps` ON `ps`.`product_id` = `pi`.`id`
LEFT JOIN `units` `u` ON `u`.`id` = `pi`.`unit_id`;

View file

@ -82,6 +82,7 @@ export {
createSpecialDealsForProduct,
updateProductDeals,
replaceProductTags,
mergeSkus,
toggleProductOutOfStock,
updateSlotProducts,
getSlotProductIds,

View file

@ -8,7 +8,7 @@ export interface Banner {
name: string
imageUrl: string
description: string | null
productIds: number[] | null
skuIds: number[] | null
redirectUrl: string | null
serialNum: number | null
isActive: boolean
@ -28,7 +28,7 @@ export async function getBanners(): Promise<Banner[]> {
name: banner.name,
imageUrl: banner.imageUrl,
description: banner.description,
productIds: banner.productIds || [],
skuIds: banner.skuIds || [],
redirectUrl: banner.redirectUrl,
serialNum: banner.serialNum,
isActive: banner.isActive,
@ -49,7 +49,7 @@ export async function getBannerById(id: number): Promise<Banner | null> {
name: banner.name,
imageUrl: banner.imageUrl,
description: banner.description,
productIds: banner.productIds || [],
skuIds: banner.skuIds || [],
redirectUrl: banner.redirectUrl,
serialNum: banner.serialNum,
isActive: banner.isActive,
@ -65,7 +65,7 @@ export async function createBanner(input: CreateBannerInput): Promise<Banner> {
name: input.name,
imageUrl: input.imageUrl,
description: input.description,
productIds: input.productIds || [],
skuIds: input.skuIds || [],
redirectUrl: input.redirectUrl,
serialNum: input.serialNum,
isActive: input.isActive,
@ -76,7 +76,7 @@ export async function createBanner(input: CreateBannerInput): Promise<Banner> {
name: banner.name,
imageUrl: banner.imageUrl,
description: banner.description,
productIds: banner.productIds || [],
skuIds: banner.skuIds || [],
redirectUrl: banner.redirectUrl,
serialNum: banner.serialNum,
isActive: banner.isActive,
@ -101,7 +101,7 @@ export async function updateBanner(id: number, input: UpdateBannerInput): Promis
name: banner.name,
imageUrl: banner.imageUrl,
description: banner.description,
productIds: banner.productIds || [],
skuIds: banner.skuIds || [],
redirectUrl: banner.redirectUrl,
serialNum: banner.serialNum,
isActive: banner.isActive,

View file

@ -9,7 +9,7 @@ export interface Coupon {
discountPercent: string | null
flatDiscount: string | null
minOrder: string | null
productIds: number[] | null
skuIds: number[] | null
maxValue: string | null
isApplyForAll: boolean
validTill: Date | null
@ -51,7 +51,9 @@ export async function getAllCoupons(
},
applicableProducts: {
with: {
product: true,
sku: {
with: { product: true },
},
},
},
},
@ -77,7 +79,9 @@ export async function getCouponById(id: number): Promise<any | null> {
},
applicableProducts: {
with: {
product: true,
sku: {
with: { product: true },
},
},
},
},
@ -90,7 +94,7 @@ export interface CreateCouponInput {
discountPercent?: string
flatDiscount?: string
minOrder?: string
productIds?: number[] | null
skuIds?: number[] | null
maxValue?: string
isApplyForAll: boolean
validTill?: Date
@ -111,7 +115,7 @@ export async function createCouponWithRelations(
discountPercent: input.discountPercent,
flatDiscount: input.flatDiscount,
minOrder: input.minOrder,
productIds: input.productIds,
skuIds: input.skuIds,
createdBy: input.createdBy,
maxValue: input.maxValue,
isApplyForAll: input.isApplyForAll,
@ -131,9 +135,9 @@ export async function createCouponWithRelations(
if (applicableProducts && applicableProducts.length > 0) {
await tx.insert(couponApplicableProducts).values(
applicableProducts.map(productId => ({
applicableProducts.map(skuId => ({
couponId: coupon.id,
productId,
skuId,
}))
)
}
@ -148,7 +152,7 @@ export interface UpdateCouponInput {
discountPercent?: string
flatDiscount?: string
minOrder?: string
productIds?: number[] | null
skuIds?: number[] | null
maxValue?: string
isApplyForAll?: boolean
validTill?: Date | null
@ -187,9 +191,9 @@ export async function updateCouponWithRelations(
await tx.delete(couponApplicableProducts).where(eq(couponApplicableProducts.couponId, id))
if (applicableProducts.length > 0) {
await tx.insert(couponApplicableProducts).values(
applicableProducts.map(productId => ({
applicableProducts.map(skuId => ({
couponId: id,
productId,
skuId,
}))
)
}
@ -319,7 +323,7 @@ export async function createReservedCouponWithProducts(
discountPercent: input.discountPercent,
flatDiscount: input.flatDiscount,
minOrder: input.minOrder,
productIds: input.productIds,
skuIds: input.skuIds,
maxValue: input.maxValue,
validTill: input.validTill,
maxLimitForUser: input.maxLimitForUser,
@ -329,9 +333,9 @@ export async function createReservedCouponWithProducts(
if (applicableProducts && applicableProducts.length > 0) {
await tx.insert(couponApplicableProducts).values(
applicableProducts.map(productId => ({
applicableProducts.map(skuId => ({
couponId: coupon.id,
productId,
skuId,
}))
)
}

View file

@ -605,7 +605,7 @@ export async function rebalanceSlots(slotIds: number[]): Promise<AdminRebalanceS
with: {
orderItems: {
with: {
product: true,
sku: true,
},
},
couponUsages: {
@ -618,14 +618,14 @@ export async function rebalanceSlots(slotIds: number[]): Promise<AdminRebalanceS
const processedOrdersData = ordersList.map((order: any) => {
let newTotal = order.orderItems.reduce((acc: number, item: any) => {
const latestPrice = +item.product.price
const latestPrice = +item.sku.price
const amount = latestPrice * Number(item.quantity)
return acc + amount
}, 0)
order.orderItems.forEach((item: any) => {
item.price = item.product.price
item.discountedPrice = item.product.price
item.price = item.sku.price
item.discountedPrice = item.sku.price
})
const coupon = order.couponUsages[0]?.coupon

View file

@ -14,6 +14,14 @@ import {
productTagInfo,
users,
storeInfo,
cartItems,
orderItems,
coupons,
reservedCoupons,
vendorSnippets,
homeBanners,
keyValStore,
couponApplicableProducts,
} from '../db/schema'
import { and, desc, eq, inArray, sql } from 'drizzle-orm'
import type { InferInsertModel, InferSelectModel } from 'drizzle-orm'
@ -969,3 +977,128 @@ export async function replaceProductTags(productId: number, tagIds: number[]): P
await db.insert(productTags).values(tagAssociations)
}
export async function mergeSkus(fromSkuId: number, toSkuId: number) {
if (fromSkuId === toSkuId) {
return { fromSkuId, toSkuId, message: 'SKUs are the same, no merge needed', counts: {} }
}
const fromSku = await db.query.productSkus.findFirst({ where: eq(productSkus.id, fromSkuId) })
if (!fromSku) throw new Error(`SKU ${fromSkuId} not found`)
const toSku = await db.query.productSkus.findFirst({ where: eq(productSkus.id, toSkuId) })
if (!toSku) throw new Error(`SKU ${toSkuId} not found`)
const counts: Record<string, number> = {}
// 1. order_items — direct update
const orderItemsResult = await db.update(orderItems)
.set({ skuId: toSkuId })
.where(eq(orderItems.skuId, fromSkuId))
counts.orderItems = orderItemsResult.changes ?? 0
// 2. special_deals — direct update
const specialDealsResult = await db.update(specialDeals)
.set({ skuId: toSkuId })
.where(eq(specialDeals.skuId, fromSkuId))
counts.specialDeals = specialDealsResult.changes ?? 0
// 3. cart_items — delete all with fromSkuId
const cartResult = await db.delete(cartItems)
.where(eq(cartItems.skuId, fromSkuId))
counts.cartItems = cartResult.changes ?? 0
// 4. coupon_applicable_products — update, but handle unique constraint
// First delete rows where (coupon_id, toSkuId) already exists
const existingCapRows = await db.query.couponApplicableProducts.findMany({
where: eq(couponApplicableProducts.skuId, toSkuId),
columns: { couponId: true },
})
const existingCouponIds = new Set(existingCapRows.map((r) => r.couponId))
if (existingCouponIds.size > 0) {
const dupResult = await db.delete(couponApplicableProducts)
.where(
and(
eq(couponApplicableProducts.skuId, fromSkuId),
inArray(couponApplicableProducts.couponId, Array.from(existingCouponIds))
)
)
counts.couponDedupDeleted = dupResult.changes ?? 0
}
// Now update remaining rows
const capResult = await db.update(couponApplicableProducts)
.set({ skuId: toSkuId })
.where(eq(couponApplicableProducts.skuId, fromSkuId))
counts.couponApplicable = capResult.changes ?? 0
// 5. JSON arrays — remap fromSkuId to toSkuId
const jsonTables: Array<{ table: any; column: string; name: string }> = [
{ table: deliverySlotInfo, column: 'skuIds', name: 'deliverySlotInfo' },
{ table: homeBanners, column: 'skuIds', name: 'homeBanners' },
{ table: coupons, column: 'skuIds', name: 'coupons' },
{ table: reservedCoupons, column: 'skuIds', name: 'reservedCoupons' },
{ table: vendorSnippets, column: 'skuIds', name: 'vendorSnippets' },
]
for (const { table, column, name } of jsonTables) {
const rows = await db.select({ id: table.id, ids: table[column] }).from(table)
let updated = 0
for (const row of rows) {
const ids: number[] = (row.ids as number[]) || []
if (!ids.includes(fromSkuId)) continue
const newIds = ids.map((id) => (id === fromSkuId ? toSkuId : id))
const deduped = [...new Set(newIds)]
if (deduped.length !== ids.length || deduped.some((id, i) => id !== ids[i])) {
await db.update(table).set({ [column]: deduped } as any).where(eq(table.id, row.id))
updated++
}
}
counts[name] = updated
}
// popularItems in key_val_store
const kvRow = await db.query.keyValStore.findFirst({
where: eq(keyValStore.key, 'popularItems'),
})
if (kvRow && kvRow.value) {
try {
const arr: number[] = JSON.parse(kvRow.value)
if (arr.includes(fromSkuId)) {
const newArr = [...new Set(arr.map((id) => (id === fromSkuId ? toSkuId : id)))]
await db.update(keyValStore)
.set({ value: JSON.stringify(newArr) })
.where(eq(keyValStore.key, 'popularItems'))
counts.popularItems = 1
}
} catch { /* value not valid JSON, skip */ }
}
// 6. Delete SKU features and the SKU itself
const featuresResult = await db.delete(skuFeatures).where(eq(skuFeatures.skuId, fromSkuId))
counts.skuFeatures = featuresResult.changes ?? 0
const skuResult = await db.delete(productSkus).where(eq(productSkus.id, fromSkuId))
counts.productSkus = skuResult.changes ?? 0
// 7. Delete orphaned product
const remainingSkus = await db.query.productSkus.findMany({
where: eq(productSkus.productId, fromSku.productId),
columns: { id: true },
})
let orphanedProductId: number | undefined
if (remainingSkus.length === 0) {
await db.delete(productInfo).where(eq(productInfo.id, fromSku.productId))
orphanedProductId = fromSku.productId
counts.orphanedProduct = 1
}
return {
fromSkuId,
toSkuId,
orphanedProductId,
counts,
}
}

View file

@ -1,5 +1,5 @@
import { db } from '../db/db_index'
import { vendorSnippets, deliverySlotInfo, productInfo, orders, orderItems, orderStatus } from '../db/schema'
import { vendorSnippets, deliverySlotInfo, productInfo, productSkus, orders, orderItems, orderStatus } from '../db/schema'
import { desc, eq, inArray } from 'drizzle-orm'
import type { InferSelectModel } from 'drizzle-orm'
import type {
@ -19,7 +19,7 @@ const mapVendorSnippet = (snippet: VendorSnippetRow): AdminVendorSnippet => ({
id: snippet.id,
snippetCode: snippet.snippetCode,
slotId: snippet.slotId ?? null,
productIds: snippet.productIds || [],
skuIds: snippet.skuIds || [],
isPermanent: snippet.isPermanent,
validTill: coerceDate(snippet.validTill),
createdAt: coerceDate(snippet.createdAt) ?? new Date(0),
@ -91,14 +91,14 @@ export async function getAllVendorSnippets(): Promise<AdminVendorSnippetWithSlot
export async function createVendorSnippet(input: {
snippetCode: string
slotId?: number
productIds: number[]
skuIds: number[]
isPermanent: boolean
validTill?: Date
}): Promise<AdminVendorSnippet> {
const [result] = await db.insert(vendorSnippets).values({
snippetCode: input.snippetCode,
slotId: input.slotId,
productIds: input.productIds,
skuIds: input.skuIds,
isPermanent: input.isPermanent,
validTill: input.validTill,
}).returning()
@ -109,7 +109,7 @@ export async function createVendorSnippet(input: {
export async function updateVendorSnippet(id: number, updates: {
snippetCode?: string
slotId?: number | null
productIds?: number[]
skuIds?: number[]
isPermanent?: boolean
validTill?: Date | null
}): Promise<AdminVendorSnippet | null> {
@ -129,14 +129,17 @@ export async function deleteVendorSnippet(id: number): Promise<AdminVendorSnippe
return result ? mapVendorSnippet(result) : null
}
export async function getProductsByIds(productIds: number[]): Promise<AdminVendorSnippetProduct[]> {
const products = await db.query.productInfo.findMany({
where: inArray(productInfo.id, productIds),
columns: { id: true, name: true },
export async function getProductsByIds(skuIds: number[]): Promise<AdminVendorSnippetProduct[]> {
const skus = await db.query.productSkus.findMany({
where: inArray(productSkus.id, skuIds),
with: { product: { columns: { name: true } } },
columns: { id: true },
})
const prods = products.map(mapProductSummary)
return prods
return skus.map((sku) => ({
id: sku.id,
name: sku.product?.name ?? 'Unknown',
})) as AdminVendorSnippetProduct[]
}
export async function getVendorSlotById(slotId: number): Promise<AdminDeliverySlot | null> {

View file

@ -9,7 +9,7 @@ export interface Coupon {
discountPercent: string | null;
flatDiscount: string | null;
minOrder: string | null;
productIds: number[] | null;
skuIds: number[] | null;
maxValue: string | null;
isApplyForAll: boolean;
validTill: Date | null;
@ -252,7 +252,7 @@ export interface CreateCouponInput {
discountPercent?: string;
flatDiscount?: string;
minOrder?: string;
productIds?: number[] | null;
skuIds?: number[] | null;
maxValue?: string;
isApplyForAll: boolean;
validTill?: Date;
@ -274,7 +274,7 @@ export async function createCouponWithRelations(
discountPercent: input.discountPercent,
flatDiscount: input.flatDiscount,
minOrder: input.minOrder,
productIds: input.productIds,
skuIds: input.skuIds,
createdBy: input.createdBy,
maxValue: input.maxValue,
isApplyForAll: input.isApplyForAll,
@ -296,9 +296,9 @@ export async function createCouponWithRelations(
// Insert applicable products
if (applicableProducts && applicableProducts.length > 0) {
await tx.insert(couponApplicableProducts).values(
applicableProducts.map(productId => ({
applicableProducts.map(skuId => ({
couponId: coupon.id,
productId,
skuId,
}))
);
}
@ -310,7 +310,7 @@ export async function createCouponWithRelations(
discountPercent: coupon.discountPercent,
flatDiscount: coupon.flatDiscount,
minOrder: coupon.minOrder,
productIds: coupon.productIds,
skuIds: coupon.skuIds,
maxValue: coupon.maxValue,
isApplyForAll: coupon.isApplyForAll,
validTill: coupon.validTill,
@ -329,7 +329,7 @@ export interface UpdateCouponInput {
discountPercent?: string;
flatDiscount?: string;
minOrder?: string;
productIds?: number[] | null;
skuIds?: number[] | null;
maxValue?: string;
isApplyForAll?: boolean;
validTill?: Date | null;
@ -371,9 +371,9 @@ export async function updateCouponWithRelations(
await tx.delete(couponApplicableProducts).where(eq(couponApplicableProducts.couponId, id));
if (applicableProducts.length > 0) {
await tx.insert(couponApplicableProducts).values(
applicableProducts.map(productId => ({
applicableProducts.map(skuId => ({
couponId: id,
productId,
skuId,
}))
);
}
@ -386,7 +386,7 @@ export async function updateCouponWithRelations(
discountPercent: coupon.discountPercent,
flatDiscount: coupon.flatDiscount,
minOrder: coupon.minOrder,
productIds: coupon.productIds,
skuIds: coupon.skuIds,
maxValue: coupon.maxValue,
isApplyForAll: coupon.isApplyForAll,
validTill: coupon.validTill,
@ -442,7 +442,7 @@ export async function generateCancellationCoupon(
discountPercent: coupon.discountPercent,
flatDiscount: coupon.flatDiscount,
minOrder: coupon.minOrder,
productIds: coupon.productIds,
skuIds: coupon.skuIds,
maxValue: coupon.maxValue,
isApplyForAll: coupon.isApplyForAll,
validTill: coupon.validTill,
@ -461,7 +461,7 @@ export interface CreateReservedCouponInput {
discountPercent?: string;
flatDiscount?: string;
minOrder?: string;
productIds?: number[] | null;
skuIds?: number[] | null;
maxValue?: string;
validTill?: Date;
maxLimitForUser?: number;
@ -480,7 +480,7 @@ export async function createReservedCouponWithProducts(
discountPercent: input.discountPercent,
flatDiscount: input.flatDiscount,
minOrder: input.minOrder,
productIds: input.productIds,
skuIds: input.skuIds,
maxValue: input.maxValue,
validTill: input.validTill,
maxLimitForUser: input.maxLimitForUser,
@ -491,9 +491,9 @@ export async function createReservedCouponWithProducts(
// Insert applicable products if provided
if (applicableProducts && applicableProducts.length > 0) {
await tx.insert(couponApplicableProducts).values(
applicableProducts.map(productId => ({
applicableProducts.map(skuId => ({
couponId: coupon.id,
productId,
skuId,
}))
);
}
@ -577,7 +577,7 @@ export async function createCouponForUser(
discountPercent: coupon.discountPercent,
flatDiscount: coupon.flatDiscount,
minOrder: coupon.minOrder,
productIds: coupon.productIds,
skuIds: coupon.skuIds,
maxValue: coupon.maxValue,
isApplyForAll: coupon.isApplyForAll,
validTill: coupon.validTill,

View file

@ -125,7 +125,7 @@ export async function getAllProductsForCache(): Promise<ProductBasicData[]> {
images: sku.images,
isOutOfStock: sku.isOutOfStock,
storeId: sku.product?.storeId ?? null,
unitNotation: features.map((f) => f.featureValue).join(' '),
unitNotation: features.map((f) => f.featureValue.replace(/\.0(?=\D|$)/g, '')).join(' '),
incrementStep: sku.product?.incrementStep ?? 1,
productQuantity: 1,
isFlashAvailable: sku.isFlashAvailable,
@ -301,7 +301,7 @@ export async function getAllSlotsWithProductsForCache(): Promise<SlotWithProduct
shortDescription: sku.product?.shortDescription ?? null,
price: String(sku.price ?? '0'),
marketPrice: sku.marketPrice ? String(sku.marketPrice) : null,
unitNotation: features.map((f: any) => f.featureValue).join(' '),
unitNotation: features.map((f: any) => f.featureValue.replace(/\.0(?=\D|$)/g, '')).join(' '),
store: sku.product?.store ? {
id: sku.product.store.id,
name: sku.product.store.name,

View file

@ -23,7 +23,7 @@ const mapCoupon = (coupon: CouponRow): UserCoupon => ({
discountPercent: coupon.discountPercent ? coupon.discountPercent.toString() : null,
flatDiscount: coupon.flatDiscount ? coupon.flatDiscount.toString() : null,
minOrder: coupon.minOrder ? coupon.minOrder.toString() : null,
productIds: coupon.productIds,
skuIds: coupon.skuIds,
maxValue: coupon.maxValue ? coupon.maxValue.toString() : null,
isApplyForAll: coupon.isApplyForAll,
validTill: coupon.validTill ?? null,
@ -51,7 +51,7 @@ const mapApplicableUser = (applicable: CouponApplicableUserRow): UserCouponAppli
const mapApplicableProduct = (applicable: CouponApplicableProductRow): UserCouponApplicableProduct => ({
id: applicable.id,
couponId: applicable.couponId,
productId: applicable.productId,
skuId: applicable.skuId,
})
const mapCouponWithRelations = (coupon: CouponRow & {
@ -119,7 +119,7 @@ export async function redeemReservedCoupon(userId: number, reservedCoupon: Reser
discountPercent: reservedCoupon.discountPercent,
flatDiscount: reservedCoupon.flatDiscount,
minOrder: reservedCoupon.minOrder,
productIds: reservedCoupon.productIds,
skuIds: reservedCoupon.skuIds,
maxValue: reservedCoupon.maxValue,
isApplyForAll: false,
validTill: reservedCoupon.validTill,

View file

@ -51,7 +51,7 @@ export async function getProductDetailById(skuId: number): Promise<UserProductDe
longDescription: product?.longDescription ?? null,
price: String(sku.price ?? '0'),
marketPrice: sku.marketPrice ? String(sku.marketPrice) : null,
unitNotation: features.map((f) => f.featureValue).join(' '),
unitNotation: features.map((f) => f.featureValue.replace(/\.0(?=\D|$)/g, '')).join(' '),
images: getStringArray(sku.images),
isOutOfStock: sku.isOutOfStock,
store: storeData ? {

View file

@ -124,8 +124,8 @@ export async function getStoreDetail(storeId: number): Promise<UserStoreDetailDa
price: String(sku.price ?? '0'),
marketPrice: sku.marketPrice ? String(sku.marketPrice) : null,
incrementStep: sku.product?.incrementStep ?? 1,
unit: features.map((f) => f.featureValue).join(' '),
unitNotation: features.map((f) => f.featureValue).join(' '),
unit: features.map((f) => f.featureValue.replace(/\.0(?=\D|$)/g, '')).join(' '),
unitNotation: features.map((f) => f.featureValue.replace(/\.0(?=\D|$)/g, '')).join(' '),
images: getStringArray(sku.images),
isOutOfStock: sku.isOutOfStock,
productQuantity: 1,

View file

@ -8,7 +8,7 @@ export interface Banner {
name: string;
imageUrl: string;
description: string | null;
productIds: number[] | null;
skuIds: number[] | null;
redirectUrl: string | null;
serialNum: number | null;
isActive: boolean;

124
scripts/s3-cleaner.js Normal file
View file

@ -0,0 +1,124 @@
#!/usr/bin/env bun
// s3-cleaner.js — Delete old versioned cache folders from S3/R2
// Usage: bun s3-cleaner.js <version>
// Example: bun s3-cleaner.js 235
// Deletes all objects under api-cache/v-0/ through api-cache/v-234/
// ============================================================
// CREDENTIALS — replace with your actual values
// ============================================================
const S3_ACCESS_KEY_ID = 'YOUR_ACCESS_KEY_ID'
const S3_SECRET_ACCESS_KEY = 'YOUR_SECRET_ACCESS_KEY'
const S3_REGION = 'auto' // 'us-east-1' for AWS, 'auto' for R2
const S3_ENDPOINT = 'https://your-account.r2.cloudflarestorage.com' // S3 or R2 endpoint
const S3_BUCKET = 'your-bucket-name'
const API_CACHE_KEY = 'api-cache' // matches API_CACHE_KEY env var in backend
// ============================================================
const threshold = parseInt(process.argv[2])
if (!threshold || isNaN(threshold) || threshold <= 0) {
console.error('Usage: bun s3-cleaner.js <version>')
console.error('Example: bun s3-cleaner.js 235')
console.error(' Deletes all v-{n} folders where n < 235')
process.exit(1)
}
const cachePrefix = API_CACHE_KEY.endsWith('/') ? API_CACHE_KEY : `${API_CACHE_KEY}/`
const versionPattern = new RegExp(`^${cachePrefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}v-(\\d+)/`)
console.log(`🧹 S3 Cache Cleaner — keeping v-${threshold}+, deleting v-0 through v-${threshold - 1}`)
console.log(` Bucket: ${S3_BUCKET}`)
console.log(` Prefix: ${cachePrefix}`)
const s3 = new Bun.S3Client({
accessKeyId: S3_ACCESS_KEY_ID,
secretAccessKey: S3_SECRET_ACCESS_KEY,
region: S3_REGION,
endpoint: S3_ENDPOINT,
bucket: S3_BUCKET,
})
async function listAllObjects(prefix) {
const allKeys = []
let continuationToken
do {
const options = { prefix, maxKeys: 1000 }
if (continuationToken) options.continuationToken = continuationToken
const result = await s3.list(options)
for (const obj of result.contents) {
allKeys.push(obj.key)
}
continuationToken = result.nextContinuationToken
process.stdout.write(`\r Listed ${allKeys.length} objects...`)
} while (continuationToken)
console.log('')
return allKeys
}
async function deleteObjects(keys) {
let deleted = 0
const batchSize = 1000
for (let i = 0; i < keys.length; i += batchSize) {
const batch = keys.slice(i, i + batchSize)
const objects = batch.map((key) => ({ key }))
const result = await s3.deleteObjects({ objects })
deleted += result.deleted?.length ?? 0
process.stdout.write(`\r Deleted ${deleted}/${keys.length}...`)
}
console.log('')
return deleted
}
// ============================================================
// MAIN
// ============================================================
try {
console.log('\n📋 Listing objects...')
const allObjects = await listAllObjects(cachePrefix)
const toDelete = []
const versionsSeen = new Set()
for (const key of allObjects) {
const match = key.match(versionPattern)
if (match) {
const ver = parseInt(match[1])
if (ver < threshold) {
toDelete.push(key)
}
versionsSeen.add(ver)
}
}
const sortedVersions = [...versionsSeen].sort((a, b) => a - b)
if (sortedVersions.length === 0) {
console.log('\n✅ No cache objects found.')
process.exit(0)
}
console.log(`\n📊 Found versions: v-${sortedVersions[0]} through v-${sortedVersions[sortedVersions.length - 1]}`)
const oldVersionCount = sortedVersions.filter((v) => v < threshold).length
console.log(` To delete: ${toDelete.length} objects across ${oldVersionCount} version(s)`)
console.log(` To keep: v-${threshold}+`)
if (toDelete.length === 0) {
console.log('\n✅ Nothing to delete.')
process.exit(0)
}
console.log('\n❗ Proceeding with deletion...')
const count = await deleteObjects(toDelete)
console.log(`\n✅ Done — deleted ${count} objects.`)
} catch (error) {
console.error(`\n❌ Error: ${error.message}`)
process.exit(1)
}