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) +}