diff --git a/apps/admin-ui/app/(drawer)/manage-orders/order-details/[id].tsx b/apps/admin-ui/app/(drawer)/manage-orders/order-details/[id].tsx
index 3851941..43c45c4 100644
--- a/apps/admin-ui/app/(drawer)/manage-orders/order-details/[id].tsx
+++ b/apps/admin-ui/app/(drawer)/manage-orders/order-details/[id].tsx
@@ -463,8 +463,8 @@ export default function OrderDetails() {
{item.name}
- {Number(item.quantity) * item.productSize} {item.unit} × ₹{item.price}
-
+ {Number(item.quantity)} x {item.productSize}{item.unit} × ₹{item.price}
+
void }
- {item.quantity * item.productSize } {item.unit}
+ {item.quantity} x {item.productSize}{item.unit}
{item.name.length > 30 ? `${item.name.substring(0, 30)}...` : item.name}
diff --git a/apps/admin-ui/app/(drawer)/prices-overview/index.tsx b/apps/admin-ui/app/(drawer)/prices-overview/index.tsx
index e9ed777..eb39369 100644
--- a/apps/admin-ui/app/(drawer)/prices-overview/index.tsx
+++ b/apps/admin-ui/app/(drawer)/prices-overview/index.tsx
@@ -402,15 +402,6 @@ export default function PricesOverview() {
/>
-
- Size
-
-
-
`v-${version}/${path}`
+const buildAvailabilityPath = (version: number) => `av-${version}/${CACHE_FILENAMES.availability}`
+
+const buildSlotsPath = (version: number) => `slots/v-${version}/${CACHE_FILENAMES.slots}`
+
function constructCacheUrl(path: string, version: number): string {
return `${getAssetsDomain()}${getApiCacheKey()}/${buildCachePath(path, version)}`
}
+function constructAvailabilityUrl(version: number): string {
+ return `${getAssetsDomain()}${buildAvailabilityPath(version)}`
+}
+
+function constructSlotsUrl(version: number): string {
+ return `${getAssetsDomain()}${buildSlotsPath(version)}`
+}
+
export interface CreateAllCacheFilesResult {
cacheVersion: number
products: string
essentialConsts: string
stores: string
- slots: string
+ slotsVersion: number
+ availabilityVersion: number
banners: string
individualStores: string[]
}
@@ -37,14 +50,16 @@ export async function createAllCacheFiles(): Promise
productsKey,
essentialConstsKey,
storesKey,
- slotsKey,
+ slotsVersion,
+ availabilityVersion,
bannersKey,
individualStoreKeys,
] = await Promise.all([
createProductsFileInternal(cacheVersion),
createEssentialConstsFileInternal(cacheVersion),
createStoresFileInternal(cacheVersion),
- createSlotsFileInternal(cacheVersion),
+ createSlotsCacheFile(),
+ createAvailabilityCacheFile(),
createBannersFileInternal(cacheVersion),
createAllStoresFilesInternal(cacheVersion),
])
@@ -56,7 +71,8 @@ export async function createAllCacheFiles(): Promise
constructCacheUrl(CACHE_FILENAMES.products, cacheVersion),
constructCacheUrl(CACHE_FILENAMES.essentialConsts, cacheVersion),
constructCacheUrl(CACHE_FILENAMES.stores, cacheVersion),
- constructCacheUrl(CACHE_FILENAMES.slots, cacheVersion),
+ constructSlotsUrl(slotsVersion),
+ constructAvailabilityUrl(availabilityVersion),
constructCacheUrl(CACHE_FILENAMES.banners, cacheVersion),
...stores.map((store) => constructCacheUrl(`stores/${store.id}.json`, cacheVersion)),
]
@@ -76,7 +92,8 @@ export async function createAllCacheFiles(): Promise
products: productsKey,
essentialConsts: essentialConstsKey,
stores: storesKey,
- slots: slotsKey,
+ slotsVersion,
+ availabilityVersion,
banners: bannersKey,
individualStores: individualStoreKeys,
}
@@ -98,6 +115,23 @@ async function createProductsFileInternal(version: number): Promise {
}
+export async function createAvailabilityCacheFile(): Promise {
+ const version = await incrementAvailabilityVersionNum()
+ const availabilityData = await scaffoldAvailability()
+ const jsonContent = JSON.stringify(availabilityData, null, 2)
+ const buffer = Buffer.from(jsonContent, 'utf-8')
+ const filePath = buildAvailabilityPath(version)
+
+ console.log(filePath)
+ await imageUploadS3(
+ buffer,
+ 'application/json',
+ filePath
+ )
+
+ return version
+}
+
async function createEssentialConstsFileInternal(version: number): Promise {
const essentialConstsData = await scaffoldEssentialConsts()
const jsonContent = JSON.stringify(essentialConstsData, null, 2)
@@ -120,15 +154,21 @@ async function createStoresFileInternal(version: number): Promise {
)
}
-async function createSlotsFileInternal(version: number): Promise {
+export async function createSlotsCacheFile(): Promise {
+ const version = await incrementSlotsVersionNum()
const slotsData = await scaffoldSlotsWithProducts()
const jsonContent = JSON.stringify(slotsData, null, 2)
const buffer = Buffer.from(jsonContent, 'utf-8')
- return await imageUploadS3(
+ const filePath = buildSlotsPath(version)
+
+ console.log(filePath)
+ await imageUploadS3(
buffer,
'application/json',
- `${getApiCacheKey()}/${buildCachePath(CACHE_FILENAMES.slots, version)}`
+ filePath
)
+
+ return version
}
async function createBannersFileInternal(version: number): Promise {
diff --git a/apps/backend/src/sqliteImporter.ts b/apps/backend/src/sqliteImporter.ts
index 111fac3..322fc09 100644
--- a/apps/backend/src/sqliteImporter.ts
+++ b/apps/backend/src/sqliteImporter.ts
@@ -240,6 +240,7 @@ export {
// Store Helpers
getAllBannersForCache,
getAllProductsForCache,
+ getAvailabilityForCache,
getAllStoresForCache,
getAllDeliverySlotsForCache,
getAllSpecialDealsForCache,
diff --git a/apps/backend/src/stores/product-store.ts b/apps/backend/src/stores/product-store.ts
index 73f7cd4..db04482 100644
--- a/apps/backend/src/stores/product-store.ts
+++ b/apps/backend/src/stores/product-store.ts
@@ -44,6 +44,7 @@ interface Product {
productName: string
images: string[] | null
price: string
+ isOffer: boolean
}>
}
@@ -250,6 +251,7 @@ export async function getAllProducts(): Promise {
productName: ci.productName,
images: ci.images ? scaffoldAssetUrl((ci.images as string[]) || []) : null,
price: ci.price,
+ isOffer: ci.isOffer,
}))
products.push({
diff --git a/apps/backend/src/trpc/apis/admin-apis/apis/product.ts b/apps/backend/src/trpc/apis/admin-apis/apis/product.ts
index 8d99e3e..eac06d3 100644
--- a/apps/backend/src/trpc/apis/admin-apis/apis/product.ts
+++ b/apps/backend/src/trpc/apis/admin-apis/apis/product.ts
@@ -3,6 +3,7 @@ import { z } from 'zod'
import { ApiError } from '@/src/lib/api-error'
import { generateSignedUrlsFromS3Urls, scaffoldAssetUrl, claimUploadUrl, extractKeyFromPresignedUrl, deleteImageUtil } from '@/src/lib/s3-client'
import { scheduleStoreInitialization } from '@/src/stores/store-initializer'
+import { createAvailabilityCacheFile } from '@/src/lib/cloud_cache'
import {
getAllProducts as getAllProductsInDb,
getProductById as getProductByIdInDb,
@@ -196,6 +197,7 @@ export const productRouter = router({
images: z.array(z.string()).optional().default([]),
isFlashAvailable: z.boolean().optional().default(false),
flashPrice: z.number().optional().nullable(),
+ isOutOfStock: z.boolean().optional().default(false),
isOffer: z.boolean().optional().default(false),
isComboOnly: z.boolean().optional().default(false),
isSuspended: z.boolean().optional().default(false),
@@ -225,6 +227,7 @@ export const productRouter = router({
images: sku.images.map((url) => extractKeyFromPresignedUrl(url)),
isFlashAvailable: sku.isFlashAvailable,
flashPrice: sku.flashPrice ?? null,
+ isOutOfStock: sku.isOutOfStock,
isOffer: sku.isOffer,
isComboOnly: sku.isComboOnly,
isSuspended: sku.isSuspended,
@@ -284,6 +287,7 @@ export const productRouter = router({
images: z.array(z.string()).optional().default([]),
isFlashAvailable: z.boolean().optional().default(false),
flashPrice: z.number().optional().nullable(),
+ isOutOfStock: z.boolean().optional().default(false),
isOffer: z.boolean().optional().default(false),
isComboOnly: z.boolean().optional().default(false),
isSuspended: z.boolean().optional().default(false),
@@ -315,6 +319,7 @@ export const productRouter = router({
images: sku.images.map((url) => extractKeyFromPresignedUrl(url)),
isFlashAvailable: sku.isFlashAvailable,
flashPrice: sku.flashPrice ?? null,
+ isOutOfStock: sku.isOutOfStock,
isOffer: sku.isOffer,
isComboOnly: sku.isComboOnly,
isSuspended: sku.isSuspended,
@@ -842,11 +847,13 @@ export const productRouter = router({
};
*/
- if (result.invalidIds.length > 0) {
- throw new ApiError(`Invalid product IDs: ${result.invalidIds.join(', ')}`, 400)
- }
+ if (result.invalidIds.length > 0) {
+ throw new ApiError(`Invalid product IDs: ${result.invalidIds.join(', ')}`, 400)
+ }
- await scheduleStoreInitialization()
+ await createAvailabilityCacheFile().catch((err) => {
+ console.error('Failed to regenerate availability cache after price update:', err)
+ })
return {
message: `Updated prices for ${result.updatedCount} product(s)`,
diff --git a/apps/backend/src/trpc/apis/admin-apis/apis/slots.ts b/apps/backend/src/trpc/apis/admin-apis/apis/slots.ts
index c2e622c..7c7932d 100644
--- a/apps/backend/src/trpc/apis/admin-apis/apis/slots.ts
+++ b/apps/backend/src/trpc/apis/admin-apis/apis/slots.ts
@@ -6,6 +6,7 @@ import { getAppUrl } from "@/src/lib/env-exporter"
// import redisClient from "@/src/lib/redis-client"
// import { getSlotSequenceKey } from "@/src/lib/redisKeyGetters"
import { scheduleStoreInitialization } from '@/src/stores/store-initializer'
+import { createSlotsCacheFile } from '@/src/lib/cloud_cache'
import {
getActiveSlotsWithProducts as getActiveSlotsWithProductsInDb,
getActiveSlots as getActiveSlotsInDb,
@@ -266,7 +267,9 @@ export const slotsRouter = router({
};
*/
- await scheduleStoreInitialization()
+ await createSlotsCacheFile().catch((err) => {
+ console.error('Failed to regenerate slots cache after product update:', err)
+ })
return {
message: result.message,
@@ -360,8 +363,10 @@ export const slotsRouter = router({
});
*/
- // Reinitialize stores to reflect changes (outside transaction)
- await scheduleStoreInitialization()
+ // Regenerate slots cache file (availability/products stay as-is)
+ await createSlotsCacheFile().catch((error) => {
+ console.error('Failed to regenerate slots cache after slot create:', error)
+ })
// Fire and forget: cleanup stale product slot associations
staleSlotsCleanup().catch((error) => {
@@ -548,8 +553,10 @@ export const slotsRouter = router({
throw new ApiError('Slot not found', 404)
}
- // Reinitialize stores to reflect changes (outside transaction)
- await scheduleStoreInitialization()
+ // Regenerate slots cache file (availability/products stay as-is)
+ await createSlotsCacheFile().catch((error) => {
+ console.error('Failed to regenerate slots cache after slot update:', error)
+ })
return result
}
@@ -587,8 +594,10 @@ export const slotsRouter = router({
throw new ApiError('Slot not found', 404)
}
- // Reinitialize stores to reflect changes
- await scheduleStoreInitialization()
+ // Regenerate slots cache file (availability/products stay as-is)
+ await createSlotsCacheFile().catch((error) => {
+ console.error('Failed to regenerate slots cache after slot delete:', error)
+ })
return {
message: 'Slot deleted successfully',
@@ -736,7 +745,9 @@ export const slotsRouter = router({
throw new ApiError('Slot not found', 404)
}
- await scheduleStoreInitialization()
+ await createSlotsCacheFile().catch((error) => {
+ console.error('Failed to regenerate slots cache after capacity update:', error)
+ })
return result
}),
diff --git a/apps/backend/src/trpc/apis/common-apis/common-trpc-index.ts b/apps/backend/src/trpc/apis/common-apis/common-trpc-index.ts
index 69d9449..912d23c 100644
--- a/apps/backend/src/trpc/apis/common-apis/common-trpc-index.ts
+++ b/apps/backend/src/trpc/apis/common-apis/common-trpc-index.ts
@@ -4,6 +4,8 @@ import {
getStoresSummary,
healthCheck,
getCacheVersion,
+ getAvailabilityVersionNum,
+ getSlotsVersionNum,
} from '@/src/dbService'
import type { StoresSummaryResponse } from '@packages/shared'
import { polygon as turfPolygon, point as turfPoint } from '@turf/helpers';
@@ -21,6 +23,8 @@ const polygon = turfPolygon(mbnrGeoJson.features[0].geometry.coordinates);
export async function scaffoldEssentialConsts() {
const consts = await getAllConstValues();
const cacheVersion = await getCacheVersion()
+ const availabilityVersionNum = await getAvailabilityVersionNum()
+ const slotsVersionNum = await getSlotsVersionNum()
return {
freeDeliveryThreshold: consts[CONST_KEYS.freeDeliveryThreshold] ?? 200,
@@ -40,6 +44,8 @@ export async function scaffoldEssentialConsts() {
assetsDomain: getAssetsDomain(),
apiCacheKey: getApiCacheKey(),
cacheVersion,
+ availabilityVersionNum,
+ slotsVersionNum,
};
}
diff --git a/apps/backend/src/trpc/apis/common-apis/common.ts b/apps/backend/src/trpc/apis/common-apis/common.ts
index 5f945b9..2f2cece 100644
--- a/apps/backend/src/trpc/apis/common-apis/common.ts
+++ b/apps/backend/src/trpc/apis/common-apis/common.ts
@@ -6,6 +6,7 @@ import {
getAllSkusSummary as getAllSkusSummaryInDb,
getAllTagsForCache,
getAllTagProductMappings,
+ getAvailabilityForCache,
} from '@/src/dbService'
import { generateSignedUrlsFromS3Urls, generateSignedUrlFromS3Url, scaffoldAssetUrl } from '@/src/lib/s3-client'
import { getAllProducts as getAllProductsFromCache } from '@/src/stores/product-store'
@@ -45,18 +46,14 @@ export async function scaffoldProducts() {
id: product.id,
name: product.name,
shortDescription: product.shortDescription,
- price: parseFloat(product.price),
- marketPrice: product.marketPrice ? parseFloat(product.marketPrice) : null,
unit: product.unitNotation,
unitNotation: product.unitNotation,
incrementStep: product.incrementStep,
productQuantity: product.productQuantity,
storeId: product.store?.id || null,
isOutOfStock: product.isOutOfStock,
- isFlashAvailable: product.isFlashAvailable,
nextDeliveryDate: nextDeliveryDate ? nextDeliveryDate.toISOString() : null,
images: product.images,
- flashPrice: product.flashPrice,
productType: product.productType || 'item'
};
})
@@ -93,6 +90,15 @@ export async function scaffoldProducts() {
};
}
+export async function scaffoldAvailability() {
+ const availability = await getAvailabilityForCache()
+
+ return {
+ availability,
+ count: availability.length,
+ };
+}
+
export const commonRouter = router({
getDashboardTags: publicProcedure
.query(async () => {
diff --git a/apps/backend/src/trpc/apis/user-apis/apis/slots.ts b/apps/backend/src/trpc/apis/user-apis/apis/slots.ts
index a938534..169f3e5 100644
--- a/apps/backend/src/trpc/apis/user-apis/apis/slots.ts
+++ b/apps/backend/src/trpc/apis/user-apis/apis/slots.ts
@@ -17,28 +17,16 @@ export async function scaffoldSlotsWithProducts(): Promise ({
- id: product.id,
- name: product.name,
- isOutOfStock: product.isOutOfStock,
- isFlashAvailable: product.isFlashAvailable,
- }));
- */
-
return {
- slots: validSlots,
+ slots: validSlots.map((slot) => ({
+ id: slot.id,
+ deliveryTime: slot.deliveryTime,
+ freezeTime: slot.freezeTime,
+ products: (slot.products || []).map((product) => ({
+ id: product.id,
+ images: product.images,
+ })),
+ })),
productAvailability,
count: validSlots.length,
};
diff --git a/apps/backend/src/trpc/router.ts b/apps/backend/src/trpc/router.ts
index f7ccecc..24cd8cd 100644
--- a/apps/backend/src/trpc/router.ts
+++ b/apps/backend/src/trpc/router.ts
@@ -3,7 +3,7 @@ import { z } from 'zod';
import { adminRouter } from '@/src/trpc/apis/admin-apis/apis/admin-trpc-index'
import { userRouter } from '@/src/trpc/apis/user-apis/apis/user-trpc-index'
import { commonApiRouter } from '@/src/trpc/apis/common-apis/common-trpc-index'
-import { scaffoldProducts } from './apis/common-apis/common';
+import { scaffoldProducts, scaffoldAvailability } from './apis/common-apis/common';
import { scaffoldStores, scaffoldStoreWithProducts } from './apis/user-apis/apis/stores';
import { scaffoldSlotsWithProducts } from './apis/user-apis/apis/slots';
import { scaffoldEssentialConsts } from './apis/common-apis/common-trpc-index';
@@ -26,6 +26,7 @@ export const appRouter = router({
export type AppRouter = typeof appRouter;
export type AllProductsApiType = Awaited>;
+export type AvailabilityApiType = Awaited>;
export type StoresApiType = Awaited>;
export type SlotsApiType = Awaited>;
export type EssentialConstsApiType = Awaited>;
diff --git a/apps/backend/wrangler-commands.md b/apps/backend/wrangler-commands.md
index 5844c56..14927e8 100644
--- a/apps/backend/wrangler-commands.md
+++ b/apps/backend/wrangler-commands.md
@@ -49,10 +49,16 @@ and paste it ABOVE the child table's block. Then verify it loads cleanly:
sqlite3 :memory: "PRAGMA foreign_keys=ON; BEGIN; .read dumps/.sql; COMMIT;"
-This should exit 0 with no error. (Example already applied to `latest_1.sql`:
-`product_info` was moved above `product_skus`.)
+This should exit 0 with no error. (Example already applied to `latest_1.sql` and
+`local_8_aug.sql`: `product_info` was moved above `product_skus`.)
## When to re-check
After ANY new `wrangler d1 export`, especially once a migration that re-creates
tables has been applied. This is a general trap, not specific to one dump.
+
+> The SKU-split migration re-creates these tables in this historical order:
+> `product_skus`, `sku_features`, `product_market_stats`, `product_info`,
+> `cart_items`, `order_items`, `product_combos`. Exports put `product_info`
+> AFTER its child `product_skus` — every fresh export needs `product_info`
+> moved above `product_skus` (or the whole chain checked) before local import.
diff --git a/apps/backend/wrangler.dev.toml b/apps/backend/wrangler.dev.toml
index bd1f08b..1048178 100644
--- a/apps/backend/wrangler.dev.toml
+++ b/apps/backend/wrangler.dev.toml
@@ -9,7 +9,7 @@ routes = [
[[d1_databases]]
binding = "DB"
database_name = "freshyo-backend-dev"
-database_id = "0814d709-5278-4311-8978-c36c0f05875d"
+database_id = "a976d1fb-c6a7-4948-b303-81c6433ed265"
#database_name = "freshyo-dev"
#database_id = "3cad440f-dc14-4baa-ab05-04eb08e206de"
migrations_dir="../../packages/db_helper_sqlite/drizzle"
diff --git a/apps/user-ui/components/ProductDetail.tsx b/apps/user-ui/components/ProductDetail.tsx
index 1fde5da..9f4d6a1 100644
--- a/apps/user-ui/components/ProductDetail.tsx
+++ b/apps/user-ui/components/ProductDetail.tsx
@@ -2,6 +2,7 @@ import React, { useState, useMemo } from 'react';
import { View, ScrollView, Alert, Dimensions, FlatList, RefreshControl } from 'react-native';
import { useRouter } from 'expo-router';
import { ImageCarousel, tw, BottomDialog, useManualRefresh, useMarkDataFetchers, LoadingDialog, ImageUploader, MyText, MyTextInput, MyTouchableOpacity, Quantifier } from 'common-ui';
+import { Image } from 'expo-image';
import { LinearGradient } from 'expo-linear-gradient';
import usePickImage from 'common-ui/src/components/use-pick-image';
import { theme } from 'common-ui/src/theme';
@@ -377,6 +378,54 @@ const ProductDetail: React.FC = ({ productId, isFlashDeliver
+ {/* Combo Items */}
+ {productDetail.productType === 'combo' && productDetail.comboItems && productDetail.comboItems.length > 0 && (
+
+
+
+
+
+
+ Included Items
+
+
+ {productDetail.comboItems.map((comboItem, index) => (
+
+
+ {comboItem.images?.[0] ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {comboItem.productName}
+
+ {comboItem.isOffer && (
+
+ OFFER
+
+ )}
+
+
+ {comboItem.unitNotation || comboItem.skuName || ''}
+
+
+
+ ))}
+
+
+ )}
+
{/* Delivery Slots */}
diff --git a/apps/user-ui/hooks/cart-query-hooks.tsx b/apps/user-ui/hooks/cart-query-hooks.tsx
index 09b7003..ca8ac5e 100644
--- a/apps/user-ui/hooks/cart-query-hooks.tsx
+++ b/apps/user-ui/hooks/cart-query-hooks.tsx
@@ -1,4 +1,4 @@
-import { useAllProducts } from '@/src/hooks/prominent-api-hooks';
+import { useCentralProductStore } from '@/src/store/centralProductStore';
import { useCentralSlotStore } from '@/src/store/centralSlotStore';
import { Alert } from 'react-native';
import { useQuery, useMutation, useQueryClient, UseQueryResult, UseMutationResult } from '@tanstack/react-query';
@@ -184,7 +184,7 @@ const clearLocalCart = async (cartType: CartType = "regular"): Promise =>
};
export function useGetCart(options: UseGetCartOptions = {}, cartType: CartType = "regular"): UseGetCartReturn {
- const { data: products } = useAllProducts();
+ const productsById = useCentralProductStore((state) => state.productsById);
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
const query: UseQueryResult = useQuery({
@@ -193,7 +193,7 @@ export function useGetCart(options: UseGetCartOptions = {}, cartType: CartType =
const cartItems = await getLocalCart(cartType);
const productMap: Record> = Object.fromEntries(
- products?.products?.map((p) => [
+ Object.values(productsById).map((p) => [
p.id,
{
id: p.id,
@@ -206,7 +206,7 @@ export function useGetCart(options: UseGetCartOptions = {}, cartType: CartType =
productQuantity: p.productQuantity,
unitNotation: p.unitNotation,
},
- ]) ?? []
+ ])
);
const items: CartItem[] = cartItems
@@ -236,7 +236,7 @@ export function useGetCart(options: UseGetCartOptions = {}, cartType: CartType =
};
},
refetchOnWindowFocus: options?.refetchOnWindowFocus ?? true,
- enabled: (options?.enabled ?? true) && !!products,
+ enabled: (options?.enabled ?? true) && Object.keys(productsById).length > 0,
});
return {
diff --git a/apps/user-ui/src/hooks/prominent-api-hooks.ts b/apps/user-ui/src/hooks/prominent-api-hooks.ts
index b6c23f4..aada9f8 100644
--- a/apps/user-ui/src/hooks/prominent-api-hooks.ts
+++ b/apps/user-ui/src/hooks/prominent-api-hooks.ts
@@ -1,7 +1,8 @@
+import React from 'react'
import { useQuery } from '@tanstack/react-query'
import axios from 'axios'
import { trpc } from '@/src/trpc-client'
-import { AllProductsApiType, StoresApiType, SlotsApiType, EssentialConstsApiType, BannersApiType, StoreWithProductsApiType } from "@backend/trpc/router";
+import { AllProductsApiType, StoresApiType, SlotsApiType, EssentialConstsApiType, BannersApiType, StoreWithProductsApiType, AvailabilityApiType } from "@backend/trpc/router";
import { CACHE_FILENAMES } from "@packages/shared";
// Local useGetEssentialConsts hook
@@ -18,6 +19,19 @@ type SlotsResponse = SlotsApiType;
type EssentialConstsResponse = EssentialConstsApiType;
type BannersResponse = BannersApiType;
type StoreWithProductsResponse = StoreWithProductsApiType;
+type AvailabilityResponse = AvailabilityApiType;
+
+type BaseProduct = AllProductsApiType['products'][number]
+type AvailabilityEntry = AvailabilityApiType['availability'][number]
+
+export type MergedProduct = BaseProduct & {
+ price: number
+ marketPrice: number | null
+ flashPrice: string | null
+ isFlashAvailable: boolean
+ isOutOfStock: boolean
+ isSuspended: boolean
+}
function useCacheUrl(filename: string): string | null {
const { data: essentialConsts } = useGetEssentialConsts()
@@ -33,11 +47,37 @@ function useCacheUrl(filename: string): string | null {
return `${assetsDomain}${apiCacheKey}/v-${cacheVersion}/${filename}`
}
+function useAvailabilityCacheUrl(): string | null {
+ const { data: essentialConsts } = useGetEssentialConsts()
+
+ const assetsDomain = essentialConsts?.assetsDomain
+ const availabilityVersionNum = essentialConsts?.availabilityVersionNum
+
+ if (!assetsDomain || availabilityVersionNum === undefined || availabilityVersionNum === null) {
+ return null
+ }
+
+ return `${assetsDomain}av-${availabilityVersionNum}/${CACHE_FILENAMES.availability}`
+}
+
+function useSlotsCacheUrl(): string | null {
+ const { data: essentialConsts } = useGetEssentialConsts()
+
+ const assetsDomain = essentialConsts?.assetsDomain
+ const slotsVersionNum = essentialConsts?.slotsVersionNum
+
+ if (!assetsDomain || slotsVersionNum === undefined || slotsVersionNum === null) {
+ return null
+ }
+
+ return `${assetsDomain}slots/v-${slotsVersionNum}/${CACHE_FILENAMES.slots}`
+}
+
export function useAllProducts() {
const cacheUrl = useCacheUrl(CACHE_FILENAMES.products)
+ const { data: availabilityData } = useAvailability()
-
- return useQuery({
+ const productsQuery = useQuery({
queryKey: ['all-products', cacheUrl],
queryFn: async () => {
if (!cacheUrl) {
@@ -49,6 +89,57 @@ export function useAllProducts() {
staleTime: 60000, // 1 minute
enabled: !!cacheUrl,
})
+
+ const mergedProducts = React.useMemo(() => {
+ const rawProducts = productsQuery.data?.products || []
+ const availabilityById: Record = {}
+ availabilityData?.availability?.forEach((entry: AvailabilityEntry) => {
+ availabilityById[entry.id] = entry
+ })
+
+ return rawProducts.map((product) => {
+ const availability = availabilityById[product.id]
+ return {
+ ...product,
+ price: availability ? Number(availability.price) : 0,
+ marketPrice: availability?.marketPrice != null ? Number(availability.marketPrice) : null,
+ flashPrice: availability?.flashPrice ?? null,
+ isFlashAvailable: availability?.isFlashAvailable ?? false,
+ isOutOfStock: availability?.isOutOfStock ?? false,
+ isSuspended: availability?.isSuspended ?? false,
+ }
+ })
+ }, [productsQuery.data, availabilityData])
+
+ const mergedData = React.useMemo(() => {
+ if (!productsQuery.data) return undefined
+ return {
+ ...productsQuery.data,
+ products: mergedProducts,
+ } as ProductsResponse & { products: MergedProduct[] }
+ }, [productsQuery.data, mergedProducts])
+
+ return {
+ ...productsQuery,
+ data: mergedData,
+ }
+}
+
+export function useAvailability() {
+ const cacheUrl = useAvailabilityCacheUrl()
+
+ return useQuery({
+ queryKey: ['availability', cacheUrl],
+ queryFn: async () => {
+ if (!cacheUrl) {
+ throw new Error('Cache URL not available')
+ }
+ const response = await axios.get(cacheUrl)
+ return response.data
+ },
+ staleTime: 60000, // 1 minute
+ enabled: !!cacheUrl,
+ })
}
export function useStores() {
@@ -69,7 +160,7 @@ export function useStores() {
}
export function useSlots() {
- const cacheUrl = useCacheUrl(CACHE_FILENAMES.slots)
+ const cacheUrl = useSlotsCacheUrl()
return useQuery({
queryKey: ['slots', cacheUrl],
diff --git a/apps/user-ui/src/store/centralProductStore.ts b/apps/user-ui/src/store/centralProductStore.ts
index a1b1b59..d228eda 100644
--- a/apps/user-ui/src/store/centralProductStore.ts
+++ b/apps/user-ui/src/store/centralProductStore.ts
@@ -1,9 +1,8 @@
import { create } from 'zustand'
import { useEffect } from 'react'
-import { useAllProducts } from '@/src/hooks/prominent-api-hooks'
-import { AllProductsApiType } from '@backend/trpc/router'
+import { useAllProducts, type MergedProduct } from '@/src/hooks/prominent-api-hooks'
-type Product = AllProductsApiType['products'][number]
+export type Product = MergedProduct
interface CentralProductState {
products: Product[]
diff --git a/apps/user-ui/src/store/centralSlotStore.ts b/apps/user-ui/src/store/centralSlotStore.ts
index 483c680..fa72172 100644
--- a/apps/user-ui/src/store/centralSlotStore.ts
+++ b/apps/user-ui/src/store/centralSlotStore.ts
@@ -1,22 +1,24 @@
import { create } from 'zustand';
-import { useSlots } from '@/src/hooks/prominent-api-hooks';
+import { useSlots, useAvailability } from '@/src/hooks/prominent-api-hooks';
import { useEffect } from 'react';
-import { SlotsApiType } from "@backend/trpc/router";
+import { SlotsApiType, AvailabilityApiType } from "@backend/trpc/router";
type Slot = SlotsApiType['slots'][number];
type ProductAvailability = SlotsApiType['productAvailability'][number];
+type AvailabilityEntry = AvailabilityApiType['availability'][number];
interface ProductSlotInfo {
slots: Slot[];
isOutOfStock: boolean;
isFlashAvailable: boolean;
+ isSuspended: boolean;
}
interface CentralSlotState {
slots: Slot[];
productSlotsMap: Record;
refetchSlots: (() => Promise) | null;
- setSlotsData: (slots: Slot[], productAvailability: ProductAvailability[]) => void;
+ setSlotsData: (slots: Slot[], productAvailability: ProductAvailability[], availability: AvailabilityEntry[]) => void;
clearSlotsData: () => void;
setRefetchSlots: (refetch: () => Promise) => void;
}
@@ -25,15 +27,20 @@ export const useCentralSlotStore = create((set) => ({
slots: [],
productSlotsMap: {},
refetchSlots: null,
- setSlotsData: (slots, productAvailability) => {
+ setSlotsData: (slots, productAvailability, availability) => {
const productSlotsMap: Record = {};
+ const availabilityById: Record = {};
+ availability.forEach((entry) => {
+ availabilityById[entry.id] = entry;
+ });
// First, create entries for ALL products from productAvailability
productAvailability.forEach((product) => {
productSlotsMap[product.id] = {
slots: [],
- isOutOfStock: product.isOutOfStock,
- isFlashAvailable: product.isFlashAvailable,
+ isOutOfStock: availabilityById[product.id]?.isOutOfStock ?? false,
+ isFlashAvailable: availabilityById[product.id]?.isFlashAvailable ?? false,
+ isSuspended: availabilityById[product.id]?.isSuspended ?? false,
};
});
@@ -54,14 +61,15 @@ export const useCentralSlotStore = create((set) => ({
export function useInitializeCentralSlotStore() {
const { data: slotsData, refetch } = useSlots();
+ const { data: availabilityData } = useAvailability();
const setSlotsData = useCentralSlotStore((state) => state.setSlotsData);
const setRefetchSlots = useCentralSlotStore((state) => state.setRefetchSlots);
useEffect(() => {
if (slotsData?.slots) {
- setSlotsData(slotsData.slots, slotsData.productAvailability || []);
+ setSlotsData(slotsData.slots, slotsData.productAvailability || [], availabilityData?.availability || []);
}
- }, [slotsData, setSlotsData]);
+ }, [slotsData, availabilityData, setSlotsData]);
useEffect(() => {
setRefetchSlots(async () => {
diff --git a/apps/web-ui/src/components/AddToCartDialog.tsx b/apps/web-ui/src/components/AddToCartDialog.tsx
index 6477f83..c1de297 100644
--- a/apps/web-ui/src/components/AddToCartDialog.tsx
+++ b/apps/web-ui/src/components/AddToCartDialog.tsx
@@ -4,6 +4,7 @@ import { BottomDialog, p, div, Quantifier } from 'web-components'
import { useSlots } from '../hooks/prominent-api-hooks'
import { useAddToCart, useUpdateCartItem, useRemoveFromCart, useGetCart } from '../hooks/cart-query-hooks'
import { useCartStore } from '../lib/stores/cart-store'
+import { useCentralSlotStore } from '../lib/stores/central-slot-store'
import { ShoppingCart, Truck, Zap, X } from 'lucide-react'
import dayjs from 'dayjs'
@@ -29,6 +30,7 @@ export default function AddToCartDialog() {
const { data: slotsData } = useSlots()
const { data: cartData } = useGetCart()
+ const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap)
const isFlashDeliveryEnabled = true
const addToCart = useAddToCart('regular')
@@ -76,7 +78,7 @@ export default function AddToCartDialog() {
const cartItem = cartData?.items?.find((item: any) => item.productId === product?.id)
const isUpdate = (cartItem?.quantity || 0) >= 1
- const productAvailability = slotsData?.productAvailability?.find((pa: any) => pa.id === product?.id)
+ const productAvailability = productSlotsMap[product?.id]
const showFlashOption = productAvailability?.isFlashAvailable === true && isFlashDeliveryEnabled
const handleAddToCart = () => {
diff --git a/apps/web-ui/src/hooks/prominent-api-hooks.ts b/apps/web-ui/src/hooks/prominent-api-hooks.ts
index 820ec54..fba16bb 100644
--- a/apps/web-ui/src/hooks/prominent-api-hooks.ts
+++ b/apps/web-ui/src/hooks/prominent-api-hooks.ts
@@ -1,8 +1,10 @@
+import { useMemo } from 'react'
import { useQuery } from '@tanstack/react-query'
import axios from 'axios'
import { trpc } from '../lib/trpc-client'
import type {
AllProductsApiType,
+ AvailabilityApiType,
StoresApiType,
SlotsApiType,
EssentialConstsApiType,
@@ -23,6 +25,17 @@ type StoresResponse = StoresApiType
type SlotsResponse = SlotsApiType
type BannersResponse = BannersApiType
type StoreWithProductsResponse = StoreWithProductsApiType
+type AvailabilityResponse = AvailabilityApiType
+
+type BaseProduct = AllProductsApiType['products'][number]
+type AvailabilityEntry = AvailabilityApiType['availability'][number]
+
+export type MergedProduct = BaseProduct & {
+ price: number
+ marketPrice: number | null
+ flashPrice: string | null
+ isFlashAvailable: boolean
+}
function useCacheUrl(filename: string): string | null {
const { data: essentialConsts } = useGetEssentialConsts()
@@ -43,10 +56,37 @@ function useCacheUrl(filename: string): string | null {
return `${assetsDomain}${apiCacheKey}/v-${cacheVersion}/${filename}`
}
+function useAvailabilityCacheUrl(): string | null {
+ const { data: essentialConsts } = useGetEssentialConsts()
+
+ const assetsDomain = essentialConsts?.assetsDomain
+ const availabilityVersionNum = essentialConsts?.availabilityVersionNum
+
+ if (!assetsDomain || availabilityVersionNum === undefined || availabilityVersionNum === null) {
+ return null
+ }
+
+ return `${assetsDomain}av-${availabilityVersionNum}/${CACHE_FILENAMES.availability}`
+}
+
+function useSlotsCacheUrl(): string | null {
+ const { data: essentialConsts } = useGetEssentialConsts()
+
+ const assetsDomain = essentialConsts?.assetsDomain
+ const slotsVersionNum = essentialConsts?.slotsVersionNum
+
+ if (!assetsDomain || slotsVersionNum === undefined || slotsVersionNum === null) {
+ return null
+ }
+
+ return `${assetsDomain}slots/v-${slotsVersionNum}/${CACHE_FILENAMES.slots}`
+}
+
export function useAllProducts() {
const cacheUrl = useCacheUrl(CACHE_FILENAMES.products)
+ const { data: availabilityData } = useAvailability()
- return useQuery({
+ const productsQuery = useQuery({
queryKey: ['all-products', cacheUrl],
queryFn: async () => {
if (!cacheUrl) {
@@ -58,6 +98,55 @@ export function useAllProducts() {
staleTime: 60000,
enabled: !!cacheUrl,
})
+
+ const mergedProducts = useMemo(() => {
+ const rawProducts = productsQuery.data?.products || []
+ const availabilityById: Record = {}
+ availabilityData?.availability?.forEach((entry: AvailabilityEntry) => {
+ availabilityById[entry.id] = entry
+ })
+
+ return rawProducts.map((product) => {
+ const availability = availabilityById[product.id]
+ return {
+ ...product,
+ price: availability ? Number(availability.price) : 0,
+ marketPrice: availability?.marketPrice != null ? Number(availability.marketPrice) : null,
+ flashPrice: availability?.flashPrice ?? null,
+ isFlashAvailable: availability?.isFlashAvailable ?? false,
+ }
+ })
+ }, [productsQuery.data, availabilityData])
+
+ const mergedData = useMemo(() => {
+ if (!productsQuery.data) return undefined
+ return {
+ ...productsQuery.data,
+ products: mergedProducts,
+ } as ProductsResponse & { products: MergedProduct[] }
+ }, [productsQuery.data, mergedProducts])
+
+ return {
+ ...productsQuery,
+ data: mergedData,
+ }
+}
+
+export function useAvailability() {
+ const cacheUrl = useAvailabilityCacheUrl()
+
+ return useQuery({
+ queryKey: ['availability', cacheUrl],
+ queryFn: async () => {
+ if (!cacheUrl) {
+ throw new Error('Cache URL not available')
+ }
+ const response = await axios.get(cacheUrl)
+ return response.data
+ },
+ staleTime: 60000,
+ enabled: !!cacheUrl,
+ })
}
export function useStores() {
@@ -78,7 +167,7 @@ export function useStores() {
}
export function useSlots() {
- const cacheUrl = useCacheUrl(CACHE_FILENAMES.slots)
+ const cacheUrl = useSlotsCacheUrl()
return useQuery({
queryKey: ['slots', cacheUrl],
diff --git a/packages/db_helper_sqlite/drizzle/0002_sku_split.sql b/packages/db_helper_sqlite/drizzle/0002_sku_split.sql
index 2d30e73..84ef397 100644
--- a/packages/db_helper_sqlite/drizzle/0002_sku_split.sql
+++ b/packages/db_helper_sqlite/drizzle/0002_sku_split.sql
@@ -9,13 +9,7 @@ CREATE TABLE `product_skus` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`product_id` integer NOT NULL,
`name` text,
- `price` text NOT NULL,
- `market_price` text,
`images` text,
- `is_out_of_stock` integer DEFAULT false NOT NULL,
- `is_suspended` integer DEFAULT false NOT NULL,
- `is_flash_available` integer DEFAULT false NOT NULL,
- `flash_price` text,
`is_offer` integer DEFAULT false NOT NULL,
`is_combo_only` integer DEFAULT false NOT NULL,
`created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL,
@@ -34,19 +28,12 @@ CREATE UNIQUE INDEX `unique_sku_feature_name` ON `sku_features` (`sku_id`,`featu
-- 2. Migrate each existing product into one default SKU plus a mandatory quantity feature.
INSERT INTO `product_skus` (
- `product_id`, `name`, `price`, `market_price`, `images`, `is_out_of_stock`,
- `is_suspended`, `is_flash_available`, `flash_price`, `created_at`
+ `product_id`, `name`, `images`, `created_at`
)
SELECT
`id`,
NULL,
- `price`,
- `market_price`,
`images`,
- `is_out_of_stock`,
- `is_suspended`,
- `is_flash_available`,
- `flash_price`,
`created_at`
FROM `product_info`;
@@ -59,6 +46,33 @@ FROM `product_info` `pi`
JOIN `product_skus` `ps` ON `ps`.`product_id` = `pi`.`id`
LEFT JOIN `units` `u` ON `u`.`id` = `pi`.`unit_id`;
+-- 2b. Create product_market_stats to hold pricing/flash/stock per SKU, and backfill it.
+CREATE TABLE `product_market_stats` (
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+ `sku_id` integer NOT NULL,
+ `market_price` text,
+ `our_price` text NOT NULL,
+ `is_flash_available` integer DEFAULT false NOT NULL,
+ `flash_price` text,
+ `is_out_of_stock` integer DEFAULT false NOT NULL,
+ `is_suspended` integer DEFAULT false NOT NULL,
+ FOREIGN KEY (`sku_id`) REFERENCES `product_skus`(`id`) ON UPDATE no action ON DELETE no action
+);
+
+CREATE UNIQUE INDEX `product_market_stats_sku_id_unique` ON `product_market_stats` (`sku_id`);
+
+INSERT INTO `product_market_stats` (`sku_id`, `market_price`, `our_price`, `is_flash_available`, `flash_price`, `is_out_of_stock`, `is_suspended`)
+SELECT
+ `ps`.`id`,
+ `pi`.`market_price`,
+ `pi`.`price`,
+ `pi`.`is_flash_available`,
+ `pi`.`flash_price`,
+ `pi`.`is_out_of_stock`,
+ `pi`.`is_suspended`
+FROM `product_info` `pi`
+JOIN `product_skus` `ps` ON `ps`.`product_id` = `pi`.`id`;
+
-- 3. Build a product_id -> sku_id mapping for downstream tables.
CREATE TABLE `__product_to_sku` (
`product_id` integer PRIMARY KEY,
diff --git a/packages/db_helper_sqlite/index.ts b/packages/db_helper_sqlite/index.ts
index bc6aad0..813cb73 100644
--- a/packages/db_helper_sqlite/index.ts
+++ b/packages/db_helper_sqlite/index.ts
@@ -32,6 +32,10 @@ export {
upsertConstants,
getCacheVersion,
incrementCacheVersion,
+ getAvailabilityVersionNum,
+ incrementAvailabilityVersionNum,
+ getSlotsVersionNum,
+ incrementSlotsVersionNum,
} from './src/admin-apis/const'
export {
@@ -314,12 +318,14 @@ export {
type BannerData,
// Product Store
getAllProductsForCache,
+ getAvailabilityForCache,
getAllStoresForCache,
getAllDeliverySlotsForCache,
getAllSpecialDealsForCache,
getAllProductTagsForCache,
getAllProductCombosForCache,
type ProductBasicData,
+ type AvailabilityCacheData,
type StoreBasicData,
type DeliverySlotData,
type SpecialDealData,
diff --git a/packages/db_helper_sqlite/src/admin-apis/const.ts b/packages/db_helper_sqlite/src/admin-apis/const.ts
index 6ffd507..194a34a 100644
--- a/packages/db_helper_sqlite/src/admin-apis/const.ts
+++ b/packages/db_helper_sqlite/src/admin-apis/const.ts
@@ -76,3 +76,69 @@ export async function incrementCacheVersion(): Promise {
return nextValue
})
}
+
+const AVAILABILITY_VERSION_KEY = CONST_KEYS.availabilityVersionNum
+
+export async function getAvailabilityVersionNum(): Promise {
+ const record = await db.query.keyValStore.findFirst({
+ where: eq(keyValStore.key, AVAILABILITY_VERSION_KEY),
+ columns: { value: true },
+ })
+
+ return record ? parseCacheVersion(record.value) : 0
+}
+
+export async function incrementAvailabilityVersionNum(): Promise {
+ return db.transaction(async (tx) => {
+ const existing = await tx.query.keyValStore.findFirst({
+ where: eq(keyValStore.key, AVAILABILITY_VERSION_KEY),
+ columns: { value: true },
+ })
+
+ const nextValue = parseCacheVersion(existing?.value) + 1
+
+ if (existing) {
+ await tx.update(keyValStore)
+ .set({ value: nextValue +'' })
+ .where(eq(keyValStore.key, AVAILABILITY_VERSION_KEY))
+ } else {
+ await tx.insert(keyValStore)
+ .values({ key: AVAILABILITY_VERSION_KEY, value: nextValue+'' })
+ }
+
+ return nextValue
+ })
+}
+
+const SLOTS_VERSION_KEY = CONST_KEYS.slotsVersionNum
+
+export async function getSlotsVersionNum(): Promise {
+ const record = await db.query.keyValStore.findFirst({
+ where: eq(keyValStore.key, SLOTS_VERSION_KEY),
+ columns: { value: true },
+ })
+
+ return record ? parseCacheVersion(record.value) : 0
+}
+
+export async function incrementSlotsVersionNum(): Promise {
+ return db.transaction(async (tx) => {
+ const existing = await tx.query.keyValStore.findFirst({
+ where: eq(keyValStore.key, SLOTS_VERSION_KEY),
+ columns: { value: true },
+ })
+
+ const nextValue = parseCacheVersion(existing?.value) + 1
+
+ if (existing) {
+ await tx.update(keyValStore)
+ .set({ value: nextValue +'' })
+ .where(eq(keyValStore.key, SLOTS_VERSION_KEY))
+ } else {
+ await tx.insert(keyValStore)
+ .values({ key: SLOTS_VERSION_KEY, value: nextValue+'' })
+ }
+
+ return nextValue
+ })
+}
diff --git a/packages/db_helper_sqlite/src/admin-apis/product.ts b/packages/db_helper_sqlite/src/admin-apis/product.ts
index 6d75c13..fa833f9 100644
--- a/packages/db_helper_sqlite/src/admin-apis/product.ts
+++ b/packages/db_helper_sqlite/src/admin-apis/product.ts
@@ -1,7 +1,7 @@
-// @ts-nocheck -- temporary: file partially updated for SKU migration; remaining functions to be fixed later
import { db } from '../db/db_index'
import {
productInfo,
+ productMarketStats,
productSkus,
skuFeatures,
productCombos,
@@ -45,7 +45,42 @@ import type {
type ProductRow = InferSelectModel
type SkuRow = InferSelectModel
+type MarketStatsRow = InferSelectModel
type SkuFeatureRow = InferSelectModel
+
+interface CreateSkuFeatureInput {
+ featureName?: string | null
+ featureValue: string
+}
+
+interface CreateComboItemInput {
+ skuId: number
+}
+
+interface CreateSkuInput {
+ name?: string | null
+ price: number
+ marketPrice?: number | null
+ images?: string[] | null
+ isFlashAvailable?: boolean
+ flashPrice?: number | null
+ isOutOfStock?: boolean
+ isSuspended?: boolean
+ isOffer?: boolean
+ isComboOnly?: boolean
+ features: CreateSkuFeatureInput[]
+ comboItems?: CreateComboItemInput[]
+}
+
+interface CreateProductInput {
+ name: string
+ shortDescription?: string | null
+ longDescription?: string | null
+ storeId?: number | null
+ incrementStep?: number
+ productType?: 'item' | 'combo'
+ skus: CreateSkuInput[]
+}
type UnitRow = InferSelectModel
type StoreRow = InferSelectModel
type SpecialDealRow = InferSelectModel
@@ -94,18 +129,23 @@ const mapSkuFeature = (feature: SkuFeatureRow): AdminSkuFeature => ({
featureValue: feature.featureValue,
})
-const mapSku = (sku: SkuRow, features: SkuFeatureRow[] = [], comboItems: any[] = []): AdminSku => ({
+const mapSku = (
+ sku: SkuRow,
+ features: SkuFeatureRow[] = [],
+ comboItems: any[] = [],
+ marketStats: MarketStatsRow | null = null
+): AdminSku => ({
id: sku.id,
productId: sku.productId,
name: sku.name ?? null,
- price: String(sku.price ?? '0'),
- marketPrice: sku.marketPrice ? String(sku.marketPrice) : null,
+ price: marketStats?.ourPrice ? String(marketStats.ourPrice) : '0',
+ marketPrice: marketStats?.marketPrice ? String(marketStats.marketPrice) : null,
images: getStringArray(sku.images),
imageKeys: getStringArray(sku.images),
- isOutOfStock: sku.isOutOfStock,
- isSuspended: sku.isSuspended,
- isFlashAvailable: sku.isFlashAvailable,
- flashPrice: sku.flashPrice ? String(sku.flashPrice) : null,
+ isOutOfStock: marketStats?.isOutOfStock ?? false,
+ isSuspended: marketStats?.isSuspended ?? false,
+ isFlashAvailable: marketStats?.isFlashAvailable ?? false,
+ flashPrice: marketStats?.flashPrice ? String(marketStats.flashPrice) : null,
isOffer: sku.isOffer,
isComboOnly: sku.isComboOnly,
createdAt: sku.createdAt,
@@ -134,7 +174,7 @@ const mapTagInfo = (tag: ProductTagInfoRow): AdminProductTagInfo => ({
export async function getAllProducts(): Promise {
type ProductWithRelationsRow = ProductRow & {
store: StoreRow | null
- skus: Array
+ skus: Array
}
const products = await db.query.productInfo.findMany({
orderBy: productInfo.name,
@@ -143,9 +183,10 @@ export async function getAllProducts(): Promise {
skus: {
with: {
features: true,
+ marketStats: true,
comboItems: {
with: {
- sku: { with: { product: true, features: true } },
+ sku: { with: { product: true, features: true, marketStats: true } },
},
},
},
@@ -163,9 +204,9 @@ export async function getAllProducts(): Promise {
features: (ci.sku?.features || []).map((f: any) => ({ featureName: f.featureName, featureValue: f.featureValue })),
productName: ci.sku?.product?.name ?? 'Unknown',
images: getStringArray(ci.sku?.images),
- price: String(ci.sku?.price ?? '0'),
+ price: ci.sku?.marketStats?.ourPrice ? String(ci.sku.marketStats.ourPrice) : '0',
}))
- return mapSku(sku, sku.features, comboItems)
+ return mapSku(sku, sku.features, comboItems, sku.marketStats)
}),
}))
}
@@ -178,9 +219,10 @@ export async function getProductById(id: number): Promise ({
skuId: ci.skuId,
skuName: ci.sku?.name ?? null,
- features: (ci.sku?.features || []).map((f) => ({ featureName: f.featureName, featureValue: f.featureValue })),
+ features: (ci.sku?.features || []).map((f: any) => ({ featureName: f.featureName, featureValue: f.featureValue })),
productName: ci.sku?.product?.name ?? 'Unknown',
images: getStringArray(ci.sku?.images),
- price: String(ci.sku?.price ?? '0'),
+ price: ci.sku?.marketStats?.ourPrice ? String(ci.sku.marketStats.ourPrice) : '0',
}))
- return mapSku(sku, sku.features, comboItems)
+ return mapSku(sku, sku.features, comboItems, sku.marketStats)
})
return {
@@ -272,20 +314,26 @@ export async function createProduct(input: CreateProductInput): Promise ({
productId: product.id,
name: sku.name ?? null,
- price: String(sku.price),
- marketPrice: sku.marketPrice != null ? String(sku.marketPrice) : null,
images: sku.images ?? null,
- isFlashAvailable: sku.isFlashAvailable ?? false,
- flashPrice: sku.flashPrice != null ? String(sku.flashPrice) : null,
isOffer: sku.isOffer ?? false,
isComboOnly: sku.isComboOnly ?? false,
- isSuspended: sku.isSuspended ?? false,
}))
).returning()
for (let i = 0; i < skuRows.length; i++) {
const skuRow = skuRows[i]
const sku = skus[i]
+
+ await db.insert(productMarketStats).values({
+ skuId: skuRow.id,
+ marketPrice: sku.marketPrice != null ? String(sku.marketPrice) : null,
+ ourPrice: sku.price != null ? String(sku.price) : '0',
+ isFlashAvailable: sku.isFlashAvailable ?? false,
+ flashPrice: sku.flashPrice != null ? String(sku.flashPrice) : null,
+ isOutOfStock: sku.isOutOfStock ?? false,
+ isSuspended: sku.isSuspended ?? false,
+ })
+
await db.insert(skuFeatures).values(
sku.features.map((f) => ({
skuId: skuRow.id,
@@ -306,13 +354,13 @@ export async function createProduct(input: CreateProductInput): Promise mapSku(s, s.features)),
+ skus: createdSkus.map((s) => mapSku(s, s.features, [], s.marketStats)),
}
}
@@ -368,11 +416,11 @@ export async function updateProduct(id: number, input: any): Promise c.comboSkuId)))
if (comboIds.length > 0) {
- const combos = await db.query.productSkus.findMany({
- where: inArray(productSkus.id, comboIds),
- columns: { id: true, isSuspended: true },
+ const combos = await db.query.productMarketStats.findMany({
+ where: inArray(productMarketStats.skuId, comboIds),
+ columns: { skuId: true, isSuspended: true },
})
- const activeComboIds = combos.filter((c) => !c.isSuspended).map((c) => c.id)
+ const activeComboIds = combos.filter((c) => !c.isSuspended).map((c) => c.skuId)
if (activeComboIds.length > 0) {
throw new Error(
`Cannot suspend SKU(s): they are members of combo(s) ${activeComboIds.join(', ')} that are not suspended`
@@ -387,17 +435,35 @@ export async function updateProduct(id: number, input: any): Promise ({
@@ -421,16 +487,21 @@ export async function updateProduct(id: number, input: any): Promise ({
skuId: newSku.id,
@@ -447,7 +518,7 @@ export async function updateProduct(id: number, input: any): Promise mapSku(s, s.features)),
+ skus: updatedProduct.skus.map((s) => mapSku(s, s.features, [], s.marketStats)),
}
}
@@ -898,15 +969,32 @@ export async function updateProductPrices(updates: Array<{
const { productId, price, marketPrice, flashPrice, isFlashAvailable } = update
const updateData: any = {}
- if (price !== undefined) updateData.price = price.toString()
+ if (price !== undefined) updateData.ourPrice = price.toString()
if (marketPrice !== undefined) updateData.marketPrice = marketPrice === null ? null : marketPrice.toString()
if (flashPrice !== undefined) updateData.flashPrice = flashPrice === null ? null : flashPrice.toString()
if (isFlashAvailable !== undefined) updateData.isFlashAvailable = isFlashAvailable
- await tx
- .update(productSkus)
- .set(updateData)
- .where(eq(productSkus.id, productId))
+ if (Object.keys(updateData).length === 0) continue
+
+ const existingMarketStats = await tx.query.productMarketStats.findFirst({
+ where: eq(productMarketStats.skuId, productId),
+ columns: { id: true },
+ })
+
+ if (existingMarketStats) {
+ await tx
+ .update(productMarketStats)
+ .set(updateData)
+ .where(eq(productMarketStats.skuId, productId))
+ } else {
+ await tx.insert(productMarketStats).values({
+ skuId: productId,
+ ourPrice: updateData.ourPrice ?? '0',
+ marketPrice: updateData.marketPrice ?? null,
+ flashPrice: updateData.flashPrice ?? null,
+ isFlashAvailable: updateData.isFlashAvailable ?? false,
+ })
+ }
}
})
@@ -956,7 +1044,7 @@ export interface CreateSpecialDealInput {
}
export async function createSpecialDealsForSku(
- productId: number,
+ skuId: number,
deals: CreateSpecialDealInput[]
): Promise {
if (deals.length === 0) {
@@ -964,7 +1052,7 @@ export async function createSpecialDealsForSku(
}
const dealInserts = deals.map((deal) => ({
- productId,
+ skuId,
quantity: deal.quantity.toString(),
price: deal.price.toString(),
validTill: new Date(deal.validTill),
@@ -1025,7 +1113,7 @@ export async function updateSkuDeals(
if (dealsToAdd.length > 0) {
const dealInserts = dealsToAdd.map((deal) => ({
- productId,
+ skuId: productId,
quantity: deal.quantity.toString(),
price: deal.price.toString(),
validTill: new Date(deal.validTill),
diff --git a/packages/db_helper_sqlite/src/db/schema.ts b/packages/db_helper_sqlite/src/db/schema.ts
index eeb6c1e..a767840 100644
--- a/packages/db_helper_sqlite/src/db/schema.ts
+++ b/packages/db_helper_sqlite/src/db/schema.ts
@@ -201,18 +201,23 @@ export const productSkus = sqliteTable('product_skus', {
id: integer().primaryKey({ autoIncrement: true }),
productId: integer('product_id').notNull().references(() => productInfo.id),
name: text(),
- price: numericText('price').notNull(),
- marketPrice: numericText('market_price'),
images: jsonText('images'),
- isOutOfStock: integer('is_out_of_stock', { mode: 'boolean' }).notNull().default(false),
- isSuspended: integer('is_suspended', { mode: 'boolean' }).notNull().default(false),
- isFlashAvailable: integer('is_flash_available', { mode: 'boolean' }).notNull().default(false),
- flashPrice: numericText('flash_price'),
isOffer: integer('is_offer', { mode: 'boolean' }).notNull().default(false),
isComboOnly: integer('is_combo_only', { mode: 'boolean' }).notNull().default(false),
createdAt: timestampText('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
})
+export const productMarketStats = sqliteTable('product_market_stats', {
+ id: integer().primaryKey({ autoIncrement: true }),
+ skuId: integer('sku_id').notNull().references(() => productSkus.id).unique(),
+ marketPrice: numericText('market_price'),
+ ourPrice: numericText('our_price').notNull(),
+ isFlashAvailable: integer('is_flash_available', { mode: 'boolean' }).notNull().default(false),
+ flashPrice: numericText('flash_price'),
+ isOutOfStock: integer('is_out_of_stock', { mode: 'boolean' }).notNull().default(false),
+ isSuspended: integer('is_suspended', { mode: 'boolean' }).notNull().default(false),
+})
+
export const skuFeatures = sqliteTable('sku_features', {
id: integer().primaryKey({ autoIncrement: true }),
skuId: integer('sku_id').notNull().references(() => productSkus.id),
@@ -590,6 +595,7 @@ export const productInfoRelations = relations(productInfo, ({ one, many }) => ({
export const productSkusRelations = relations(productSkus, ({ one, many }) => ({
product: one(productInfo, { fields: [productSkus.productId], references: [productInfo.id] }),
features: many(skuFeatures),
+ marketStats: one(productMarketStats),
specialDeals: many(specialDeals),
orderItems: many(orderItems),
cartItems: many(cartItems),
@@ -597,6 +603,10 @@ export const productSkusRelations = relations(productSkus, ({ one, many }) => ({
comboItems: many(productCombos, { relationName: 'comboSku' }),
}))
+export const productMarketStatsRelations = relations(productMarketStats, ({ one }) => ({
+ sku: one(productSkus, { fields: [productMarketStats.skuId], references: [productSkus.id] }),
+}))
+
export const skuFeaturesRelations = relations(skuFeatures, ({ one }) => ({
sku: one(productSkus, { fields: [skuFeatures.skuId], references: [productSkus.id] }),
}))
diff --git a/packages/db_helper_sqlite/src/lib/const-keys.ts b/packages/db_helper_sqlite/src/lib/const-keys.ts
index 60e81e2..2c61b25 100644
--- a/packages/db_helper_sqlite/src/lib/const-keys.ts
+++ b/packages/db_helper_sqlite/src/lib/const-keys.ts
@@ -13,6 +13,8 @@ export const CONST_KEYS = {
readableOrderId: 'readableOrderId',
versionNum: 'versionNum',
cacheVersion: 'cache_version',
+ availabilityVersionNum: 'availability_version_num',
+ slotsVersionNum: 'slots_version_num',
playStoreUrl: 'playStoreUrl',
appStoreUrl: 'appStoreUrl',
popularItems: 'popularItems',
@@ -37,6 +39,8 @@ export const CONST_LABELS: Record = {
readableOrderId: 'Readable Order ID',
versionNum: 'Version Number',
'cache_version': 'Cache Version',
+ availability_version_num: 'Availability Cache Version',
+ slots_version_num: 'Slots Cache Version',
playStoreUrl: 'Play Store URL',
appStoreUrl: 'App Store URL',
popularItems: 'Popular Items',
@@ -67,6 +71,8 @@ export const CONST_TYPES: Record = {
readableOrderId: 'number',
versionNum: 'string',
'cache_version': 'number',
+ availability_version_num: 'number',
+ slots_version_num: 'number',
playStoreUrl: 'string',
appStoreUrl: 'string',
popularItems: 'string',
@@ -91,6 +97,8 @@ export const CONST_VISIBILITY: Record = {
readableOrderId: false,
versionNum: true,
'cache_version': false,
+ availability_version_num: false,
+ slots_version_num: false,
playStoreUrl: true,
appStoreUrl: true,
popularItems: true,
diff --git a/packages/db_helper_sqlite/src/stores/store-helpers.ts b/packages/db_helper_sqlite/src/stores/store-helpers.ts
index 1404b17..03758b6 100644
--- a/packages/db_helper_sqlite/src/stores/store-helpers.ts
+++ b/packages/db_helper_sqlite/src/stores/store-helpers.ts
@@ -5,6 +5,7 @@ import { db } from '../db/db_index'
import {
homeBanners,
productInfo,
+ productMarketStats,
productSkus,
skuFeatures,
deliverySlotInfo,
@@ -61,6 +62,16 @@ export interface ProductBasicData {
productType: string
}
+export interface AvailabilityCacheData {
+ id: number
+ price: string
+ marketPrice: string | null
+ flashPrice: string | null
+ isFlashAvailable: boolean
+ isOutOfStock: boolean
+ isSuspended: boolean
+}
+
export interface StoreBasicData {
id: number
name: string
@@ -107,15 +118,18 @@ export interface ProductTagData {
export async function getAllProductsForCache(): Promise {
const skus = await db.query.productSkus.findMany({
- where: eq(productSkus.isSuspended, false),
with: {
product: true,
features: true,
+ marketStats: true,
},
})
- return skus.map((sku) => {
+ return skus
+ .filter((sku) => !sku.marketStats?.isSuspended)
+ .map((sku) => {
const features = sku.features || []
+ const marketStats = sku.marketStats
return {
id: sku.id,
productId: sku.productId,
@@ -123,21 +137,37 @@ export async function getAllProductsForCache(): Promise {
skuName: sku.name ?? null,
shortDescription: sku.product?.shortDescription ?? null,
longDescription: sku.product?.longDescription ?? null,
- price: String(sku.price ?? '0'),
- marketPrice: sku.marketPrice ? String(sku.marketPrice) : null,
+ price: marketStats?.ourPrice ? String(marketStats.ourPrice) : '0',
+ marketPrice: marketStats?.marketPrice ? String(marketStats.marketPrice) : null,
images: sku.images,
- isOutOfStock: sku.isOutOfStock,
+ isOutOfStock: marketStats?.isOutOfStock ?? false,
storeId: sku.product?.storeId ?? null,
unitNotation: composeUnitNotation(features),
incrementStep: sku.product?.incrementStep ?? 1,
productQuantity: 1,
- isFlashAvailable: sku.isFlashAvailable,
- flashPrice: sku.flashPrice ? String(sku.flashPrice) : null,
+ isFlashAvailable: marketStats?.isFlashAvailable ?? false,
+ flashPrice: marketStats?.flashPrice ? String(marketStats.flashPrice) : null,
productType: sku.product?.productType ?? 'item',
}
})
}
+export async function getAvailabilityForCache(): Promise {
+ const stats = await db.query.productMarketStats.findMany({})
+
+ return stats
+ .filter((stat) => !stat.isSuspended)
+ .map((stat) => ({
+ id: stat.skuId,
+ price: stat.ourPrice ? String(stat.ourPrice) : '0',
+ marketPrice: stat.marketPrice ? String(stat.marketPrice) : null,
+ flashPrice: stat.flashPrice ? String(stat.flashPrice) : null,
+ isFlashAvailable: stat.isFlashAvailable,
+ isOutOfStock: stat.isOutOfStock,
+ isSuspended: stat.isSuspended,
+ }))
+}
+
export async function getAllStoresForCache(): Promise {
return db.query.storeInfo.findMany({
columns: { id: true, name: true, description: true },
@@ -192,20 +222,21 @@ export interface ProductComboCacheData {
images: unknown
unitNotation: string
price: string
+ isOffer: boolean
}
export async function getAllProductCombosForCache(): Promise {
const results = await db.query.productCombos.findMany({
with: {
- sku: { with: { product: true, features: true } },
+ sku: { with: { product: true, features: true, marketStats: true } },
},
})
const suspendedSkuIds = new Set(
(await db
- .select({ id: productSkus.id })
- .from(productSkus)
- .where(eq(productSkus.isSuspended, true))).map((r) => r.id)
+ .select({ id: productMarketStats.skuId })
+ .from(productMarketStats)
+ .where(eq(productMarketStats.isSuspended, true))).map((r) => r.id)
)
return results
@@ -219,7 +250,8 @@ export async function getAllProductCombosForCache(): Promise 0) {
skusData = await db.query.productSkus.findMany({
- where: eq(productSkus.isSuspended, false),
with: {
product: {
with: { store: true },
},
features: true,
+ marketStats: true,
},
})
- skusData = skusData.filter((item: any) => skuIdSet.has(item.id))
+ skusData = skusData.filter((item: any) => skuIdSet.has(item.id) && !item.marketStats?.isSuspended)
}
const skuMap = new Map(skusData.map((s: any) => [s.id, s]))
@@ -341,6 +373,7 @@ export async function getAllSlotsWithProductsForCache(): Promise => p != null)
.map((sku: any) => {
const features = sku.features || []
+ const marketStats = sku.marketStats
return {
id: sku.id,
productId: sku.productId,
@@ -348,8 +381,8 @@ export async function getAllSlotsWithProductsForCache(): Promise {
export async function getProductDetailById(skuId: number): Promise {
const sku = await db.query.productSkus.findFirst({
- where: and(eq(productSkus.id, skuId), eq(productSkus.isSuspended, false)),
+ where: eq(productSkus.id, skuId),
with: {
product: true,
features: true,
+ marketStats: true,
},
})
if (!sku) {
return null
}
+ if (sku.marketStats?.isSuspended) {
+ return null
+ }
const features = sku.features || []
const product = sku.product
+ const marketStats = sku.marketStats
const storeData = product?.storeId ? await db.query.storeInfo.findFirst({
where: eq(storeInfo.id, product.storeId),
@@ -48,7 +53,7 @@ export async function getProductDetailById(skuId: number): Promise ({
quantity: String(deal.quantity ?? '0'),
@@ -202,30 +208,32 @@ export async function getAllProductsWithUnits(tagId?: number): Promise {
+ if (sku.marketStats?.isSuspended) return false
if (!tagId) return true
return taggedProductIdSet.has(sku.productId)
})
.map((sku) => {
const features = sku.features || []
+ const marketStats = sku.marketStats
return {
id: sku.product?.id ?? 0,
name: composeSkuName(sku.product?.name ?? 'Unknown', features),
skuId: sku.id,
skuName: sku.name ?? null,
shortDescription: sku.product?.shortDescription ?? null,
- price: String(sku.price ?? '0'),
- marketPrice: sku.marketPrice ? String(sku.marketPrice) : null,
+ price: marketStats?.ourPrice ? String(marketStats.ourPrice) : '0',
+ marketPrice: marketStats?.marketPrice ? String(marketStats.marketPrice) : null,
images: sku.images,
- isOutOfStock: sku.isOutOfStock,
+ isOutOfStock: marketStats?.isOutOfStock ?? false,
unitShortNotation: composeUnitNotation(features),
productQuantity: 1,
features: features.map((f) => ({ featureName: f.featureName, featureValue: f.featureValue })),
@@ -238,9 +246,9 @@ export async function getAllProductsWithUnits(tagId?: number): Promise {
const suspendedSkus = await db
- .select({ id: productSkus.id })
- .from(productSkus)
- .where(eq(productSkus.isSuspended, true))
+ .select({ id: productMarketStats.skuId })
+ .from(productMarketStats)
+ .where(eq(productMarketStats.isSuspended, true))
return suspendedSkus.map(sp => sp.id)
}
@@ -279,16 +287,18 @@ export interface SkuSummary {
export async function getAllSkusSummary(): Promise {
const skus = await db.query.productSkus.findMany({
- where: eq(productSkus.isSuspended, false),
with: {
features: true,
+ marketStats: true,
product: {
columns: { name: true },
},
},
})
- return skus.map((sku) => {
+ return skus
+ .filter((sku) => !sku.marketStats?.isSuspended)
+ .map((sku) => {
const featureValues = (sku.features || []).map((f) => f.featureValue)
const label = [sku.product?.name ?? 'Unknown', ...featureValues].join(' ')
return {
@@ -319,10 +329,12 @@ export interface OffersPageData {
const mapOffersPageProduct = (sku: {
id: number
- price: string | null
- marketPrice: string | null
+ marketStats: {
+ ourPrice: string | null
+ marketPrice: string | null
+ isOutOfStock: boolean
+ } | null
images: unknown
- isOutOfStock: boolean
product: { name: string; incrementStep: number | null } | null
features: Array<{ featureValue: string }>
}): OffersPageProductData => {
@@ -330,21 +342,21 @@ const mapOffersPageProduct = (sku: {
return {
id: sku.id,
name: composeSkuName(sku.product?.name ?? 'Unknown', features),
- price: String(sku.price ?? '0'),
- marketPrice: sku.marketPrice ? String(sku.marketPrice) : null,
+ price: sku.marketStats?.ourPrice ? String(sku.marketStats.ourPrice) : '0',
+ marketPrice: sku.marketStats?.marketPrice ? String(sku.marketStats.marketPrice) : null,
unitNotation: composeUnitNotation(features),
images: sku.images,
- isOutOfStock: sku.isOutOfStock,
+ isOutOfStock: sku.marketStats?.isOutOfStock ?? false,
incrementStep: sku.product?.incrementStep ?? 1,
}
}
export async function getOffersAndCombos(): Promise {
const skus = await db.query.productSkus.findMany({
- where: eq(productSkus.isSuspended, false),
with: {
product: true,
features: true,
+ marketStats: true,
},
})
@@ -352,6 +364,7 @@ export async function getOffersAndCombos(): Promise {
const offers: OffersPageProductData[] = []
for (const sku of skus) {
+ if (sku.marketStats?.isSuspended) continue
if (sku.product?.productType === 'combo') {
combos.push(mapOffersPageProduct(sku))
}
diff --git a/packages/db_helper_sqlite/src/user-apis/slots.ts b/packages/db_helper_sqlite/src/user-apis/slots.ts
index 8ad89ce..f631ffd 100644
--- a/packages/db_helper_sqlite/src/user-apis/slots.ts
+++ b/packages/db_helper_sqlite/src/user-apis/slots.ts
@@ -1,5 +1,5 @@
import { db } from '../db/db_index'
-import { deliverySlotInfo, productSkus } from '../db/schema'
+import { deliverySlotInfo, productMarketStats } from '../db/schema'
import { asc, eq } from 'drizzle-orm'
import type { InferSelectModel } from 'drizzle-orm'
import type { UserDeliverySlot, UserSlotAvailability } from '@packages/shared'
@@ -27,17 +27,16 @@ export async function getActiveSlotsList(): Promise {
}
export async function getProductAvailability(): Promise {
- const skus = await db.query.productSkus.findMany({
- where: eq(productSkus.isSuspended, false),
- with: {
- product: { columns: { name: true } },
+ const stats = await db.query.productMarketStats.findMany({
+ where: eq(productMarketStats.isSuspended, false),
+ columns: {
+ skuId: true,
+ isOutOfStock: true,
},
})
- return skus.map((sku) => ({
- id: sku.id,
- name: sku.product?.name ?? 'Unknown',
- isOutOfStock: sku.isOutOfStock,
- isFlashAvailable: sku.isFlashAvailable,
+ return stats.map((stat) => ({
+ id: stat.skuId,
+ isOutOfStock: stat.isOutOfStock,
}))
}
diff --git a/packages/db_helper_sqlite/src/user-apis/stores.ts b/packages/db_helper_sqlite/src/user-apis/stores.ts
index dd9019a..b31680d 100644
--- a/packages/db_helper_sqlite/src/user-apis/stores.ts
+++ b/packages/db_helper_sqlite/src/user-apis/stores.ts
@@ -21,13 +21,13 @@ export async function getStoreSummaries(): Promise {
}).from(storeInfo)
const skus = await db.query.productSkus.findMany({
- where: eq(productSkus.isSuspended, false),
- with: { product: true },
+ with: { product: true, marketStats: true },
orderBy: asc(productSkus.id),
})
+ const activeSkus = skus.filter((sku) => !sku.marketStats?.isSuspended)
const skusByStore = new Map()
- for (const sku of skus) {
+ for (const sku of activeSkus) {
const storeId = sku.product?.storeId
if (storeId == null) continue
if (!skusByStore.has(storeId)) skusByStore.set(storeId, [])
@@ -77,30 +77,31 @@ export async function getStoreDetail(storeId: number): Promise 0
? await db.query.productSkus.findMany({
- where: and(
- inArray(productSkus.productId, productIdArr),
- eq(productSkus.isSuspended, false)
- ),
+ where: inArray(productSkus.productId, productIdArr),
with: {
product: true,
features: true,
+ marketStats: true,
},
})
: []
- const products: UserStoreProductData[] = skus.map((sku) => {
+ const products: UserStoreProductData[] = skus
+ .filter((sku) => !sku.marketStats?.isSuspended)
+ .map((sku) => {
const features = sku.features || []
+ const marketStats = sku.marketStats
return {
id: sku.id,
name: composeSkuName(sku.product?.name ?? 'Unknown', features),
shortDescription: sku.product?.shortDescription ?? null,
- price: String(sku.price ?? '0'),
- marketPrice: sku.marketPrice ? String(sku.marketPrice) : null,
+ price: marketStats?.ourPrice ? String(marketStats.ourPrice) : '0',
+ marketPrice: marketStats?.marketPrice ? String(marketStats.marketPrice) : null,
incrementStep: sku.product?.incrementStep ?? 1,
unit: composeUnitNotation(features),
unitNotation: composeUnitNotation(features),
images: getStringArray(sku.images),
- isOutOfStock: sku.isOutOfStock,
+ isOutOfStock: marketStats?.isOutOfStock ?? false,
productQuantity: 1,
}
})
diff --git a/packages/shared/index.ts b/packages/shared/index.ts
index a502ff8..79d2335 100644
--- a/packages/shared/index.ts
+++ b/packages/shared/index.ts
@@ -2,6 +2,7 @@ export const CACHE_FILENAMES = {
products: 'products.json',
stores: 'stores.json',
slots: 'slots.json',
+ availability: 'availability.json',
essentialConsts: 'essential-consts.json',
banners: 'banners.json',
} as const
diff --git a/packages/shared/types/user.ts b/packages/shared/types/user.ts
index 1efc02b..e98bd3f 100644
--- a/packages/shared/types/user.ts
+++ b/packages/shared/types/user.ts
@@ -267,6 +267,7 @@ export interface UserProductComboItem {
productName: string;
images: string[] | null;
price: string;
+ isOffer: boolean;
}
export interface UserProductDetailData {
@@ -320,32 +321,34 @@ export interface UserCreateReviewResponse {
export interface UserSlotProduct {
id: number;
- name: string;
- shortDescription: string | null;
- productQuantity: number;
- price: string;
- marketPrice: string | null;
- unit: string | null;
- images: string[];
- isOutOfStock: boolean;
- storeId: number | null;
- nextDeliveryDate: Date;
+ images: string[] | null;
}
export interface UserSlotWithProducts {
id: number;
deliveryTime: Date;
freezeTime: Date;
- isActive: boolean;
- isCapacityFull: boolean;
products: UserSlotProduct[];
}
export interface UserSlotAvailability {
id: number;
- name: string;
isOutOfStock: boolean;
+}
+
+export interface UserAvailabilityEntry {
+ id: number;
+ price: string;
+ marketPrice: string | null;
+ flashPrice: string | null;
isFlashAvailable: boolean;
+ isOutOfStock: boolean;
+ isSuspended: boolean;
+}
+
+export interface UserAvailabilityResponse {
+ availability: UserAvailabilityEntry[];
+ count: number;
}
export interface UserDeliverySlot {