enh
This commit is contained in:
parent
b41980736a
commit
0ca746d3c6
33 changed files with 770 additions and 231 deletions
|
|
@ -463,7 +463,7 @@ export default function OrderDetails() {
|
|||
{item.name}
|
||||
</MyText>
|
||||
<MyText style={tw`text-xs text-gray-500`}>
|
||||
{Number(item.quantity) * item.productSize} {item.unit} × ₹{item.price}
|
||||
{Number(item.quantity)} x {item.productSize}{item.unit} × ₹{item.price}
|
||||
</MyText>
|
||||
<View style={tw`flex-row items-center mt-2 gap-3`}>
|
||||
<TouchableOpacity
|
||||
|
|
|
|||
|
|
@ -413,7 +413,7 @@ const OrderItem = ({ order, refetch }: { order: OrderType; refetch: () => void }
|
|||
<View key={idx} style={tw`py-2 border-b border-gray-50 last:border-0`}>
|
||||
<View style={tw`flex-row items-center`}>
|
||||
<View style={tw`bg-gray-100 px-2 py-1 rounded items-center justify-center mr-2`}>
|
||||
<MyText style={tw`text-xs font-bold text-gray-600`}>{item.quantity * item.productSize } {item.unit}</MyText>
|
||||
<MyText style={tw`text-xs font-bold text-gray-600`}>{item.quantity} x {item.productSize}{item.unit}</MyText>
|
||||
</View>
|
||||
<MyText style={tw`text-sm text-gray-800 flex-1`} numberOfLines={1} ellipsizeMode="tail">
|
||||
{item.name.length > 30 ? `${item.name.substring(0, 30)}...` : item.name}
|
||||
|
|
|
|||
|
|
@ -402,15 +402,6 @@ export default function PricesOverview() {
|
|||
/>
|
||||
</View>
|
||||
|
||||
<View style={tw`mb-4`}>
|
||||
<MyText style={tw`text-sm font-medium mb-1`}>Size</MyText>
|
||||
<TextInput
|
||||
style={tw`border border-gray-300 rounded-md px-3 py-2`}
|
||||
keyboardType="decimal-pad"
|
||||
placeholder="Enter size"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={tw`bg-blue-600 py-3 rounded-md items-center`}
|
||||
onPress={saveEditDialog}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { Buffer } from 'buffer'
|
||||
import { scaffoldProducts } from '@/src/trpc/apis/common-apis/common'
|
||||
import { scaffoldProducts, scaffoldAvailability } from '@/src/trpc/apis/common-apis/common'
|
||||
import { scaffoldEssentialConsts } from '@/src/trpc/apis/common-apis/common-trpc-index'
|
||||
import { scaffoldStores } from '@/src/trpc/apis/user-apis/apis/stores'
|
||||
import { scaffoldSlotsWithProducts } from '@/src/trpc/apis/user-apis/apis/slots'
|
||||
import { scaffoldBanners } from '@/src/trpc/apis/user-apis/apis/banners'
|
||||
import { scaffoldStoreWithProducts } from '@/src/trpc/apis/user-apis/apis/stores'
|
||||
import { getStoresSummary, incrementCacheVersion } from '@/src/dbService'
|
||||
import { getStoresSummary, incrementCacheVersion, incrementAvailabilityVersionNum, incrementSlotsVersionNum } from '@/src/dbService'
|
||||
import { imageUploadS3 } from '@/src/lib/s3-client'
|
||||
import { getApiCacheKey, getCloudflareApiToken, getCloudflareZoneId, getAssetsDomain } from '@/src/lib/env-exporter'
|
||||
import { CACHE_FILENAMES } from '@packages/shared'
|
||||
|
|
@ -13,16 +13,29 @@ import { retryWithExponentialBackoff } from '@/src/lib/retry'
|
|||
|
||||
const buildCachePath = (path: string, version: number) => `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<CreateAllCacheFilesResult>
|
|||
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<CreateAllCacheFilesResult>
|
|||
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<CreateAllCacheFilesResult>
|
|||
products: productsKey,
|
||||
essentialConsts: essentialConstsKey,
|
||||
stores: storesKey,
|
||||
slots: slotsKey,
|
||||
slotsVersion,
|
||||
availabilityVersion,
|
||||
banners: bannersKey,
|
||||
individualStores: individualStoreKeys,
|
||||
}
|
||||
|
|
@ -98,6 +115,23 @@ async function createProductsFileInternal(version: number): Promise<string> {
|
|||
|
||||
}
|
||||
|
||||
export async function createAvailabilityCacheFile(): Promise<number> {
|
||||
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<string> {
|
||||
const essentialConstsData = await scaffoldEssentialConsts()
|
||||
const jsonContent = JSON.stringify(essentialConstsData, null, 2)
|
||||
|
|
@ -120,15 +154,21 @@ async function createStoresFileInternal(version: number): Promise<string> {
|
|||
)
|
||||
}
|
||||
|
||||
async function createSlotsFileInternal(version: number): Promise<string> {
|
||||
export async function createSlotsCacheFile(): Promise<number> {
|
||||
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<string> {
|
||||
|
|
|
|||
|
|
@ -240,6 +240,7 @@ export {
|
|||
// Store Helpers
|
||||
getAllBannersForCache,
|
||||
getAllProductsForCache,
|
||||
getAvailabilityForCache,
|
||||
getAllStoresForCache,
|
||||
getAllDeliverySlotsForCache,
|
||||
getAllSpecialDealsForCache,
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ interface Product {
|
|||
productName: string
|
||||
images: string[] | null
|
||||
price: string
|
||||
isOffer: boolean
|
||||
}>
|
||||
}
|
||||
|
||||
|
|
@ -250,6 +251,7 @@ export async function getAllProducts(): Promise<Product[]> {
|
|||
productName: ci.productName,
|
||||
images: ci.images ? scaffoldAssetUrl((ci.images as string[]) || []) : null,
|
||||
price: ci.price,
|
||||
isOffer: ci.isOffer,
|
||||
}))
|
||||
|
||||
products.push({
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -846,7 +851,9 @@ export const productRouter = router({
|
|||
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)`,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -17,28 +17,16 @@ export async function scaffoldSlotsWithProducts(): Promise<UserSlotsWithProducts
|
|||
|
||||
const productAvailability = await getUserProductAvailabilityInDb()
|
||||
|
||||
/*
|
||||
// Old implementation - direct DB query:
|
||||
const allProducts = await db
|
||||
.select({
|
||||
id: productInfo.id,
|
||||
name: productInfo.name,
|
||||
isOutOfStock: productInfo.isOutOfStock,
|
||||
isFlashAvailable: productInfo.isFlashAvailable,
|
||||
})
|
||||
.from(productInfo)
|
||||
.where(eq(productInfo.isSuspended, false));
|
||||
|
||||
const productAvailability = allProducts.map(product => ({
|
||||
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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<ReturnType<typeof scaffoldProducts>>;
|
||||
export type AvailabilityApiType = Awaited<ReturnType<typeof scaffoldAvailability>>;
|
||||
export type StoresApiType = Awaited<ReturnType<typeof scaffoldStores>>;
|
||||
export type SlotsApiType = Awaited<ReturnType<typeof scaffoldSlotsWithProducts>>;
|
||||
export type EssentialConstsApiType = Awaited<ReturnType<typeof scaffoldEssentialConsts>>;
|
||||
|
|
|
|||
|
|
@ -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/<dump>.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.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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<ProductDetailProps> = ({ productId, isFlashDeliver
|
|||
</View>
|
||||
</View>
|
||||
|
||||
{/* Combo Items */}
|
||||
{productDetail.productType === 'combo' && productDetail.comboItems && productDetail.comboItems.length > 0 && (
|
||||
<View style={tw`px-4 mb-4`}>
|
||||
<View style={tw`bg-white p-5 rounded-2xl shadow-sm border border-gray-100`}>
|
||||
<View style={tw`flex-row items-center mb-4`}>
|
||||
<View style={tw`w-8 h-8 bg-brand50 rounded-full items-center justify-center mr-3`}>
|
||||
<MaterialIcons name="view-list" size={18} color="#3B82F6" />
|
||||
</View>
|
||||
<MyText style={tw`text-lg font-bold text-gray-900`}>Included Items</MyText>
|
||||
</View>
|
||||
|
||||
{productDetail.comboItems.map((comboItem, index) => (
|
||||
<View
|
||||
key={comboItem.skuId}
|
||||
style={tw`flex-row items-center py-2 ${index !== productDetail.comboItems.length - 1 ? 'border-b border-gray-50' : ''}`}
|
||||
>
|
||||
<View style={tw`w-12 h-12 bg-gray-100 rounded-lg overflow-hidden items-center justify-center mr-3`}>
|
||||
{comboItem.images?.[0] ? (
|
||||
<Image
|
||||
source={{ uri: comboItem.images[0] }}
|
||||
style={tw`w-full h-full`}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
) : (
|
||||
<MaterialIcons name="image" size={20} color="#9CA3AF" />
|
||||
)}
|
||||
</View>
|
||||
<View style={tw`flex-1`}>
|
||||
<View style={tw`flex-row items-center`}>
|
||||
<MyText style={tw`text-sm font-bold text-gray-900 flex-1`} numberOfLines={1}>
|
||||
{comboItem.productName}
|
||||
</MyText>
|
||||
{comboItem.isOffer && (
|
||||
<View style={tw`ml-2 bg-pink-100 px-2 py-0.5 rounded-full`}>
|
||||
<MyText style={tw`text-[10px] font-bold text-pink-600`}>OFFER</MyText>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<MyText style={tw`text-xs text-gray-500 mt-0.5`}>
|
||||
{comboItem.unitNotation || comboItem.skuName || ''}
|
||||
</MyText>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Delivery Slots */}
|
||||
<View style={tw`px-4 mb-4`}>
|
||||
<View style={tw`bg-white p-5 rounded-2xl shadow-sm border border-gray-100`}>
|
||||
|
|
|
|||
|
|
@ -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<void> =>
|
|||
};
|
||||
|
||||
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<CartData, Error> = useQuery({
|
||||
|
|
@ -193,7 +193,7 @@ export function useGetCart(options: UseGetCartOptions = {}, cartType: CartType =
|
|||
const cartItems = await getLocalCart(cartType);
|
||||
|
||||
const productMap: Record<number, Omit<ProductSummary, 'isOutOfStock' | 'isFlashAvailable'>> = 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 {
|
||||
|
|
|
|||
|
|
@ -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<ProductsResponse>({
|
||||
const productsQuery = useQuery<ProductsResponse>({
|
||||
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<number, AvailabilityEntry> = {}
|
||||
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<AvailabilityResponse>({
|
||||
queryKey: ['availability', cacheUrl],
|
||||
queryFn: async () => {
|
||||
if (!cacheUrl) {
|
||||
throw new Error('Cache URL not available')
|
||||
}
|
||||
const response = await axios.get<AvailabilityResponse>(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<SlotsResponse>({
|
||||
queryKey: ['slots', cacheUrl],
|
||||
|
|
|
|||
|
|
@ -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[]
|
||||
|
|
|
|||
|
|
@ -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<number, ProductSlotInfo>;
|
||||
refetchSlots: (() => Promise<void>) | null;
|
||||
setSlotsData: (slots: Slot[], productAvailability: ProductAvailability[]) => void;
|
||||
setSlotsData: (slots: Slot[], productAvailability: ProductAvailability[], availability: AvailabilityEntry[]) => void;
|
||||
clearSlotsData: () => void;
|
||||
setRefetchSlots: (refetch: () => Promise<void>) => void;
|
||||
}
|
||||
|
|
@ -25,15 +27,20 @@ export const useCentralSlotStore = create<CentralSlotState>((set) => ({
|
|||
slots: [],
|
||||
productSlotsMap: {},
|
||||
refetchSlots: null,
|
||||
setSlotsData: (slots, productAvailability) => {
|
||||
setSlotsData: (slots, productAvailability, availability) => {
|
||||
const productSlotsMap: Record<number, ProductSlotInfo> = {};
|
||||
const availabilityById: Record<number, AvailabilityEntry> = {};
|
||||
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<CentralSlotState>((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 () => {
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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<ProductsResponse>({
|
||||
const productsQuery = useQuery<ProductsResponse>({
|
||||
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<number, AvailabilityEntry> = {}
|
||||
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<AvailabilityResponse>({
|
||||
queryKey: ['availability', cacheUrl],
|
||||
queryFn: async () => {
|
||||
if (!cacheUrl) {
|
||||
throw new Error('Cache URL not available')
|
||||
}
|
||||
const response = await axios.get<AvailabilityResponse>(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<SlotsResponse>({
|
||||
queryKey: ['slots', cacheUrl],
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -76,3 +76,69 @@ export async function incrementCacheVersion(): Promise<number> {
|
|||
return nextValue
|
||||
})
|
||||
}
|
||||
|
||||
const AVAILABILITY_VERSION_KEY = CONST_KEYS.availabilityVersionNum
|
||||
|
||||
export async function getAvailabilityVersionNum(): Promise<number> {
|
||||
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<number> {
|
||||
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<number> {
|
||||
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<number> {
|
||||
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
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<typeof productInfo>
|
||||
type SkuRow = InferSelectModel<typeof productSkus>
|
||||
type MarketStatsRow = InferSelectModel<typeof productMarketStats>
|
||||
type SkuFeatureRow = InferSelectModel<typeof skuFeatures>
|
||||
|
||||
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<typeof units>
|
||||
type StoreRow = InferSelectModel<typeof storeInfo>
|
||||
type SpecialDealRow = InferSelectModel<typeof specialDeals>
|
||||
|
|
@ -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<AdminProductWithRelations[]> {
|
||||
type ProductWithRelationsRow = ProductRow & {
|
||||
store: StoreRow | null
|
||||
skus: Array<SkuRow & { features: SkuFeatureRow[]; comboItems: any[] }>
|
||||
skus: Array<SkuRow & { features: SkuFeatureRow[]; comboItems: any[]; marketStats: MarketStatsRow | null }>
|
||||
}
|
||||
const products = await db.query.productInfo.findMany({
|
||||
orderBy: productInfo.name,
|
||||
|
|
@ -143,9 +183,10 @@ export async function getAllProducts(): Promise<AdminProductWithRelations[]> {
|
|||
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<AdminProductWithRelations[]> {
|
|||
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<AdminProductWithDetail
|
|||
skus: {
|
||||
with: {
|
||||
features: true,
|
||||
marketStats: true,
|
||||
comboItems: {
|
||||
with: {
|
||||
sku: { with: { product: true, features: true } },
|
||||
sku: { with: { product: true, features: true, marketStats: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -212,12 +254,12 @@ export async function getProductById(id: number): Promise<AdminProductWithDetail
|
|||
const comboItems = (sku.comboItems || []).map((ci: any) => ({
|
||||
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<AdminPro
|
|||
skus.map((sku) => ({
|
||||
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<AdminPro
|
|||
|
||||
const createdSkus = await db.query.productSkus.findMany({
|
||||
where: eq(productSkus.productId, product.id),
|
||||
with: { features: true },
|
||||
with: { features: true, marketStats: true },
|
||||
})
|
||||
|
||||
return {
|
||||
...mapProduct(product),
|
||||
store: null,
|
||||
skus: createdSkus.map((s) => 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<AdminProduc
|
|||
const comboIds = Array.from(new Set(comboMemberships.map((c) => 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<AdminProduc
|
|||
await db.update(productSkus)
|
||||
.set({
|
||||
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,
|
||||
})
|
||||
.where(eq(productSkus.id, sku.id))
|
||||
|
||||
const existingMarketStats = await db.query.productMarketStats.findFirst({
|
||||
where: eq(productMarketStats.skuId, sku.id),
|
||||
columns: { id: true },
|
||||
})
|
||||
const marketStatsValues = {
|
||||
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,
|
||||
}
|
||||
if (existingMarketStats) {
|
||||
await db.update(productMarketStats)
|
||||
.set(marketStatsValues)
|
||||
.where(eq(productMarketStats.skuId, sku.id))
|
||||
} else {
|
||||
await db.insert(productMarketStats).values({
|
||||
skuId: sku.id,
|
||||
...marketStatsValues,
|
||||
})
|
||||
}
|
||||
|
||||
await db.delete(skuFeatures).where(eq(skuFeatures.skuId, sku.id))
|
||||
await db.insert(skuFeatures).values(
|
||||
sku.features.map((f: any) => ({
|
||||
|
|
@ -421,16 +487,21 @@ export async function updateProduct(id: number, input: any): Promise<AdminProduc
|
|||
const [newSku] = await db.insert(productSkus).values({
|
||||
productId: 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()
|
||||
|
||||
await db.insert(productMarketStats).values({
|
||||
skuId: newSku.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: any) => ({
|
||||
skuId: newSku.id,
|
||||
|
|
@ -447,7 +518,7 @@ export async function updateProduct(id: number, input: any): Promise<AdminProduc
|
|||
with: {
|
||||
store: true,
|
||||
skus: {
|
||||
with: { features: true },
|
||||
with: { features: true, marketStats: true },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
@ -459,7 +530,7 @@ export async function updateProduct(id: number, input: any): Promise<AdminProduc
|
|||
return {
|
||||
...mapProduct(updatedProduct),
|
||||
store: updatedProduct.store ? mapStore(updatedProduct.store) : null,
|
||||
skus: updatedProduct.skus.map((s) => 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
|
||||
|
||||
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(productSkus)
|
||||
.update(productMarketStats)
|
||||
.set(updateData)
|
||||
.where(eq(productSkus.id, productId))
|
||||
.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<AdminSpecialDeal[]> {
|
||||
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),
|
||||
|
|
|
|||
|
|
@ -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<string[] | null>('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] }),
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -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<ConstKey, string> = {
|
|||
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<ConstKey, ConstValueType> = {
|
|||
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<ConstKey, boolean> = {
|
|||
readableOrderId: false,
|
||||
versionNum: true,
|
||||
'cache_version': false,
|
||||
availability_version_num: false,
|
||||
slots_version_num: false,
|
||||
playStoreUrl: true,
|
||||
appStoreUrl: true,
|
||||
popularItems: true,
|
||||
|
|
|
|||
|
|
@ -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<ProductBasicData[]> {
|
||||
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<ProductBasicData[]> {
|
|||
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<AvailabilityCacheData[]> {
|
||||
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<StoreBasicData[]> {
|
||||
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<ProductComboCacheData[]> {
|
||||
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<ProductComboCacheDa
|
|||
skuName: ci.sku?.name ?? null,
|
||||
images: ci.sku?.images,
|
||||
unitNotation: composeUnitNotation(features),
|
||||
price: String(ci.sku?.price ?? '0'),
|
||||
price: ci.sku?.marketStats?.ourPrice ? String(ci.sku.marketStats.ourPrice) : '0',
|
||||
isOffer: ci.sku?.isOffer ?? false,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -317,15 +349,15 @@ export async function getAllSlotsWithProductsForCache(): Promise<SlotWithProduct
|
|||
let skusData: any[] = []
|
||||
if (skuIdsArray.length > 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<SlotWithProduct
|
|||
.filter((p): p is NonNullable<typeof p> => 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<SlotWithProduct
|
|||
skuName: sku.name ?? null,
|
||||
productQuantity: 1,
|
||||
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,
|
||||
unitNotation: composeUnitNotation(features),
|
||||
store: sku.product?.store ? {
|
||||
id: sku.product.store.id,
|
||||
|
|
@ -357,10 +390,10 @@ export async function getAllSlotsWithProductsForCache(): Promise<SlotWithProduct
|
|||
description: sku.product.store.description
|
||||
} : null,
|
||||
images: sku.images,
|
||||
isOutOfStock: sku.isOutOfStock,
|
||||
isOutOfStock: marketStats?.isOutOfStock ?? false,
|
||||
storeId: sku.product?.storeId ?? null,
|
||||
isFlashAvailable: sku.isFlashAvailable,
|
||||
flashPrice: sku.flashPrice ? String(sku.flashPrice) : null,
|
||||
isFlashAvailable: marketStats?.isFlashAvailable ?? false,
|
||||
flashPrice: marketStats?.flashPrice ? String(marketStats.flashPrice) : null,
|
||||
}
|
||||
}),
|
||||
})) as SlotWithProductsData[]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { db } from '../db/db_index'
|
||||
import { deliverySlotInfo, productInfo, productCombos, productSkus, productReviews, productTags, specialDeals, storeInfo, users } from '../db/schema'
|
||||
import { deliverySlotInfo, productInfo, productCombos, productSkus, productMarketStats, productReviews, productTags, specialDeals, storeInfo, users } from '../db/schema'
|
||||
import { and, desc, eq, gt, sql } from 'drizzle-orm'
|
||||
import type { UserProductDetailData, UserProductReview } from '@packages/shared'
|
||||
import { composeSkuName, composeUnitNotation } from '../lib/sku-features'
|
||||
|
|
@ -11,19 +11,24 @@ const getStringArray = (value: unknown): string[] | null => {
|
|||
|
||||
export async function getProductDetailById(skuId: number): Promise<UserProductDetailData | null> {
|
||||
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<UserProductDe
|
|||
const comboItemsData = await db.query.productCombos.findMany({
|
||||
where: eq(productCombos.comboSkuId, skuId),
|
||||
with: {
|
||||
sku: { with: { product: true, features: true } },
|
||||
sku: { with: { product: true, features: true, marketStats: true } },
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -60,7 +65,8 @@ export async function getProductDetailById(skuId: number): Promise<UserProductDe
|
|||
unitNotation: composeUnitNotation(ciFeatures),
|
||||
productName: composeSkuName(ci.sku?.product?.name ?? 'Unknown', ciFeatures),
|
||||
images: getStringArray(ci.sku?.images),
|
||||
price: String(ci.sku?.price ?? '0'),
|
||||
price: ci.sku?.marketStats?.ourPrice ? String(ci.sku.marketStats.ourPrice) : '0',
|
||||
isOffer: ci.sku?.isOffer ?? false,
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -70,11 +76,11 @@ export async function getProductDetailById(skuId: number): Promise<UserProductDe
|
|||
name: composeSkuName(product?.name ?? 'Unknown', features),
|
||||
shortDescription: product?.shortDescription ?? null,
|
||||
longDescription: 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,
|
||||
unitNotation: composeUnitNotation(features),
|
||||
images: getStringArray(sku.images),
|
||||
isOutOfStock: sku.isOutOfStock,
|
||||
isOutOfStock: marketStats?.isOutOfStock ?? false,
|
||||
store: storeData ? {
|
||||
id: storeData.id,
|
||||
name: storeData.name,
|
||||
|
|
@ -82,8 +88,8 @@ export async function getProductDetailById(skuId: number): Promise<UserProductDe
|
|||
} : null,
|
||||
incrementStep: product?.incrementStep ?? 1,
|
||||
productQuantity: 1,
|
||||
isFlashAvailable: sku.isFlashAvailable,
|
||||
flashPrice: sku.flashPrice?.toString() || null,
|
||||
isFlashAvailable: marketStats?.isFlashAvailable ?? false,
|
||||
flashPrice: marketStats?.flashPrice?.toString() || null,
|
||||
deliverySlots: [],
|
||||
specialDeals: specialDealsData.map((deal) => ({
|
||||
quantity: String(deal.quantity ?? '0'),
|
||||
|
|
@ -202,30 +208,32 @@ export async function getAllProductsWithUnits(tagId?: number): Promise<ProductSu
|
|||
}
|
||||
|
||||
const skus = await db.query.productSkus.findMany({
|
||||
where: eq(productSkus.isSuspended, false),
|
||||
with: {
|
||||
product: true,
|
||||
features: true,
|
||||
marketStats: true,
|
||||
},
|
||||
})
|
||||
|
||||
return skus
|
||||
.filter((sku) => {
|
||||
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<ProductSu
|
|||
*/
|
||||
export async function getSuspendedSkuIds(): Promise<number[]> {
|
||||
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<SkuSummary[]> {
|
||||
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
|
||||
marketStats: {
|
||||
ourPrice: string | null
|
||||
marketPrice: string | null
|
||||
images: unknown
|
||||
isOutOfStock: boolean
|
||||
} | null
|
||||
images: unknown
|
||||
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<OffersPageData> {
|
||||
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<OffersPageData> {
|
|||
const offers: OffersPageProductData[] = []
|
||||
|
||||
for (const sku of skus) {
|
||||
if (sku.marketStats?.isSuspended) continue
|
||||
if (sku.product?.productType === 'combo') {
|
||||
combos.push(mapOffersPageProduct(sku))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<UserDeliverySlot[]> {
|
|||
}
|
||||
|
||||
export async function getProductAvailability(): Promise<UserSlotAvailability[]> {
|
||||
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,
|
||||
}))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,13 +21,13 @@ export async function getStoreSummaries(): Promise<UserStoreSummaryData[]> {
|
|||
}).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<number, typeof skus>()
|
||||
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<UserStoreDetailDa
|
|||
|
||||
const skus = productIdArr.length > 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,
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue