From f6bcc23ca2284713834f1b7311d4173722b95f7d Mon Sep 17 00:00:00 2001
From: shafi54 <108669266+shafi-aviz@users.noreply.github.com>
Date: Fri, 31 Jul 2026 20:47:35 +0530
Subject: [PATCH] fully functional
---
.../app/(drawer)/coupons/edit/[id].tsx | 2 +-
.../app/(drawer)/dashboard-banners/create.tsx | 4 +-
.../(drawer)/dashboard-banners/edit/[id].tsx | 16 +--
.../app/(drawer)/dashboard-banners/index.tsx | 2 +-
.../app/(drawer)/slots/slot-details.tsx | 4 +-
.../app/(drawer)/vendor-snippets/index.tsx | 4 +-
apps/admin-ui/components/BannerForm.tsx | 8 +-
.../admin-ui/components/VendorSnippetForm.tsx | 16 +--
apps/admin-ui/src/api-hooks/banner.api.ts | 6 +-
apps/admin-ui/src/components/CouponForm.tsx | 2 +-
apps/admin-ui/types/vendor-snippets.ts | 4 +-
apps/backend/package.json | 2 +-
apps/backend/src/sqliteImporter.ts | 1 +
apps/backend/src/stores/banner-store.ts | 8 +-
.../src/trpc/apis/admin-apis/apis/banner.ts | 28 ++--
.../src/trpc/apis/admin-apis/apis/coupon.ts | 30 ++--
.../apis/admin-apis/apis/vendor-snippets.ts | 50 +++----
.../src/trpc/apis/user-apis/apis/coupon.ts | 8 +-
apps/backend/wrangler-commands.md | 3 +
apps/user-ui/components/ProductCard.tsx | 2 +-
apps/user-ui/components/ProductDetail.tsx | 6 +-
apps/user-ui/components/SlotSpecificView.tsx | 2 +-
apps/user-ui/components/cart-page.tsx | 7 +-
apps/user-ui/components/floating-cart-bar.tsx | 7 +-
.../src/components/AddToCartDialog.tsx | 2 +-
.../drizzle/0002_sku_split.sql | 6 +-
packages/db_helper_sqlite/index.ts | 1 +
.../db_helper_sqlite/src/admin-apis/banner.ts | 12 +-
.../db_helper_sqlite/src/admin-apis/coupon.ts | 30 ++--
.../db_helper_sqlite/src/admin-apis/order.ts | 8 +-
.../src/admin-apis/product.ts | 133 ++++++++++++++++++
.../src/admin-apis/vendor-snippets.ts | 25 ++--
.../src/helper_methods/coupon.ts | 32 ++---
.../src/stores/store-helpers.ts | 4 +-
.../db_helper_sqlite/src/user-apis/coupon.ts | 6 +-
.../db_helper_sqlite/src/user-apis/product.ts | 2 +-
.../db_helper_sqlite/src/user-apis/stores.ts | 4 +-
packages/shared/types/banner.types.ts | 2 +-
scripts/s3-cleaner.js | 124 ++++++++++++++++
39 files changed, 436 insertions(+), 177 deletions(-)
create mode 100644 scripts/s3-cleaner.js
diff --git a/apps/admin-ui/app/(drawer)/coupons/edit/[id].tsx b/apps/admin-ui/app/(drawer)/coupons/edit/[id].tsx
index cde58f3..1320290 100644
--- a/apps/admin-ui/app/(drawer)/coupons/edit/[id].tsx
+++ b/apps/admin-ui/app/(drawer)/coupons/edit/[id].tsx
@@ -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
};
diff --git a/apps/admin-ui/app/(drawer)/dashboard-banners/create.tsx b/apps/admin-ui/app/(drawer)/dashboard-banners/create.tsx
index dbacfa1..ba4d0d9 100644
--- a/apps/admin-ui/app/(drawer)/dashboard-banners/create.tsx
+++ b/apps/admin-ui/app/(drawer)/dashboard-banners/create.tsx
@@ -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,
});
diff --git a/apps/admin-ui/app/(drawer)/dashboard-banners/edit/[id].tsx b/apps/admin-ui/app/(drawer)/dashboard-banners/edit/[id].tsx
index 6615f05..4e86bbc 100644
--- a/apps/admin-ui/app/(drawer)/dashboard-banners/edit/[id].tsx
+++ b/apps/admin-ui/app/(drawer)/dashboard-banners/edit/[id].tsx
@@ -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,
});
diff --git a/apps/admin-ui/app/(drawer)/dashboard-banners/index.tsx b/apps/admin-ui/app/(drawer)/dashboard-banners/index.tsx
index 29bc085..9b04ad4 100644
--- a/apps/admin-ui/app/(drawer)/dashboard-banners/index.tsx
+++ b/apps/admin-ui/app/(drawer)/dashboard-banners/index.tsx
@@ -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;
diff --git a/apps/admin-ui/app/(drawer)/slots/slot-details.tsx b/apps/admin-ui/app/(drawer)/slots/slot-details.tsx
index 2369c70..08a6287 100644
--- a/apps/admin-ui/app/(drawer)/slots/slot-details.tsx
+++ b/apps/admin-ui/app/(drawer)/slots/slot-details.tsx
@@ -113,7 +113,7 @@ export default function SlotDetails() {
{
- 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() {
>
- {snippet.productIds.length} products
+ {snippet.skuIds.length} products
diff --git a/apps/admin-ui/app/(drawer)/vendor-snippets/index.tsx b/apps/admin-ui/app/(drawer)/vendor-snippets/index.tsx
index f34cbfa..9c1ab52 100644
--- a/apps/admin-ui/app/(drawer)/vendor-snippets/index.tsx
+++ b/apps/admin-ui/app/(drawer)/vendor-snippets/index.tsx
@@ -192,7 +192,7 @@ const SnippetItem = ({
>
- {snippet.productIds.length} Items
+ {snippet.skuIds.length} Items
@@ -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,
};
diff --git a/apps/admin-ui/components/BannerForm.tsx b/apps/admin-ui/components/BannerForm.tsx
index 8b3258b..033b299 100644
--- a/apps/admin-ui/components/BannerForm.tsx
+++ b/apps/admin-ui/components/BannerForm.tsx
@@ -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({
{
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"
diff --git a/apps/admin-ui/components/VendorSnippetForm.tsx b/apps/admin-ui/components/VendorSnippetForm.tsx
index 2148a77..0d0b2fa 100644
--- a/apps/admin-ui/components/VendorSnippetForm.tsx
+++ b/apps/admin-ui/components/VendorSnippetForm.tsx
@@ -34,7 +34,7 @@ const VendorSnippetForm: React.FC = ({
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 = ({
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 = ({
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 = ({
{/* Product Selection */}
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 && (
- {formik.errors.productIds}
+ {formik.errors.skuIds && formik.touched.skuIds && (
+ {formik.errors.skuIds}
)}
diff --git a/apps/admin-ui/src/api-hooks/banner.api.ts b/apps/admin-ui/src/api-hooks/banner.api.ts
index 70bddb3..cdcefbe 100644
--- a/apps/admin-ui/src/api-hooks/banner.api.ts
+++ b/apps/admin-ui/src/api-hooks/banner.api.ts
@@ -4,7 +4,7 @@ export interface Banner {
name: string;
imageUrl: string;
description?: string;
- productId?: number;
+ skuIds?: number[];
redirectUrl?: string;
serialNum: number;
isActive: boolean;
@@ -16,11 +16,11 @@ export interface CreateBannerPayload {
name: string;
imageUrl: string;
description?: string;
- productId?: number;
+ skuIds?: number[];
redirectUrl?: string;
serialNum: number;
}
export interface UpdateBannerPayload extends Partial {
isActive?: boolean;
-}
\ No newline at end of file
+}
diff --git a/apps/admin-ui/src/components/CouponForm.tsx b/apps/admin-ui/src/components/CouponForm.tsx
index 0568404..9251c54 100644
--- a/apps/admin-ui/src/components/CouponForm.tsx
+++ b/apps/admin-ui/src/components/CouponForm.tsx
@@ -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,
diff --git a/apps/admin-ui/types/vendor-snippets.ts b/apps/admin-ui/types/vendor-snippets.ts
index c62341e..0271492 100644
--- a/apps/admin-ui/types/vendor-snippets.ts
+++ b/apps/admin-ui/types/vendor-snippets.ts
@@ -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;
}
\ No newline at end of file
diff --git a/apps/backend/package.json b/apps/backend/package.json
index ab217f8..8ae1681 100755
--- a/apps/backend/package.json
+++ b/apps/backend/package.json
@@ -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"
},
diff --git a/apps/backend/src/sqliteImporter.ts b/apps/backend/src/sqliteImporter.ts
index d685475..b55fb03 100644
--- a/apps/backend/src/sqliteImporter.ts
+++ b/apps/backend/src/sqliteImporter.ts
@@ -72,6 +72,7 @@ export {
createSpecialDealsForProduct,
updateProductDeals,
replaceProductTags,
+ mergeSkus,
toggleProductOutOfStock,
updateSlotProducts,
getSlotProductIds,
diff --git a/apps/backend/src/stores/banner-store.ts b/apps/backend/src/stores/banner-store.ts
index 4d36f8c..5a5ed7a 100644
--- a/apps/backend/src/stores/banner-store.ts
+++ b/apps/backend/src/stores/banner-store.ts
@@ -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 {
// 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 {
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 {
name: banner.name,
imageUrl: signedImageUrl,
serialNum: banner.serialNum,
- productIds: banner.productIds,
+ skuIds: banner.skuIds,
createdAt: banner.createdAt,
}
})
diff --git a/apps/backend/src/trpc/apis/admin-apis/apis/banner.ts b/apps/backend/src/trpc/apis/admin-apis/apis/banner.ts
index 47a071b..c3daf25 100644
--- a/apps/backend/src/trpc/apis/admin-apis/apis/banner.ts
+++ b/apps/backend/src/trpc/apis/admin-apis/apis/banner.ts
@@ -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,
diff --git a/apps/backend/src/trpc/apis/admin-apis/apis/coupon.ts b/apps/backend/src/trpc/apis/admin-apis/apis/coupon.ts
index 70767c1..e8e9bfd 100644
--- a/apps/backend/src/trpc/apis/admin-apis/apis/coupon.ts
+++ b/apps/backend/src/trpc/apis/admin-apis/apis/coupon.ts
@@ -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 => {
- 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 => {
- 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,
}))
);
}
diff --git a/apps/backend/src/trpc/apis/admin-apis/apis/vendor-snippets.ts b/apps/backend/src/trpc/apis/admin-apis/apis/vendor-snippets.ts
index 9b400ee..196671a 100644
--- a/apps/backend/src/trpc/apis/admin-apis/apis/vendor-snippets.ts
+++ b/apps/backend/src/trpc/apis/admin-apis/apis/vendor-snippets.ts
@@ -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 => {
- 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,
diff --git a/apps/backend/src/trpc/apis/user-apis/apis/coupon.ts b/apps/backend/src/trpc/apis/user-apis/apis/coupon.ts
index c196fc5..a7b66c3 100644
--- a/apps/backend/src/trpc/apis/user-apis/apis/coupon.ts
+++ b/apps/backend/src/trpc/apis/user-apis/apis/coupon.ts
@@ -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 => {
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,
diff --git a/apps/backend/wrangler-commands.md b/apps/backend/wrangler-commands.md
index 25e25a9..40425d1 100644
--- a/apps/backend/wrangler-commands.md
+++ b/apps/backend/wrangler-commands.md
@@ -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
\ No newline at end of file
diff --git a/apps/user-ui/components/ProductCard.tsx b/apps/user-ui/components/ProductCard.tsx
index 6d3e401..91dcad6 100644
--- a/apps/user-ui/components/ProductCard.tsx
+++ b/apps/user-ui/components/ProductCard.tsx
@@ -222,7 +222,7 @@ const ProductCard: React.FC = ({
)}
- Quantity: {formatQuantity(item.productQuantity || 1, item.unitNotation).display}
+ Quantity: {item.unitNotation}
{showDeliveryInfo && displayDeliveryDate && (
diff --git a/apps/user-ui/components/ProductDetail.tsx b/apps/user-ui/components/ProductDetail.tsx
index 02205ca..0feed66 100644
--- a/apps/user-ui/components/ProductDetail.tsx
+++ b/apps/user-ui/components/ProductDetail.tsx
@@ -278,7 +278,7 @@ const ProductDetail: React.FC = ({ productId, isFlashDeliver
₹{productDetail.price}
- / {formatQuantity(productDetail.productQuantity || 1, productDetail.unitNotation).display}
+ / {productDetail.unitNotation}
{/* Show market price discount if available */}
{productDetail.marketPrice && (
@@ -295,7 +295,7 @@ const ProductDetail: React.FC = ({ productId, isFlashDeliver
{productAvailability?.isFlashAvailable && productDetail.flashPrice && productDetail.flashPrice !== productDetail.price && (
- 1 Hr Delivery: ₹{productDetail.flashPrice} / {formatQuantity(productDetail.productQuantity || 1, productDetail.unitNotation).display}
+ 1 Hr Delivery: ₹{productDetail.flashPrice} / {productDetail.unitNotation}
)}
@@ -462,7 +462,7 @@ const ProductDetail: React.FC = ({ productId, isFlashDeliver
{productDetail.specialDeals.map((deal: { quantity: string; price: string; validTill: string }, index: number) => (
- Buy {deal.quantity} {formatQuantity(parseFloat(deal.quantity), productDetail.unitNotation).display}
+ Buy {deal.quantity} • {productDetail.unitNotation}
₹{deal.price}
))}
diff --git a/apps/user-ui/components/SlotSpecificView.tsx b/apps/user-ui/components/SlotSpecificView.tsx
index f4977e8..5e796a7 100644
--- a/apps/user-ui/components/SlotSpecificView.tsx
+++ b/apps/user-ui/components/SlotSpecificView.tsx
@@ -317,7 +317,7 @@ const CompactProductCard = ({
{item.marketPrice && Number(item.marketPrice) > Number(item.price) && (
₹{item.marketPrice}
)}
- Quantity: {formatQuantity(item.productQuantity || 1, item.unit || item.unitNotation).display}
+ Quantity: {item.unit || item.unitNotation}
diff --git a/apps/user-ui/components/cart-page.tsx b/apps/user-ui/components/cart-page.tsx
index 1b4b897..c4202bb 100644
--- a/apps/user-ui/components/cart-page.tsx
+++ b/apps/user-ui/components/cart-page.tsx
@@ -461,12 +461,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
{(() => {
- 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;
})()}
diff --git a/apps/user-ui/components/floating-cart-bar.tsx b/apps/user-ui/components/floating-cart-bar.tsx
index 717278e..905ba43 100644
--- a/apps/user-ui/components/floating-cart-bar.tsx
+++ b/apps/user-ui/components/floating-cart-bar.tsx
@@ -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 (
- {truncatedName} ({productQuantity}{unit})
+ {truncatedName} ({unit})
);
};
@@ -272,7 +272,7 @@ const FloatingCartBar: React.FC = ({
style={tw`w-8 h-8 rounded-lg bg-slate-50 border border-slate-100`}
/>
- {formatQuantity(productsById[item.skuId]?.productQuantity || 1, productsById[item.skuId]?.unitNotation || '').display}
+ {productsById[item.skuId]?.unitNotation || ''}
@@ -280,7 +280,6 @@ const FloatingCartBar: React.FC = ({
Select Delivery Slot
{product?.name && (
- {product.name} ({product.productQuantity}{product.unitNotation ? ` ${product.unitNotation}` : ''})
+ {product.name} ({product.unitNotation ? ` ${product.unitNotation}` : ''})
)}
diff --git a/packages/db_helper_sqlite/drizzle/0002_sku_split.sql b/packages/db_helper_sqlite/drizzle/0002_sku_split.sql
index 6d7c60b..aa127e2 100644
--- a/packages/db_helper_sqlite/drizzle/0002_sku_split.sql
+++ b/packages/db_helper_sqlite/drizzle/0002_sku_split.sql
@@ -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`;
diff --git a/packages/db_helper_sqlite/index.ts b/packages/db_helper_sqlite/index.ts
index 703feb7..f40fb44 100644
--- a/packages/db_helper_sqlite/index.ts
+++ b/packages/db_helper_sqlite/index.ts
@@ -82,6 +82,7 @@ export {
createSpecialDealsForProduct,
updateProductDeals,
replaceProductTags,
+ mergeSkus,
toggleProductOutOfStock,
updateSlotProducts,
getSlotProductIds,
diff --git a/packages/db_helper_sqlite/src/admin-apis/banner.ts b/packages/db_helper_sqlite/src/admin-apis/banner.ts
index 76cdd32..6413bd6 100644
--- a/packages/db_helper_sqlite/src/admin-apis/banner.ts
+++ b/packages/db_helper_sqlite/src/admin-apis/banner.ts
@@ -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 {
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 {
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 {
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 {
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,
diff --git a/packages/db_helper_sqlite/src/admin-apis/coupon.ts b/packages/db_helper_sqlite/src/admin-apis/coupon.ts
index 9b917d4..2e173b3 100644
--- a/packages/db_helper_sqlite/src/admin-apis/coupon.ts
+++ b/packages/db_helper_sqlite/src/admin-apis/coupon.ts
@@ -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 {
},
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,
}))
)
}
diff --git a/packages/db_helper_sqlite/src/admin-apis/order.ts b/packages/db_helper_sqlite/src/admin-apis/order.ts
index 7dd8ac7..259a68b 100644
--- a/packages/db_helper_sqlite/src/admin-apis/order.ts
+++ b/packages/db_helper_sqlite/src/admin-apis/order.ts
@@ -605,7 +605,7 @@ export async function rebalanceSlots(slotIds: number[]): Promise {
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
diff --git a/packages/db_helper_sqlite/src/admin-apis/product.ts b/packages/db_helper_sqlite/src/admin-apis/product.ts
index a8102b6..1275a67 100644
--- a/packages/db_helper_sqlite/src/admin-apis/product.ts
+++ b/packages/db_helper_sqlite/src/admin-apis/product.ts
@@ -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 = {}
+
+ // 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,
+ }
+}
diff --git a/packages/db_helper_sqlite/src/admin-apis/vendor-snippets.ts b/packages/db_helper_sqlite/src/admin-apis/vendor-snippets.ts
index c52e105..b32107c 100644
--- a/packages/db_helper_sqlite/src/admin-apis/vendor-snippets.ts
+++ b/packages/db_helper_sqlite/src/admin-apis/vendor-snippets.ts
@@ -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 {
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 {
@@ -129,14 +129,17 @@ export async function deleteVendorSnippet(id: number): Promise {
- const products = await db.query.productInfo.findMany({
- where: inArray(productInfo.id, productIds),
- columns: { id: true, name: true },
+export async function getProductsByIds(skuIds: number[]): Promise {
+ 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 {
diff --git a/packages/db_helper_sqlite/src/helper_methods/coupon.ts b/packages/db_helper_sqlite/src/helper_methods/coupon.ts
index 4320252..be1c187 100644
--- a/packages/db_helper_sqlite/src/helper_methods/coupon.ts
+++ b/packages/db_helper_sqlite/src/helper_methods/coupon.ts
@@ -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,
diff --git a/packages/db_helper_sqlite/src/stores/store-helpers.ts b/packages/db_helper_sqlite/src/stores/store-helpers.ts
index 90f5701..d0905db 100644
--- a/packages/db_helper_sqlite/src/stores/store-helpers.ts
+++ b/packages/db_helper_sqlite/src/stores/store-helpers.ts
@@ -125,7 +125,7 @@ export async function getAllProductsForCache(): Promise {
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 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,
diff --git a/packages/db_helper_sqlite/src/user-apis/coupon.ts b/packages/db_helper_sqlite/src/user-apis/coupon.ts
index 61b11a2..48f2349 100644
--- a/packages/db_helper_sqlite/src/user-apis/coupon.ts
+++ b/packages/db_helper_sqlite/src/user-apis/coupon.ts
@@ -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,
diff --git a/packages/db_helper_sqlite/src/user-apis/product.ts b/packages/db_helper_sqlite/src/user-apis/product.ts
index 900b7e4..17d8194 100644
--- a/packages/db_helper_sqlite/src/user-apis/product.ts
+++ b/packages/db_helper_sqlite/src/user-apis/product.ts
@@ -51,7 +51,7 @@ export async function getProductDetailById(skuId: number): Promise f.featureValue).join(' '),
+ unitNotation: features.map((f) => f.featureValue.replace(/\.0(?=\D|$)/g, '')).join(' '),
images: getStringArray(sku.images),
isOutOfStock: sku.isOutOfStock,
store: storeData ? {
diff --git a/packages/db_helper_sqlite/src/user-apis/stores.ts b/packages/db_helper_sqlite/src/user-apis/stores.ts
index 9383afb..d5d2a25 100644
--- a/packages/db_helper_sqlite/src/user-apis/stores.ts
+++ b/packages/db_helper_sqlite/src/user-apis/stores.ts
@@ -124,8 +124,8 @@ export async function getStoreDetail(storeId: number): Promise 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,
diff --git a/packages/shared/types/banner.types.ts b/packages/shared/types/banner.types.ts
index a010158..0b8e548 100644
--- a/packages/shared/types/banner.types.ts
+++ b/packages/shared/types/banner.types.ts
@@ -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;
diff --git a/scripts/s3-cleaner.js b/scripts/s3-cleaner.js
new file mode 100644
index 0000000..d60e8f2
--- /dev/null
+++ b/scripts/s3-cleaner.js
@@ -0,0 +1,124 @@
+#!/usr/bin/env bun
+// s3-cleaner.js — Delete old versioned cache folders from S3/R2
+// Usage: bun s3-cleaner.js
+// 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 ')
+ 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)
+}