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, maxValue: coupon.maxValue ? parseFloat(coupon.maxValue) : undefined,
validTill: coupon.validTill ? dayjs(coupon.validTill).format('YYYY-MM-DD') : undefined, validTill: coupon.validTill ? dayjs(coupon.validTill).format('YYYY-MM-DD') : undefined,
maxLimitForUser: coupon.maxLimitForUser || undefined, maxLimitForUser: coupon.maxLimitForUser || undefined,
productIds: coupon.productIds, skuIds: coupon.skuIds,
isReservedCoupon: false, // Normal coupons isReservedCoupon: false, // Normal coupons
}; };

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -15,7 +15,7 @@
"deploy:dev": "wrangler deploy --config wrangler.dev.toml", "deploy:dev": "wrangler deploy --config wrangler.dev.toml",
"wrangler:dev": "wrangler dev worker.ts --config wrangler.toml", "wrangler:dev": "wrangler dev worker.ts --config wrangler.toml",
"wrangler:deploy": "wrangler deploy 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: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" "docker:push": "docker push mohdshafiuddin54/health_petal:latest"
}, },

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,3 +5,6 @@
--file dumps/latest.sql --remote --file dumps/latest.sql --remote
# run a single file # 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>
<View style={tw`flex-row items-center mb-2`}> <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> </View>
{showDeliveryInfo && displayDeliveryDate && ( {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`}> <MyText style={tw`text-3xl font-bold text-gray-900`}>
{productDetail.price} {productDetail.price}
</MyText> </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 */} {/* Show market price discount if available */}
{productDetail.marketPrice && ( {productDetail.marketPrice && (
@ -295,7 +295,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
{productAvailability?.isFlashAvailable && productDetail.flashPrice && productDetail.flashPrice !== productDetail.price && ( {productAvailability?.isFlashAvailable && productDetail.flashPrice && productDetail.flashPrice !== productDetail.price && (
<View style={tw`mt-1`}> <View style={tw`mt-1`}>
<MyText style={tw`text-pink-600 text-lg font-bold`}> <MyText style={tw`text-pink-600 text-lg font-bold`}>
1 Hr Delivery: {productDetail.flashPrice} / {formatQuantity(productDetail.productQuantity || 1, productDetail.unitNotation).display} 1 Hr Delivery: {productDetail.flashPrice} / {productDetail.unitNotation}
</MyText> </MyText>
</View> </View>
)} )}
@ -462,7 +462,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
{productDetail.specialDeals.map((deal: { quantity: string; price: string; validTill: string }, index: number) => ( {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`}> <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> <MyText style={tw`text-amber-900 font-bold text-lg`}>{deal.price}</MyText>
</View> </View>
))} ))}

View file

@ -317,7 +317,7 @@ const CompactProductCard = ({
{item.marketPrice && Number(item.marketPrice) > Number(item.price) && ( {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-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>
</View> </View>

View file

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

View file

@ -60,12 +60,12 @@ const formatTimeRange = (deliveryTime: string | Date) => {
}; };
// Product name component with quantity // 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 truncatedName = name.length > 25 ? name.substring(0, 25) + '...' : name;
const unit = unitNotation ? ` ${unitNotation}` : ''; const unit = unitNotation ? ` ${unitNotation}` : '';
return ( return (
<MyText style={tw`text-slate-900 font-extrabold text-sm flex-1`} numberOfLines={1}> <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> </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`} 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-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> </MyText>
</View> </View>
@ -280,7 +280,6 @@ const FloatingCartBar: React.FC<FloatingCartBarProps> = ({
<View style={tw`flex-row items-center justify-between mb-1`}> <View style={tw`flex-row items-center justify-between mb-1`}>
<ProductNameWithQuantity <ProductNameWithQuantity
name={productsById[item.skuId]?.name || ''} name={productsById[item.skuId]?.name || ''}
productQuantity={productsById[item.skuId]?.productQuantity || 0}
unitNotation={productsById[item.skuId]?.unitNotation || ''} unitNotation={productsById[item.skuId]?.unitNotation || ''}
/> />
<MiniQuantifier <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> <MyText style={tw`text-xl font-bold text-gray-900`}>Select Delivery Slot</MyText>
{product?.name && ( {product?.name && (
<MyText style={tw`text-sm text-gray-500`}> <MyText style={tw`text-sm text-gray-500`}>
{product.name} ({product.productQuantity}{product.unitNotation ? ` ${product.unitNotation}` : ''}) {product.name} ({product.unitNotation ? ` ${product.unitNotation}` : ''})
</MyText> </MyText>
)} )}
</View> </View>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -125,7 +125,7 @@ export async function getAllProductsForCache(): Promise<ProductBasicData[]> {
images: sku.images, images: sku.images,
isOutOfStock: sku.isOutOfStock, isOutOfStock: sku.isOutOfStock,
storeId: sku.product?.storeId ?? null, 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, incrementStep: sku.product?.incrementStep ?? 1,
productQuantity: 1, productQuantity: 1,
isFlashAvailable: sku.isFlashAvailable, isFlashAvailable: sku.isFlashAvailable,
@ -301,7 +301,7 @@ export async function getAllSlotsWithProductsForCache(): Promise<SlotWithProduct
shortDescription: sku.product?.shortDescription ?? null, shortDescription: sku.product?.shortDescription ?? null,
price: String(sku.price ?? '0'), price: String(sku.price ?? '0'),
marketPrice: sku.marketPrice ? String(sku.marketPrice) : null, 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 ? { store: sku.product?.store ? {
id: sku.product.store.id, id: sku.product.store.id,
name: sku.product.store.name, name: sku.product.store.name,

View file

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

View file

@ -51,7 +51,7 @@ export async function getProductDetailById(skuId: number): Promise<UserProductDe
longDescription: product?.longDescription ?? null, longDescription: product?.longDescription ?? null,
price: String(sku.price ?? '0'), price: String(sku.price ?? '0'),
marketPrice: sku.marketPrice ? String(sku.marketPrice) : null, 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), images: getStringArray(sku.images),
isOutOfStock: sku.isOutOfStock, isOutOfStock: sku.isOutOfStock,
store: storeData ? { store: storeData ? {

View file

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

View file

@ -8,7 +8,7 @@ export interface Banner {
name: string; name: string;
imageUrl: string; imageUrl: string;
description: string | null; description: string | null;
productIds: number[] | null; skuIds: number[] | null;
redirectUrl: string | null; redirectUrl: string | null;
serialNum: number | null; serialNum: number | null;
isActive: boolean; 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)
}