fully functional
This commit is contained in:
parent
5c2b3aaa67
commit
f6bcc23ca2
39 changed files with 436 additions and 177 deletions
|
|
@ -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
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ export default function SlotDetails() {
|
|||
<View style={tw`flex-row items-center`}>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
const snippetProducts = products.filter(p => snippet.productIds.includes(p.id));
|
||||
const snippetProducts = products.filter(p => snippet.skuIds.includes(p.id));
|
||||
setDialogProducts(snippetProducts);
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
|
|
@ -121,7 +121,7 @@ export default function SlotDetails() {
|
|||
>
|
||||
<MaterialIcons name="shopping-bag" size={14} color="#94A3B8" />
|
||||
<MyText style={tw`text-xs font-medium text-blue-600 ml-1 underline`}>
|
||||
{snippet.productIds.length} products
|
||||
{snippet.skuIds.length} products
|
||||
</MyText>
|
||||
</TouchableOpacity>
|
||||
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ const SnippetItem = ({
|
|||
>
|
||||
<MaterialIcons name="shopping-bag" size={14} color="#94A3B8" />
|
||||
<MyText style={tw`text-xs font-bold text-slate-500 ml-1.5`}>
|
||||
{snippet.productIds.length} Items
|
||||
{snippet.skuIds.length} Items
|
||||
</MyText>
|
||||
<MaterialIcons name="chevron-right" size={14} color="#94A3B8" />
|
||||
</MyTouchableOpacity>
|
||||
|
|
@ -279,7 +279,7 @@ const handleViewProducts = (products: VendorSnippetProduct[]) => {
|
|||
snippetCode: snippet.snippetCode,
|
||||
slotId: snippet.slotId || 0, // Convert null to number for form
|
||||
isPermanent: snippet.isPermanent,
|
||||
productIds: snippet.productIds,
|
||||
skuIds: snippet.skuIds,
|
||||
validTill: snippet.validTill,
|
||||
createdAt: snippet.createdAt,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export interface BannerFormData {
|
|||
name: string;
|
||||
imageUrl: string;
|
||||
description: string;
|
||||
productIds: number[];
|
||||
skuIds: number[];
|
||||
redirectUrl: string;
|
||||
// serialNum removed - will be assigned automatically by backend
|
||||
}
|
||||
|
|
@ -32,7 +32,7 @@ interface BannerFormProps {
|
|||
const validationSchema = Yup.object().shape({
|
||||
name: Yup.string().trim().required('Banner name is required').max(255),
|
||||
description: Yup.string().max(500),
|
||||
productIds: Yup.array()
|
||||
skuIds: Yup.array()
|
||||
.of(Yup.number())
|
||||
.optional(),
|
||||
redirectUrl: Yup.string()
|
||||
|
|
@ -177,10 +177,10 @@ export default function BannerForm({
|
|||
|
||||
<View style={{ marginBottom: 16 }}>
|
||||
<ProductsSelector
|
||||
value={values.productIds}
|
||||
value={values.skuIds}
|
||||
onChange={(value) => {
|
||||
const selectedValues = Array.isArray(value) ? value : [value];
|
||||
setFieldValue('productIds', selectedValues.map(v => Number(v)));
|
||||
setFieldValue('skuIds', selectedValues.map(v => Number(v)));
|
||||
}}
|
||||
multiple={true}
|
||||
label="Select Products"
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ const VendorSnippetForm: React.FC<VendorSnippetFormProps> = ({
|
|||
snippetCode: snippet?.snippetCode || '',
|
||||
slotId: snippet?.slotId?.toString() || '',
|
||||
isPermanent: snippet?.isPermanent || false,
|
||||
productIds: snippet?.productIds?.map(id => id.toString()) || [],
|
||||
skuIds: snippet?.skuIds?.map(id => id.toString()) || [],
|
||||
validTill: snippet?.validTill ? new Date(snippet.validTill) : null,
|
||||
},
|
||||
validate: (values) => {
|
||||
|
|
@ -55,8 +55,8 @@ const VendorSnippetForm: React.FC<VendorSnippetFormProps> = ({
|
|||
errors.slotId = 'Slot selection is required';
|
||||
}
|
||||
|
||||
if (values.productIds.length === 0) {
|
||||
errors.productIds = 'At least one product must be selected';
|
||||
if (values.skuIds.length === 0) {
|
||||
errors.skuIds = 'At least one product must be selected';
|
||||
}
|
||||
|
||||
return errors;
|
||||
|
|
@ -68,7 +68,7 @@ const VendorSnippetForm: React.FC<VendorSnippetFormProps> = ({
|
|||
snippetCode: values.snippetCode,
|
||||
slotId: values.isPermanent ? undefined : parseInt(values.slotId || '0'),
|
||||
isPermanent: values.isPermanent,
|
||||
productIds: values.productIds.map(id => parseInt(id)),
|
||||
skuIds: values.skuIds.map(id => parseInt(id)),
|
||||
validTill: values.validTill ? values.validTill.toISOString() : undefined,
|
||||
};
|
||||
|
||||
|
|
@ -180,15 +180,15 @@ const VendorSnippetForm: React.FC<VendorSnippetFormProps> = ({
|
|||
{/* Product Selection */}
|
||||
<View>
|
||||
<ProductsSelector
|
||||
value={formik.values.productIds.map(id => parseInt(id))}
|
||||
onChange={(selectedProductIds) => formik.setFieldValue('productIds', (selectedProductIds as number[]).map(id => id.toString()))}
|
||||
value={formik.values.skuIds.map(id => parseInt(id))}
|
||||
onChange={(selectedProductIds) => formik.setFieldValue('skuIds', (selectedProductIds as number[]).map(id => id.toString()))}
|
||||
multiple={true}
|
||||
label="Select Products"
|
||||
placeholder="Select products"
|
||||
labelFormat={(product) => `${product.name} (${product.unit})`}
|
||||
/>
|
||||
{formik.errors.productIds && formik.touched.productIds && (
|
||||
<MyText style={tw`text-red-500 text-sm mt-1`}>{formik.errors.productIds}</MyText>
|
||||
{formik.errors.skuIds && formik.touched.skuIds && (
|
||||
<MyText style={tw`text-red-500 text-sm mt-1`}>{formik.errors.skuIds}</MyText>
|
||||
)}
|
||||
</View>
|
||||
|
||||
|
|
|
|||
|
|
@ -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<CreateBannerPayload> {
|
||||
isActive?: boolean;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ export {
|
|||
createSpecialDealsForProduct,
|
||||
updateProductDeals,
|
||||
replaceProductTags,
|
||||
mergeSkus,
|
||||
toggleProductOutOfStock,
|
||||
updateSlotProducts,
|
||||
getSlotProductIds,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ interface Banner {
|
|||
name: string
|
||||
imageUrl: string | null
|
||||
serialNum: number | null
|
||||
productIds: number[] | null
|
||||
skuIds: number[] | null
|
||||
createdAt: Date
|
||||
}
|
||||
|
||||
|
|
@ -46,7 +46,7 @@ export async function initializeBannerStore(): Promise<void> {
|
|||
// name: banner.name,
|
||||
// imageUrl: signedImageUrl,
|
||||
// serialNum: banner.serialNum,
|
||||
// productIds: banner.productIds,
|
||||
// skuIds: banner.skuIds,
|
||||
// createdAt: banner.createdAt,
|
||||
// }
|
||||
//
|
||||
|
|
@ -78,7 +78,7 @@ export async function getBannerById(id: number): Promise<Banner | null> {
|
|||
name: banner.name,
|
||||
imageUrl: signedImageUrl,
|
||||
serialNum: banner.serialNum,
|
||||
productIds: banner.productIds,
|
||||
skuIds: banner.skuIds,
|
||||
createdAt: banner.createdAt,
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -121,7 +121,7 @@ export async function getAllBanners(): Promise<Banner[]> {
|
|||
name: banner.name,
|
||||
imageUrl: signedImageUrl,
|
||||
serialNum: banner.serialNum,
|
||||
productIds: banner.productIds,
|
||||
skuIds: banner.skuIds,
|
||||
createdAt: banner.createdAt,
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ const createCouponBodySchema = z.object({
|
|||
flatDiscount: z.number().optional(),
|
||||
minOrder: z.number().optional(),
|
||||
targetUser: z.number().optional(),
|
||||
productIds: z.array(z.number()).optional().nullable(),
|
||||
skuIds: z.array(z.number()).optional().nullable(),
|
||||
applicableUsers: z.array(z.number()).optional(),
|
||||
applicableProducts: z.array(z.number()).optional(),
|
||||
maxValue: z.number().optional(),
|
||||
|
|
@ -49,7 +49,7 @@ export const couponRouter = router({
|
|||
create: protectedProcedure
|
||||
.input(createCouponBodySchema)
|
||||
.mutation(async ({ input, ctx }): Promise<Coupon> => {
|
||||
const { couponCode, isUserBased, discountPercent, flatDiscount, minOrder, productIds, applicableUsers, applicableProducts, maxValue, isApplyForAll, validTill, maxLimitForUser, exclusiveApply } = input;
|
||||
const { couponCode, isUserBased, discountPercent, flatDiscount, minOrder, skuIds, applicableUsers, applicableProducts, maxValue, isApplyForAll, validTill, maxLimitForUser, exclusiveApply } = input;
|
||||
|
||||
// Validation: ensure at least one discount type is provided
|
||||
if ((!discountPercent && !flatDiscount) || (discountPercent && flatDiscount)) {
|
||||
|
|
@ -101,7 +101,7 @@ export const couponRouter = router({
|
|||
discountPercent: discountPercent?.toString(),
|
||||
flatDiscount: flatDiscount?.toString(),
|
||||
minOrder: minOrder?.toString(),
|
||||
productIds: productIds || null,
|
||||
skuIds: skuIds || null,
|
||||
createdBy: staffUserId,
|
||||
maxValue: maxValue?.toString(),
|
||||
isApplyForAll: isApplyForAll || false,
|
||||
|
|
@ -121,7 +121,7 @@ export const couponRouter = router({
|
|||
discountPercent: discountPercent?.toString(),
|
||||
flatDiscount: flatDiscount?.toString(),
|
||||
minOrder: minOrder?.toString(),
|
||||
productIds: productIds || null,
|
||||
skuIds: skuIds || null,
|
||||
createdBy: staffUserId,
|
||||
maxValue: maxValue?.toString(),
|
||||
isApplyForAll: isApplyForAll || false,
|
||||
|
|
@ -145,9 +145,9 @@ export const couponRouter = router({
|
|||
// Insert applicable products
|
||||
if (applicableProducts && applicableProducts.length > 0) {
|
||||
await db.insert(couponApplicableProducts).values(
|
||||
applicableProducts.map(productId => ({
|
||||
applicableProducts.map(skuId => ({
|
||||
couponId: coupon.id,
|
||||
productId,
|
||||
skuId,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
|
@ -185,7 +185,7 @@ export const couponRouter = router({
|
|||
|
||||
return {
|
||||
...result,
|
||||
productIds: (result.productIds as number[]) || undefined,
|
||||
skuIds: (result.skuIds as number[]) || undefined,
|
||||
applicableUsers: result.applicableUsers.map((au: any) => au.user),
|
||||
applicableProducts: result.applicableProducts.map((ap: any) => ap.product),
|
||||
};
|
||||
|
|
@ -221,7 +221,7 @@ export const couponRouter = router({
|
|||
if (updates.maxLimitForUser !== undefined) updateData.maxLimitForUser = updates.maxLimitForUser;
|
||||
if (updates.exclusiveApply !== undefined) updateData.exclusiveApply = updates.exclusiveApply;
|
||||
if (updates.isInvalidated !== undefined) updateData.isInvalidated = updates.isInvalidated;
|
||||
if (updates.productIds !== undefined) updateData.productIds = updates.productIds;
|
||||
if (updates.skuIds !== undefined) updateData.skuIds = updates.skuIds;
|
||||
|
||||
// Using dbService helper (new implementation)
|
||||
const coupon = await updateCouponWithRelations(
|
||||
|
|
@ -260,9 +260,9 @@ export const couponRouter = router({
|
|||
await db.delete(couponApplicableProducts).where(eq(couponApplicableProducts.couponId, id));
|
||||
if (updates.applicableProducts.length > 0) {
|
||||
await db.insert(couponApplicableProducts).values(
|
||||
updates.applicableProducts.map(productId => ({
|
||||
updates.applicableProducts.map(skuId => ({
|
||||
couponId: id,
|
||||
productId,
|
||||
skuId,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
|
@ -405,7 +405,7 @@ export const couponRouter = router({
|
|||
createReservedCoupon: protectedProcedure
|
||||
.input(createCouponBodySchema)
|
||||
.mutation(async ({ input, ctx }): Promise<any> => {
|
||||
const { couponCode, isUserBased, discountPercent, flatDiscount, minOrder, productIds, applicableProducts, maxValue, validTill, maxLimitForUser, exclusiveApply } = input;
|
||||
const { couponCode, isUserBased, discountPercent, flatDiscount, minOrder, skuIds, applicableProducts, maxValue, validTill, maxLimitForUser, exclusiveApply } = input;
|
||||
|
||||
// Validation: ensure at least one discount type is provided
|
||||
if ((!discountPercent && !flatDiscount) || (discountPercent && flatDiscount)) {
|
||||
|
|
@ -434,7 +434,7 @@ export const couponRouter = router({
|
|||
discountPercent: discountPercent?.toString(),
|
||||
flatDiscount: flatDiscount?.toString(),
|
||||
minOrder: minOrder?.toString(),
|
||||
productIds,
|
||||
skuIds,
|
||||
maxValue: maxValue?.toString(),
|
||||
validTill: validTill ? dayjs(validTill).toDate() : undefined,
|
||||
maxLimitForUser,
|
||||
|
|
@ -452,7 +452,7 @@ export const couponRouter = router({
|
|||
discountPercent: discountPercent?.toString(),
|
||||
flatDiscount: flatDiscount?.toString(),
|
||||
minOrder: minOrder?.toString(),
|
||||
productIds,
|
||||
skuIds,
|
||||
maxValue: maxValue?.toString(),
|
||||
validTill: validTill ? dayjs(validTill).toDate() : undefined,
|
||||
maxLimitForUser,
|
||||
|
|
@ -465,9 +465,9 @@ export const couponRouter = router({
|
|||
// Insert applicable products if provided
|
||||
if (applicableProducts && applicableProducts.length > 0) {
|
||||
await db.insert(couponApplicableProducts).values(
|
||||
applicableProducts.map(productId => ({
|
||||
applicableProducts.map(skuId => ({
|
||||
couponId: coupon.id,
|
||||
productId,
|
||||
skuId,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ import type {
|
|||
const createSnippetSchema = z.object({
|
||||
snippetCode: z.string().min(1, "Snippet code is required"),
|
||||
slotId: z.number().optional(),
|
||||
productIds: z.array(z.number().int().positive()).min(1, "At least one product is required"),
|
||||
skuIds: z.array(z.number().int().positive()).min(1, "At least one product is required"),
|
||||
validTill: z.string().optional(),
|
||||
isPermanent: z.boolean().default(false)
|
||||
});
|
||||
|
|
@ -41,7 +41,7 @@ const updateSnippetSchema = z.object({
|
|||
id: z.number().int().positive(),
|
||||
updates: createSnippetSchema.partial().extend({
|
||||
snippetCode: z.string().min(1).optional(),
|
||||
productIds: z.array(z.number().int().positive()).optional(),
|
||||
skuIds: z.array(z.number().int().positive()).optional(),
|
||||
isPermanent: z.boolean().default(false)
|
||||
}),
|
||||
});
|
||||
|
|
@ -50,7 +50,7 @@ export const vendorSnippetsRouter = router({
|
|||
create: protectedProcedure
|
||||
.input(createSnippetSchema)
|
||||
.mutation(async ({ input, ctx }): Promise<AdminVendorSnippet> => {
|
||||
const { snippetCode, slotId, productIds, validTill, isPermanent } = input;
|
||||
const { snippetCode, slotId, skuIds, validTill, isPermanent } = input;
|
||||
|
||||
// Get staff user ID from auth middleware
|
||||
const staffUserId = ctx.staffUser?.id;
|
||||
|
|
@ -65,8 +65,8 @@ export const vendorSnippetsRouter = router({
|
|||
}
|
||||
}
|
||||
|
||||
const products = await getProductsByIdsInDb(productIds)
|
||||
if (products.length !== productIds.length) {
|
||||
const products = await getProductsByIdsInDb(skuIds)
|
||||
if (products.length !== skuIds.length) {
|
||||
throw new Error("One or more invalid product IDs")
|
||||
}
|
||||
|
||||
|
|
@ -78,7 +78,7 @@ export const vendorSnippetsRouter = router({
|
|||
const result = await createVendorSnippetInDb({
|
||||
snippetCode,
|
||||
slotId,
|
||||
productIds,
|
||||
skuIds,
|
||||
isPermanent,
|
||||
validTill: validTill ? new Date(validTill) : undefined,
|
||||
})
|
||||
|
|
@ -97,9 +97,9 @@ export const vendorSnippetsRouter = router({
|
|||
|
||||
// Validate products exist
|
||||
const products = await db.query.productInfo.findMany({
|
||||
where: inArray(productInfo.id, productIds),
|
||||
where: inArray(productInfo.id, skuIds),
|
||||
});
|
||||
if (products.length !== productIds.length) {
|
||||
if (products.length !== skuIds.length) {
|
||||
throw new Error("One or more invalid product IDs");
|
||||
}
|
||||
|
||||
|
|
@ -114,7 +114,7 @@ export const vendorSnippetsRouter = router({
|
|||
const result = await db.insert(vendorSnippets).values({
|
||||
snippetCode,
|
||||
slotId,
|
||||
productIds,
|
||||
skuIds,
|
||||
isPermanent,
|
||||
validTill: validTill ? new Date(validTill) : undefined,
|
||||
}).returning();
|
||||
|
|
@ -134,7 +134,7 @@ export const vendorSnippetsRouter = router({
|
|||
|
||||
const snippetsWithProducts = await Promise.all(
|
||||
result.map(async (snippet) => {
|
||||
const products = await getProductsByIdsInDb(snippet.productIds)
|
||||
const products = await getProductsByIdsInDb(snippet.skuIds)
|
||||
|
||||
return {
|
||||
...snippet,
|
||||
|
|
@ -156,7 +156,7 @@ export const vendorSnippetsRouter = router({
|
|||
const snippetsWithProducts = await Promise.all(
|
||||
result.map(async (snippet) => {
|
||||
const products = await db.query.productInfo.findMany({
|
||||
where: inArray(productInfo.id, snippet.productIds),
|
||||
where: inArray(productInfo.id, snippet.skuIds),
|
||||
columns: { id: true, name: true },
|
||||
});
|
||||
|
||||
|
|
@ -226,9 +226,9 @@ export const vendorSnippetsRouter = router({
|
|||
}
|
||||
}
|
||||
|
||||
if (updates.productIds) {
|
||||
const products = await getProductsByIdsInDb(updates.productIds)
|
||||
if (products.length !== updates.productIds.length) {
|
||||
if (updates.skuIds) {
|
||||
const products = await getProductsByIdsInDb(updates.skuIds)
|
||||
if (products.length !== updates.skuIds.length) {
|
||||
throw new Error('One or more invalid product IDs')
|
||||
}
|
||||
}
|
||||
|
|
@ -269,11 +269,11 @@ export const vendorSnippetsRouter = router({
|
|||
}
|
||||
|
||||
// Validate products if being updated
|
||||
if (updates.productIds) {
|
||||
if (updates.skuIds) {
|
||||
const products = await db.query.productInfo.findMany({
|
||||
where: inArray(productInfo.id, updates.productIds),
|
||||
where: inArray(productInfo.id, updates.skuIds),
|
||||
});
|
||||
if (products.length !== updates.productIds.length) {
|
||||
if (products.length !== updates.skuIds.length) {
|
||||
throw new Error("One or more invalid product IDs");
|
||||
}
|
||||
}
|
||||
|
|
@ -404,14 +404,14 @@ export const vendorSnippetsRouter = router({
|
|||
const status = order.orderStatus;
|
||||
if (status[0].isCancelled) return false;
|
||||
const orderProductIds = order.orderItems.map(item => item.productId);
|
||||
return snippet.productIds.some(productId => orderProductIds.includes(productId));
|
||||
return snippet.skuIds.some(productId => orderProductIds.includes(productId));
|
||||
});
|
||||
|
||||
// Format the response
|
||||
const formattedOrders = filteredOrders.map(order => {
|
||||
// Filter orderItems to only include products attached to the snippet
|
||||
const attachedOrderItems = order.orderItems.filter(item =>
|
||||
snippet.productIds.includes(item.productId)
|
||||
snippet.skuIds.includes(item.productId)
|
||||
);
|
||||
|
||||
const products = attachedOrderItems.map(item => ({
|
||||
|
|
@ -439,7 +439,7 @@ export const vendorSnippetsRouter = router({
|
|||
sequence: order.slot.deliverySequence,
|
||||
} : null,
|
||||
products,
|
||||
matchedProducts: snippet.productIds, // All snippet products are considered matched
|
||||
matchedProducts: snippet.skuIds, // All snippet products are considered matched
|
||||
snippetCode: snippet.snippetCode,
|
||||
};
|
||||
});
|
||||
|
|
@ -451,7 +451,7 @@ export const vendorSnippetsRouter = router({
|
|||
id: snippet.id,
|
||||
snippetCode: snippet.snippetCode,
|
||||
slotId: snippet.slotId,
|
||||
productIds: snippet.productIds,
|
||||
skuIds: snippet.skuIds,
|
||||
validTill: snippet.validTill?.toISOString(),
|
||||
createdAt: snippet.createdAt.toISOString(),
|
||||
isPermanent: snippet.isPermanent,
|
||||
|
|
@ -589,14 +589,14 @@ export const vendorSnippetsRouter = router({
|
|||
const status = order.orderStatus;
|
||||
if (status[0]?.isCancelled) return false;
|
||||
const orderProductIds = order.orderItems.map(item => item.productId);
|
||||
return snippet.productIds.some(productId => orderProductIds.includes(productId));
|
||||
return snippet.skuIds.some(productId => orderProductIds.includes(productId));
|
||||
});
|
||||
|
||||
// Format the response
|
||||
const formattedOrders = filteredOrders.map(order => {
|
||||
// Filter orderItems to only include products attached to the snippet
|
||||
const attachedOrderItems = order.orderItems.filter(item =>
|
||||
snippet.productIds.includes(item.productId)
|
||||
snippet.skuIds.includes(item.productId)
|
||||
);
|
||||
|
||||
const products = attachedOrderItems.map(item => ({
|
||||
|
|
@ -624,7 +624,7 @@ export const vendorSnippetsRouter = router({
|
|||
sequence: order.slot.deliverySequence,
|
||||
} : null,
|
||||
products,
|
||||
matchedProducts: snippet.productIds,
|
||||
matchedProducts: snippet.skuIds,
|
||||
snippetCode: snippet.snippetCode,
|
||||
};
|
||||
});
|
||||
|
|
@ -636,7 +636,7 @@ export const vendorSnippetsRouter = router({
|
|||
id: snippet.id,
|
||||
snippetCode: snippet.snippetCode,
|
||||
slotId: snippet.slotId,
|
||||
productIds: snippet.productIds,
|
||||
skuIds: snippet.skuIds,
|
||||
validTill: snippet.validTill?.toISOString(),
|
||||
createdAt: snippet.createdAt ? snippet.createdAt.toISOString() : new Date(0).toISOString(),
|
||||
isPermanent: snippet.isPermanent,
|
||||
|
|
|
|||
|
|
@ -87,10 +87,10 @@ export const userCouponRouter = router({
|
|||
}),
|
||||
|
||||
getProductCoupons: protectedProcedure
|
||||
.input(z.object({ productId: z.number().int().positive() }))
|
||||
.input(z.object({ skuId: z.number().int().positive() }))
|
||||
.query(async ({ input, ctx }): Promise<UserEligibleCouponsResponse> => {
|
||||
const userId = ctx.user.userId;
|
||||
const { productId } = input;
|
||||
const { skuId } = input;
|
||||
|
||||
// Get all active, non-expired coupons
|
||||
const allCoupons = await getUserActiveCouponsWithRelationsInDb(userId)
|
||||
|
|
@ -129,7 +129,7 @@ export const userCouponRouter = router({
|
|||
const userApplicable = !coupon.isUserBased || applicableUsers.some(au => au.userId === userId);
|
||||
|
||||
const applicableProducts = coupon.applicableProducts || [];
|
||||
const productApplicable = applicableProducts.length === 0 || applicableProducts.some(ap => ap.productId === productId);
|
||||
const productApplicable = applicableProducts.length === 0 || applicableProducts.some(ap => ap.skuId === skuId);
|
||||
|
||||
return userApplicable && productApplicable;
|
||||
});
|
||||
|
|
@ -248,7 +248,7 @@ export const userCouponRouter = router({
|
|||
discountPercent: reservedCoupon.discountPercent,
|
||||
flatDiscount: reservedCoupon.flatDiscount,
|
||||
minOrder: reservedCoupon.minOrder,
|
||||
productIds: reservedCoupon.productIds,
|
||||
skuIds: reservedCoupon.skuIds,
|
||||
maxValue: reservedCoupon.maxValue,
|
||||
isApplyForAll: false,
|
||||
validTill: reservedCoupon.validTill,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -222,7 +222,7 @@ const ProductCard: React.FC<ProductCardProps> = ({
|
|||
)}
|
||||
</View>
|
||||
<View style={tw`flex-row items-center mb-2`}>
|
||||
<MyText style={tw`text-gray-500 text-xs font-medium`}>Quantity: <MyText style={tw`text-[#f81260] font-semibold`}>{formatQuantity(item.productQuantity || 1, item.unitNotation).display}</MyText></MyText>
|
||||
<MyText style={tw`text-gray-500 text-xs font-medium`}>Quantity: <MyText style={tw`text-[#f81260] font-semibold`}>{item.unitNotation}</MyText></MyText>
|
||||
</View>
|
||||
|
||||
{showDeliveryInfo && displayDeliveryDate && (
|
||||
|
|
|
|||
|
|
@ -278,7 +278,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
|
|||
<MyText style={tw`text-3xl font-bold text-gray-900`}>
|
||||
₹{productDetail.price}
|
||||
</MyText>
|
||||
<MyText style={tw`text-gray-500 text-lg mb-1 ml-1`}>/ {formatQuantity(productDetail.productQuantity || 1, productDetail.unitNotation).display}</MyText>
|
||||
<MyText style={tw`text-gray-500 text-lg mb-1 ml-1`}>/ {productDetail.unitNotation}</MyText>
|
||||
|
||||
{/* Show market price discount if available */}
|
||||
{productDetail.marketPrice && (
|
||||
|
|
@ -295,7 +295,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
|
|||
{productAvailability?.isFlashAvailable && productDetail.flashPrice && productDetail.flashPrice !== productDetail.price && (
|
||||
<View style={tw`mt-1`}>
|
||||
<MyText style={tw`text-pink-600 text-lg font-bold`}>
|
||||
1 Hr Delivery: ₹{productDetail.flashPrice} / {formatQuantity(productDetail.productQuantity || 1, productDetail.unitNotation).display}
|
||||
1 Hr Delivery: ₹{productDetail.flashPrice} / {productDetail.unitNotation}
|
||||
</MyText>
|
||||
</View>
|
||||
)}
|
||||
|
|
@ -462,7 +462,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
|
|||
|
||||
{productDetail.specialDeals.map((deal: { quantity: string; price: string; validTill: string }, index: number) => (
|
||||
<View key={index} style={tw`flex-row justify-between items-center p-3 bg-amber-50 rounded-xl border border-amber-100 mb-2`}>
|
||||
<MyText style={tw`text-amber-900 font-medium`}>Buy {deal.quantity} {formatQuantity(parseFloat(deal.quantity), productDetail.unitNotation).display}</MyText>
|
||||
<MyText style={tw`text-amber-900 font-medium`}>Buy {deal.quantity} • {productDetail.unitNotation}</MyText>
|
||||
<MyText style={tw`text-amber-900 font-bold text-lg`}>₹{deal.price}</MyText>
|
||||
</View>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -317,7 +317,7 @@ const CompactProductCard = ({
|
|||
{item.marketPrice && Number(item.marketPrice) > Number(item.price) && (
|
||||
<MyText style={tw`text-gray-400 text-xs ml-1 line-through`}>₹{item.marketPrice}</MyText>
|
||||
)}
|
||||
<MyText style={tw`text-gray-600 text-xs ml-1`}>Quantity: <MyText style={tw`text-[#f81260] font-semibold`}>{formatQuantity(item.productQuantity || 1, item.unit || item.unitNotation).display}</MyText></MyText>
|
||||
<MyText style={tw`text-gray-600 text-xs ml-1`}>Quantity: <MyText style={tw`text-[#f81260] font-semibold`}>{item.unit || item.unitNotation}</MyText></MyText>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -461,12 +461,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
|
|||
</MyText>
|
||||
<MyText style={tw`text-xs text-gray-500 mr-2`}>
|
||||
{(() => {
|
||||
const qty = product?.productQuantity || 1;
|
||||
const unit = product?.unitNotation || '';
|
||||
if (unit?.toLowerCase() === 'kg' && qty < 1) {
|
||||
return `${Math.round(qty * 1000)}g`;
|
||||
}
|
||||
return `${qty}${unit}`;
|
||||
return unit;
|
||||
})()}
|
||||
</MyText>
|
||||
|
||||
|
|
|
|||
|
|
@ -60,12 +60,12 @@ const formatTimeRange = (deliveryTime: string | Date) => {
|
|||
};
|
||||
|
||||
// Product name component with quantity
|
||||
const ProductNameWithQuantity = ({ name, productQuantity, unitNotation }: { name: string; productQuantity: number; unitNotation: string }) => {
|
||||
const ProductNameWithQuantity = ({ name, unitNotation }: { name: string; unitNotation: string }) => {
|
||||
const truncatedName = name.length > 25 ? name.substring(0, 25) + '...' : name;
|
||||
const unit = unitNotation ? ` ${unitNotation}` : '';
|
||||
return (
|
||||
<MyText style={tw`text-slate-900 font-extrabold text-sm flex-1`} numberOfLines={1}>
|
||||
{truncatedName} <MyText style={tw`text-slate-500 font-medium text-xs`}>({productQuantity}{unit})</MyText>
|
||||
{truncatedName} <MyText style={tw`text-slate-500 font-medium text-xs`}>({unit})</MyText>
|
||||
</MyText>
|
||||
);
|
||||
};
|
||||
|
|
@ -272,7 +272,7 @@ const FloatingCartBar: React.FC<FloatingCartBarProps> = ({
|
|||
style={tw`w-8 h-8 rounded-lg bg-slate-50 border border-slate-100`}
|
||||
/>
|
||||
<MyText style={tw`text-gray-500 text-[9px] font-medium mt-1`}>
|
||||
<MyText style={tw`text-[#f81260] font-semibold`}>{formatQuantity(productsById[item.skuId]?.productQuantity || 1, productsById[item.skuId]?.unitNotation || '').display}</MyText>
|
||||
<MyText style={tw`text-[#f81260] font-semibold`}>{productsById[item.skuId]?.unitNotation || ''}</MyText>
|
||||
</MyText>
|
||||
</View>
|
||||
|
||||
|
|
@ -280,7 +280,6 @@ const FloatingCartBar: React.FC<FloatingCartBarProps> = ({
|
|||
<View style={tw`flex-row items-center justify-between mb-1`}>
|
||||
<ProductNameWithQuantity
|
||||
name={productsById[item.skuId]?.name || ''}
|
||||
productQuantity={productsById[item.skuId]?.productQuantity || 0}
|
||||
unitNotation={productsById[item.skuId]?.unitNotation || ''}
|
||||
/>
|
||||
<MiniQuantifier
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ export default function AddToCartDialog() {
|
|||
<MyText style={tw`text-xl font-bold text-gray-900`}>Select Delivery Slot</MyText>
|
||||
{product?.name && (
|
||||
<MyText style={tw`text-sm text-gray-500`}>
|
||||
{product.name} ({product.productQuantity}{product.unitNotation ? ` ${product.unitNotation}` : ''})
|
||||
{product.name} ({product.unitNotation ? ` ${product.unitNotation}` : ''})
|
||||
</MyText>
|
||||
)}
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -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`;
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ export {
|
|||
createSpecialDealsForProduct,
|
||||
updateProductDeals,
|
||||
replaceProductTags,
|
||||
mergeSkus,
|
||||
toggleProductOutOfStock,
|
||||
updateSlotProducts,
|
||||
getSlotProductIds,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export interface Banner {
|
|||
name: string
|
||||
imageUrl: string
|
||||
description: string | null
|
||||
productIds: number[] | null
|
||||
skuIds: number[] | null
|
||||
redirectUrl: string | null
|
||||
serialNum: number | null
|
||||
isActive: boolean
|
||||
|
|
@ -28,7 +28,7 @@ export async function getBanners(): Promise<Banner[]> {
|
|||
name: banner.name,
|
||||
imageUrl: banner.imageUrl,
|
||||
description: banner.description,
|
||||
productIds: banner.productIds || [],
|
||||
skuIds: banner.skuIds || [],
|
||||
redirectUrl: banner.redirectUrl,
|
||||
serialNum: banner.serialNum,
|
||||
isActive: banner.isActive,
|
||||
|
|
@ -49,7 +49,7 @@ export async function getBannerById(id: number): Promise<Banner | null> {
|
|||
name: banner.name,
|
||||
imageUrl: banner.imageUrl,
|
||||
description: banner.description,
|
||||
productIds: banner.productIds || [],
|
||||
skuIds: banner.skuIds || [],
|
||||
redirectUrl: banner.redirectUrl,
|
||||
serialNum: banner.serialNum,
|
||||
isActive: banner.isActive,
|
||||
|
|
@ -65,7 +65,7 @@ export async function createBanner(input: CreateBannerInput): Promise<Banner> {
|
|||
name: input.name,
|
||||
imageUrl: input.imageUrl,
|
||||
description: input.description,
|
||||
productIds: input.productIds || [],
|
||||
skuIds: input.skuIds || [],
|
||||
redirectUrl: input.redirectUrl,
|
||||
serialNum: input.serialNum,
|
||||
isActive: input.isActive,
|
||||
|
|
@ -76,7 +76,7 @@ export async function createBanner(input: CreateBannerInput): Promise<Banner> {
|
|||
name: banner.name,
|
||||
imageUrl: banner.imageUrl,
|
||||
description: banner.description,
|
||||
productIds: banner.productIds || [],
|
||||
skuIds: banner.skuIds || [],
|
||||
redirectUrl: banner.redirectUrl,
|
||||
serialNum: banner.serialNum,
|
||||
isActive: banner.isActive,
|
||||
|
|
@ -101,7 +101,7 @@ export async function updateBanner(id: number, input: UpdateBannerInput): Promis
|
|||
name: banner.name,
|
||||
imageUrl: banner.imageUrl,
|
||||
description: banner.description,
|
||||
productIds: banner.productIds || [],
|
||||
skuIds: banner.skuIds || [],
|
||||
redirectUrl: banner.redirectUrl,
|
||||
serialNum: banner.serialNum,
|
||||
isActive: banner.isActive,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export interface Coupon {
|
|||
discountPercent: string | null
|
||||
flatDiscount: string | null
|
||||
minOrder: string | null
|
||||
productIds: number[] | null
|
||||
skuIds: number[] | null
|
||||
maxValue: string | null
|
||||
isApplyForAll: boolean
|
||||
validTill: Date | null
|
||||
|
|
@ -51,7 +51,9 @@ export async function getAllCoupons(
|
|||
},
|
||||
applicableProducts: {
|
||||
with: {
|
||||
product: true,
|
||||
sku: {
|
||||
with: { product: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -77,7 +79,9 @@ export async function getCouponById(id: number): Promise<any | null> {
|
|||
},
|
||||
applicableProducts: {
|
||||
with: {
|
||||
product: true,
|
||||
sku: {
|
||||
with: { product: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -90,7 +94,7 @@ export interface CreateCouponInput {
|
|||
discountPercent?: string
|
||||
flatDiscount?: string
|
||||
minOrder?: string
|
||||
productIds?: number[] | null
|
||||
skuIds?: number[] | null
|
||||
maxValue?: string
|
||||
isApplyForAll: boolean
|
||||
validTill?: Date
|
||||
|
|
@ -111,7 +115,7 @@ export async function createCouponWithRelations(
|
|||
discountPercent: input.discountPercent,
|
||||
flatDiscount: input.flatDiscount,
|
||||
minOrder: input.minOrder,
|
||||
productIds: input.productIds,
|
||||
skuIds: input.skuIds,
|
||||
createdBy: input.createdBy,
|
||||
maxValue: input.maxValue,
|
||||
isApplyForAll: input.isApplyForAll,
|
||||
|
|
@ -131,9 +135,9 @@ export async function createCouponWithRelations(
|
|||
|
||||
if (applicableProducts && applicableProducts.length > 0) {
|
||||
await tx.insert(couponApplicableProducts).values(
|
||||
applicableProducts.map(productId => ({
|
||||
applicableProducts.map(skuId => ({
|
||||
couponId: coupon.id,
|
||||
productId,
|
||||
skuId,
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
|
@ -148,7 +152,7 @@ export interface UpdateCouponInput {
|
|||
discountPercent?: string
|
||||
flatDiscount?: string
|
||||
minOrder?: string
|
||||
productIds?: number[] | null
|
||||
skuIds?: number[] | null
|
||||
maxValue?: string
|
||||
isApplyForAll?: boolean
|
||||
validTill?: Date | null
|
||||
|
|
@ -187,9 +191,9 @@ export async function updateCouponWithRelations(
|
|||
await tx.delete(couponApplicableProducts).where(eq(couponApplicableProducts.couponId, id))
|
||||
if (applicableProducts.length > 0) {
|
||||
await tx.insert(couponApplicableProducts).values(
|
||||
applicableProducts.map(productId => ({
|
||||
applicableProducts.map(skuId => ({
|
||||
couponId: id,
|
||||
productId,
|
||||
skuId,
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
|
@ -319,7 +323,7 @@ export async function createReservedCouponWithProducts(
|
|||
discountPercent: input.discountPercent,
|
||||
flatDiscount: input.flatDiscount,
|
||||
minOrder: input.minOrder,
|
||||
productIds: input.productIds,
|
||||
skuIds: input.skuIds,
|
||||
maxValue: input.maxValue,
|
||||
validTill: input.validTill,
|
||||
maxLimitForUser: input.maxLimitForUser,
|
||||
|
|
@ -329,9 +333,9 @@ export async function createReservedCouponWithProducts(
|
|||
|
||||
if (applicableProducts && applicableProducts.length > 0) {
|
||||
await tx.insert(couponApplicableProducts).values(
|
||||
applicableProducts.map(productId => ({
|
||||
applicableProducts.map(skuId => ({
|
||||
couponId: coupon.id,
|
||||
productId,
|
||||
skuId,
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -605,7 +605,7 @@ export async function rebalanceSlots(slotIds: number[]): Promise<AdminRebalanceS
|
|||
with: {
|
||||
orderItems: {
|
||||
with: {
|
||||
product: true,
|
||||
sku: true,
|
||||
},
|
||||
},
|
||||
couponUsages: {
|
||||
|
|
@ -618,14 +618,14 @@ export async function rebalanceSlots(slotIds: number[]): Promise<AdminRebalanceS
|
|||
|
||||
const processedOrdersData = ordersList.map((order: any) => {
|
||||
let newTotal = order.orderItems.reduce((acc: number, item: any) => {
|
||||
const latestPrice = +item.product.price
|
||||
const latestPrice = +item.sku.price
|
||||
const amount = latestPrice * Number(item.quantity)
|
||||
return acc + amount
|
||||
}, 0)
|
||||
|
||||
order.orderItems.forEach((item: any) => {
|
||||
item.price = item.product.price
|
||||
item.discountedPrice = item.product.price
|
||||
item.price = item.sku.price
|
||||
item.discountedPrice = item.sku.price
|
||||
})
|
||||
|
||||
const coupon = order.couponUsages[0]?.coupon
|
||||
|
|
|
|||
|
|
@ -14,6 +14,14 @@ import {
|
|||
productTagInfo,
|
||||
users,
|
||||
storeInfo,
|
||||
cartItems,
|
||||
orderItems,
|
||||
coupons,
|
||||
reservedCoupons,
|
||||
vendorSnippets,
|
||||
homeBanners,
|
||||
keyValStore,
|
||||
couponApplicableProducts,
|
||||
} from '../db/schema'
|
||||
import { and, desc, eq, inArray, sql } from 'drizzle-orm'
|
||||
import type { InferInsertModel, InferSelectModel } from 'drizzle-orm'
|
||||
|
|
@ -969,3 +977,128 @@ export async function replaceProductTags(productId: number, tagIds: number[]): P
|
|||
|
||||
await db.insert(productTags).values(tagAssociations)
|
||||
}
|
||||
|
||||
export async function mergeSkus(fromSkuId: number, toSkuId: number) {
|
||||
if (fromSkuId === toSkuId) {
|
||||
return { fromSkuId, toSkuId, message: 'SKUs are the same, no merge needed', counts: {} }
|
||||
}
|
||||
|
||||
const fromSku = await db.query.productSkus.findFirst({ where: eq(productSkus.id, fromSkuId) })
|
||||
if (!fromSku) throw new Error(`SKU ${fromSkuId} not found`)
|
||||
|
||||
const toSku = await db.query.productSkus.findFirst({ where: eq(productSkus.id, toSkuId) })
|
||||
if (!toSku) throw new Error(`SKU ${toSkuId} not found`)
|
||||
|
||||
const counts: Record<string, number> = {}
|
||||
|
||||
// 1. order_items — direct update
|
||||
const orderItemsResult = await db.update(orderItems)
|
||||
.set({ skuId: toSkuId })
|
||||
.where(eq(orderItems.skuId, fromSkuId))
|
||||
counts.orderItems = orderItemsResult.changes ?? 0
|
||||
|
||||
// 2. special_deals — direct update
|
||||
const specialDealsResult = await db.update(specialDeals)
|
||||
.set({ skuId: toSkuId })
|
||||
.where(eq(specialDeals.skuId, fromSkuId))
|
||||
counts.specialDeals = specialDealsResult.changes ?? 0
|
||||
|
||||
// 3. cart_items — delete all with fromSkuId
|
||||
const cartResult = await db.delete(cartItems)
|
||||
.where(eq(cartItems.skuId, fromSkuId))
|
||||
counts.cartItems = cartResult.changes ?? 0
|
||||
|
||||
// 4. coupon_applicable_products — update, but handle unique constraint
|
||||
// First delete rows where (coupon_id, toSkuId) already exists
|
||||
const existingCapRows = await db.query.couponApplicableProducts.findMany({
|
||||
where: eq(couponApplicableProducts.skuId, toSkuId),
|
||||
columns: { couponId: true },
|
||||
})
|
||||
const existingCouponIds = new Set(existingCapRows.map((r) => r.couponId))
|
||||
|
||||
if (existingCouponIds.size > 0) {
|
||||
const dupResult = await db.delete(couponApplicableProducts)
|
||||
.where(
|
||||
and(
|
||||
eq(couponApplicableProducts.skuId, fromSkuId),
|
||||
inArray(couponApplicableProducts.couponId, Array.from(existingCouponIds))
|
||||
)
|
||||
)
|
||||
counts.couponDedupDeleted = dupResult.changes ?? 0
|
||||
}
|
||||
|
||||
// Now update remaining rows
|
||||
const capResult = await db.update(couponApplicableProducts)
|
||||
.set({ skuId: toSkuId })
|
||||
.where(eq(couponApplicableProducts.skuId, fromSkuId))
|
||||
counts.couponApplicable = capResult.changes ?? 0
|
||||
|
||||
// 5. JSON arrays — remap fromSkuId to toSkuId
|
||||
const jsonTables: Array<{ table: any; column: string; name: string }> = [
|
||||
{ table: deliverySlotInfo, column: 'skuIds', name: 'deliverySlotInfo' },
|
||||
{ table: homeBanners, column: 'skuIds', name: 'homeBanners' },
|
||||
{ table: coupons, column: 'skuIds', name: 'coupons' },
|
||||
{ table: reservedCoupons, column: 'skuIds', name: 'reservedCoupons' },
|
||||
{ table: vendorSnippets, column: 'skuIds', name: 'vendorSnippets' },
|
||||
]
|
||||
|
||||
for (const { table, column, name } of jsonTables) {
|
||||
const rows = await db.select({ id: table.id, ids: table[column] }).from(table)
|
||||
let updated = 0
|
||||
for (const row of rows) {
|
||||
const ids: number[] = (row.ids as number[]) || []
|
||||
if (!ids.includes(fromSkuId)) continue
|
||||
const newIds = ids.map((id) => (id === fromSkuId ? toSkuId : id))
|
||||
const deduped = [...new Set(newIds)]
|
||||
if (deduped.length !== ids.length || deduped.some((id, i) => id !== ids[i])) {
|
||||
await db.update(table).set({ [column]: deduped } as any).where(eq(table.id, row.id))
|
||||
updated++
|
||||
}
|
||||
}
|
||||
counts[name] = updated
|
||||
}
|
||||
|
||||
// popularItems in key_val_store
|
||||
const kvRow = await db.query.keyValStore.findFirst({
|
||||
where: eq(keyValStore.key, 'popularItems'),
|
||||
})
|
||||
if (kvRow && kvRow.value) {
|
||||
try {
|
||||
const arr: number[] = JSON.parse(kvRow.value)
|
||||
if (arr.includes(fromSkuId)) {
|
||||
const newArr = [...new Set(arr.map((id) => (id === fromSkuId ? toSkuId : id)))]
|
||||
await db.update(keyValStore)
|
||||
.set({ value: JSON.stringify(newArr) })
|
||||
.where(eq(keyValStore.key, 'popularItems'))
|
||||
counts.popularItems = 1
|
||||
}
|
||||
} catch { /* value not valid JSON, skip */ }
|
||||
}
|
||||
|
||||
// 6. Delete SKU features and the SKU itself
|
||||
const featuresResult = await db.delete(skuFeatures).where(eq(skuFeatures.skuId, fromSkuId))
|
||||
counts.skuFeatures = featuresResult.changes ?? 0
|
||||
|
||||
const skuResult = await db.delete(productSkus).where(eq(productSkus.id, fromSkuId))
|
||||
counts.productSkus = skuResult.changes ?? 0
|
||||
|
||||
// 7. Delete orphaned product
|
||||
const remainingSkus = await db.query.productSkus.findMany({
|
||||
where: eq(productSkus.productId, fromSku.productId),
|
||||
columns: { id: true },
|
||||
})
|
||||
|
||||
let orphanedProductId: number | undefined
|
||||
if (remainingSkus.length === 0) {
|
||||
await db.delete(productInfo).where(eq(productInfo.id, fromSku.productId))
|
||||
orphanedProductId = fromSku.productId
|
||||
counts.orphanedProduct = 1
|
||||
}
|
||||
|
||||
return {
|
||||
fromSkuId,
|
||||
toSkuId,
|
||||
orphanedProductId,
|
||||
counts,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { db } from '../db/db_index'
|
||||
import { vendorSnippets, deliverySlotInfo, productInfo, orders, orderItems, orderStatus } from '../db/schema'
|
||||
import { vendorSnippets, deliverySlotInfo, productInfo, productSkus, orders, orderItems, orderStatus } from '../db/schema'
|
||||
import { desc, eq, inArray } from 'drizzle-orm'
|
||||
import type { InferSelectModel } from 'drizzle-orm'
|
||||
import type {
|
||||
|
|
@ -19,7 +19,7 @@ const mapVendorSnippet = (snippet: VendorSnippetRow): AdminVendorSnippet => ({
|
|||
id: snippet.id,
|
||||
snippetCode: snippet.snippetCode,
|
||||
slotId: snippet.slotId ?? null,
|
||||
productIds: snippet.productIds || [],
|
||||
skuIds: snippet.skuIds || [],
|
||||
isPermanent: snippet.isPermanent,
|
||||
validTill: coerceDate(snippet.validTill),
|
||||
createdAt: coerceDate(snippet.createdAt) ?? new Date(0),
|
||||
|
|
@ -91,14 +91,14 @@ export async function getAllVendorSnippets(): Promise<AdminVendorSnippetWithSlot
|
|||
export async function createVendorSnippet(input: {
|
||||
snippetCode: string
|
||||
slotId?: number
|
||||
productIds: number[]
|
||||
skuIds: number[]
|
||||
isPermanent: boolean
|
||||
validTill?: Date
|
||||
}): Promise<AdminVendorSnippet> {
|
||||
const [result] = await db.insert(vendorSnippets).values({
|
||||
snippetCode: input.snippetCode,
|
||||
slotId: input.slotId,
|
||||
productIds: input.productIds,
|
||||
skuIds: input.skuIds,
|
||||
isPermanent: input.isPermanent,
|
||||
validTill: input.validTill,
|
||||
}).returning()
|
||||
|
|
@ -109,7 +109,7 @@ export async function createVendorSnippet(input: {
|
|||
export async function updateVendorSnippet(id: number, updates: {
|
||||
snippetCode?: string
|
||||
slotId?: number | null
|
||||
productIds?: number[]
|
||||
skuIds?: number[]
|
||||
isPermanent?: boolean
|
||||
validTill?: Date | null
|
||||
}): Promise<AdminVendorSnippet | null> {
|
||||
|
|
@ -129,14 +129,17 @@ export async function deleteVendorSnippet(id: number): Promise<AdminVendorSnippe
|
|||
return result ? mapVendorSnippet(result) : null
|
||||
}
|
||||
|
||||
export async function getProductsByIds(productIds: number[]): Promise<AdminVendorSnippetProduct[]> {
|
||||
const products = await db.query.productInfo.findMany({
|
||||
where: inArray(productInfo.id, productIds),
|
||||
columns: { id: true, name: true },
|
||||
export async function getProductsByIds(skuIds: number[]): Promise<AdminVendorSnippetProduct[]> {
|
||||
const skus = await db.query.productSkus.findMany({
|
||||
where: inArray(productSkus.id, skuIds),
|
||||
with: { product: { columns: { name: true } } },
|
||||
columns: { id: true },
|
||||
})
|
||||
|
||||
const prods = products.map(mapProductSummary)
|
||||
return prods
|
||||
return skus.map((sku) => ({
|
||||
id: sku.id,
|
||||
name: sku.product?.name ?? 'Unknown',
|
||||
})) as AdminVendorSnippetProduct[]
|
||||
}
|
||||
|
||||
export async function getVendorSlotById(slotId: number): Promise<AdminDeliverySlot | null> {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ export async function getAllProductsForCache(): Promise<ProductBasicData[]> {
|
|||
images: sku.images,
|
||||
isOutOfStock: sku.isOutOfStock,
|
||||
storeId: sku.product?.storeId ?? null,
|
||||
unitNotation: features.map((f) => f.featureValue).join(' '),
|
||||
unitNotation: features.map((f) => f.featureValue.replace(/\.0(?=\D|$)/g, '')).join(' '),
|
||||
incrementStep: sku.product?.incrementStep ?? 1,
|
||||
productQuantity: 1,
|
||||
isFlashAvailable: sku.isFlashAvailable,
|
||||
|
|
@ -301,7 +301,7 @@ export async function getAllSlotsWithProductsForCache(): Promise<SlotWithProduct
|
|||
shortDescription: sku.product?.shortDescription ?? null,
|
||||
price: String(sku.price ?? '0'),
|
||||
marketPrice: sku.marketPrice ? String(sku.marketPrice) : null,
|
||||
unitNotation: features.map((f: any) => f.featureValue).join(' '),
|
||||
unitNotation: features.map((f: any) => f.featureValue.replace(/\.0(?=\D|$)/g, '')).join(' '),
|
||||
store: sku.product?.store ? {
|
||||
id: sku.product.store.id,
|
||||
name: sku.product.store.name,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export async function getProductDetailById(skuId: number): Promise<UserProductDe
|
|||
longDescription: product?.longDescription ?? null,
|
||||
price: String(sku.price ?? '0'),
|
||||
marketPrice: sku.marketPrice ? String(sku.marketPrice) : null,
|
||||
unitNotation: features.map((f) => f.featureValue).join(' '),
|
||||
unitNotation: features.map((f) => f.featureValue.replace(/\.0(?=\D|$)/g, '')).join(' '),
|
||||
images: getStringArray(sku.images),
|
||||
isOutOfStock: sku.isOutOfStock,
|
||||
store: storeData ? {
|
||||
|
|
|
|||
|
|
@ -124,8 +124,8 @@ export async function getStoreDetail(storeId: number): Promise<UserStoreDetailDa
|
|||
price: String(sku.price ?? '0'),
|
||||
marketPrice: sku.marketPrice ? String(sku.marketPrice) : null,
|
||||
incrementStep: sku.product?.incrementStep ?? 1,
|
||||
unit: features.map((f) => f.featureValue).join(' '),
|
||||
unitNotation: features.map((f) => f.featureValue).join(' '),
|
||||
unit: features.map((f) => f.featureValue.replace(/\.0(?=\D|$)/g, '')).join(' '),
|
||||
unitNotation: features.map((f) => f.featureValue.replace(/\.0(?=\D|$)/g, '')).join(' '),
|
||||
images: getStringArray(sku.images),
|
||||
isOutOfStock: sku.isOutOfStock,
|
||||
productQuantity: 1,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export interface Banner {
|
|||
name: string;
|
||||
imageUrl: string;
|
||||
description: string | null;
|
||||
productIds: number[] | null;
|
||||
skuIds: number[] | null;
|
||||
redirectUrl: string | null;
|
||||
serialNum: number | null;
|
||||
isActive: boolean;
|
||||
|
|
|
|||
124
scripts/s3-cleaner.js
Normal file
124
scripts/s3-cleaner.js
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
#!/usr/bin/env bun
|
||||
// s3-cleaner.js — Delete old versioned cache folders from S3/R2
|
||||
// Usage: bun s3-cleaner.js <version>
|
||||
// Example: bun s3-cleaner.js 235
|
||||
// Deletes all objects under api-cache/v-0/ through api-cache/v-234/
|
||||
|
||||
// ============================================================
|
||||
// CREDENTIALS — replace with your actual values
|
||||
// ============================================================
|
||||
const S3_ACCESS_KEY_ID = 'YOUR_ACCESS_KEY_ID'
|
||||
const S3_SECRET_ACCESS_KEY = 'YOUR_SECRET_ACCESS_KEY'
|
||||
const S3_REGION = 'auto' // 'us-east-1' for AWS, 'auto' for R2
|
||||
const S3_ENDPOINT = 'https://your-account.r2.cloudflarestorage.com' // S3 or R2 endpoint
|
||||
const S3_BUCKET = 'your-bucket-name'
|
||||
const API_CACHE_KEY = 'api-cache' // matches API_CACHE_KEY env var in backend
|
||||
|
||||
// ============================================================
|
||||
|
||||
const threshold = parseInt(process.argv[2])
|
||||
if (!threshold || isNaN(threshold) || threshold <= 0) {
|
||||
console.error('Usage: bun s3-cleaner.js <version>')
|
||||
console.error('Example: bun s3-cleaner.js 235')
|
||||
console.error(' Deletes all v-{n} folders where n < 235')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const cachePrefix = API_CACHE_KEY.endsWith('/') ? API_CACHE_KEY : `${API_CACHE_KEY}/`
|
||||
const versionPattern = new RegExp(`^${cachePrefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}v-(\\d+)/`)
|
||||
|
||||
console.log(`🧹 S3 Cache Cleaner — keeping v-${threshold}+, deleting v-0 through v-${threshold - 1}`)
|
||||
console.log(` Bucket: ${S3_BUCKET}`)
|
||||
console.log(` Prefix: ${cachePrefix}`)
|
||||
|
||||
const s3 = new Bun.S3Client({
|
||||
accessKeyId: S3_ACCESS_KEY_ID,
|
||||
secretAccessKey: S3_SECRET_ACCESS_KEY,
|
||||
region: S3_REGION,
|
||||
endpoint: S3_ENDPOINT,
|
||||
bucket: S3_BUCKET,
|
||||
})
|
||||
|
||||
async function listAllObjects(prefix) {
|
||||
const allKeys = []
|
||||
let continuationToken
|
||||
|
||||
do {
|
||||
const options = { prefix, maxKeys: 1000 }
|
||||
if (continuationToken) options.continuationToken = continuationToken
|
||||
|
||||
const result = await s3.list(options)
|
||||
for (const obj of result.contents) {
|
||||
allKeys.push(obj.key)
|
||||
}
|
||||
continuationToken = result.nextContinuationToken
|
||||
|
||||
process.stdout.write(`\r Listed ${allKeys.length} objects...`)
|
||||
} while (continuationToken)
|
||||
|
||||
console.log('')
|
||||
return allKeys
|
||||
}
|
||||
|
||||
async function deleteObjects(keys) {
|
||||
let deleted = 0
|
||||
const batchSize = 1000
|
||||
|
||||
for (let i = 0; i < keys.length; i += batchSize) {
|
||||
const batch = keys.slice(i, i + batchSize)
|
||||
const objects = batch.map((key) => ({ key }))
|
||||
|
||||
const result = await s3.deleteObjects({ objects })
|
||||
deleted += result.deleted?.length ?? 0
|
||||
|
||||
process.stdout.write(`\r Deleted ${deleted}/${keys.length}...`)
|
||||
}
|
||||
|
||||
console.log('')
|
||||
return deleted
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// MAIN
|
||||
// ============================================================
|
||||
try {
|
||||
console.log('\n📋 Listing objects...')
|
||||
const allObjects = await listAllObjects(cachePrefix)
|
||||
|
||||
const toDelete = []
|
||||
const versionsSeen = new Set()
|
||||
for (const key of allObjects) {
|
||||
const match = key.match(versionPattern)
|
||||
if (match) {
|
||||
const ver = parseInt(match[1])
|
||||
if (ver < threshold) {
|
||||
toDelete.push(key)
|
||||
}
|
||||
versionsSeen.add(ver)
|
||||
}
|
||||
}
|
||||
|
||||
const sortedVersions = [...versionsSeen].sort((a, b) => a - b)
|
||||
|
||||
if (sortedVersions.length === 0) {
|
||||
console.log('\n✅ No cache objects found.')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
console.log(`\n📊 Found versions: v-${sortedVersions[0]} through v-${sortedVersions[sortedVersions.length - 1]}`)
|
||||
const oldVersionCount = sortedVersions.filter((v) => v < threshold).length
|
||||
console.log(` To delete: ${toDelete.length} objects across ${oldVersionCount} version(s)`)
|
||||
console.log(` To keep: v-${threshold}+`)
|
||||
|
||||
if (toDelete.length === 0) {
|
||||
console.log('\n✅ Nothing to delete.')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
console.log('\n❗ Proceeding with deletion...')
|
||||
const count = await deleteObjects(toDelete)
|
||||
console.log(`\n✅ Done — deleted ${count} objects.`)
|
||||
} catch (error) {
|
||||
console.error(`\n❌ Error: ${error.message}`)
|
||||
process.exit(1)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue