This commit is contained in:
shafi54 2026-03-29 12:12:51 +05:30
parent 7432f8dfd5
commit b86fa8a2e0
49 changed files with 26899 additions and 26208 deletions

View file

@ -60,7 +60,12 @@ const StoreForm = forwardRef<StoreFormRef, StoreFormProps>((props, ref) => {
}); });
}, [initialValues, initialSelectedProducts]); }, [initialValues, initialSelectedProducts]);
const staffOptions = staffData?.staff.map(staff => ({ const existingImageUrls = useMemo(
() => (formInitialValues.imageUrl ? [formInitialValues.imageUrl] : []),
[formInitialValues.imageUrl]
)
const staffOptions = staffData?.staff.map((staff: { id: number; name: string }) => ({
label: staff.name, label: staff.name,
value: staff.id, value: staff.id,
})) || []; })) || [];
@ -168,10 +173,15 @@ const StoreForm = forwardRef<StoreFormRef, StoreFormProps>((props, ref) => {
<MyText style={tw`text-sm font-bold text-gray-700 mb-3 uppercase tracking-wider`}>Store Image</MyText> <MyText style={tw`text-sm font-bold text-gray-700 mb-3 uppercase tracking-wider`}>Store Image</MyText>
<ImageUploader <ImageUploader
images={displayImages} images={displayImages}
existingImageUrls={formInitialValues.imageUrl ? [formInitialValues.imageUrl] : []} existingImageUrls={existingImageUrls}
onAddImage={handleImagePick} onAddImage={handleImagePick}
onRemoveImage={handleRemoveImage} onRemoveImage={handleRemoveImage}
onRemoveExistingImage={() => setFormInitialValues({ ...formInitialValues, imageUrl: undefined })} onRemoveExistingImage={() =>
setFormInitialValues((prev) => ({
...prev,
imageUrl: undefined,
}))
}
allowMultiple={false} allowMultiple={false}
/> />
</View> </View>

View file

@ -53,16 +53,17 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
initialValues, initialValues,
onSubmit, onSubmit,
isLoading, isLoading,
existingImages = [], existingImages:existingImagesRaw,
existingImageKeys = [], existingImageKeys = [],
}, ref) => { }, ref) => {
const { theme } = useTheme(); const { theme } = useTheme();
const [images, setImages] = useState<ImageUploaderNeoItem[]>(existingImages); const [images, setImages] = useState<ImageUploaderNeoItem[]>([]);
const existingImages = existingImagesRaw || []
// Sync images state when existingImages prop changes (e.g., when async query data arrives) // Sync images state when existingImages prop changes (e.g., when async query data arrives)
useEffect(() => { useEffect(() => {
setImages(existingImages); setImages(existingImages);
}, [existingImages]); }, [existingImagesRaw]);
const { data: storesData } = trpc.common.getStoresSummary.useQuery(); const { data: storesData } = trpc.common.getStoresSummary.useQuery();
const storeOptions = storesData?.stores.map(store => ({ const storeOptions = storesData?.stores.map(store => ({

View file

@ -29,15 +29,18 @@ interface TagFormProps {
const TagForm = forwardRef<any, TagFormProps>(({ const TagForm = forwardRef<any, TagFormProps>(({
mode, mode,
initialValues, initialValues,
existingImageUrl = '', existingImageUrl: existingImageUrlRaw,
onSubmit, onSubmit,
isLoading, isLoading,
stores = [], stores: storesRaw,
}, ref) => { }, ref) => {
const [images, setImages] = useState<ImageUploaderNeoItem[]>([]) const [images, setImages] = useState<ImageUploaderNeoItem[]>([])
const [removedExisting, setRemovedExisting] = useState(false) const [removedExisting, setRemovedExisting] = useState(false)
const [isDashboardTagChecked, setIsDashboardTagChecked] = useState<boolean>(Boolean(initialValues.isDashboardTag)); const [isDashboardTagChecked, setIsDashboardTagChecked] = useState<boolean>(Boolean(initialValues.isDashboardTag));
const existingImageUrl = existingImageUrlRaw || ''
const stores = storesRaw || []
// Update checkbox when initial values change // Update checkbox when initial values change
useEffect(() => { useEffect(() => {
setIsDashboardTagChecked(Boolean(initialValues.isDashboardTag)); setIsDashboardTagChecked(Boolean(initialValues.isDashboardTag));
@ -47,7 +50,7 @@ const TagForm = forwardRef<any, TagFormProps>(({
setImages([]) setImages([])
} }
setRemovedExisting(false) setRemovedExisting(false)
}, [existingImageUrl, initialValues.isDashboardTag]); }, [existingImageUrlRaw, initialValues.isDashboardTag]);
const validationSchema = Yup.object().shape({ const validationSchema = Yup.object().shape({

View file

@ -3,12 +3,12 @@
// export dynamically through wrangler, or we can potentially let users directly // export dynamically through wrangler, or we can potentially let users directly
// add them as a sort of "plugin" system. // add them as a sort of "plugin" system.
import ENTRY, { __INTERNAL_WRANGLER_MIDDLEWARE__ } from "/Users/mohammedshafiuddin/WebDev/freshyo/apps/backend/.wrangler/tmp/bundle-aek2Ls/middleware-insertion-facade.js"; import ENTRY, { __INTERNAL_WRANGLER_MIDDLEWARE__ } from "/Users/mohammedshafiuddin/WebDev/freshyo/apps/backend/.wrangler/tmp/bundle-MabhKO/middleware-insertion-facade.js";
import { __facade_invoke__, __facade_register__, Dispatcher } from "/Users/mohammedshafiuddin/WebDev/freshyo/node_modules/wrangler/templates/middleware/common.ts"; import { __facade_invoke__, __facade_register__, Dispatcher } from "/Users/mohammedshafiuddin/WebDev/freshyo/node_modules/wrangler/templates/middleware/common.ts";
import type { WorkerEntrypointConstructor } from "/Users/mohammedshafiuddin/WebDev/freshyo/apps/backend/.wrangler/tmp/bundle-aek2Ls/middleware-insertion-facade.js"; import type { WorkerEntrypointConstructor } from "/Users/mohammedshafiuddin/WebDev/freshyo/apps/backend/.wrangler/tmp/bundle-MabhKO/middleware-insertion-facade.js";
// Preserve all the exports from the worker // Preserve all the exports from the worker
export * from "/Users/mohammedshafiuddin/WebDev/freshyo/apps/backend/.wrangler/tmp/bundle-aek2Ls/middleware-insertion-facade.js"; export * from "/Users/mohammedshafiuddin/WebDev/freshyo/apps/backend/.wrangler/tmp/bundle-MabhKO/middleware-insertion-facade.js";
class __Facade_ScheduledController__ implements ScheduledController { class __Facade_ScheduledController__ implements ScheduledController {
readonly #noRetry: ScheduledController["noRetry"]; readonly #noRetry: ScheduledController["noRetry"];

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,43 @@
import dayjs from 'dayjs'
import { initializeAllStores } from '@/src/stores/store-initializer'
const LAST_TRIGGER_KEY = 'lastTrigger'
const ALARM_DELAY_MINUTES = 0.5
// const ALARM_DELAY_MINUTES = 3
export class CacheCreator {
private state: any
constructor(state: any) {
this.state = state
}
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url)
if (request.method === 'POST' && url.pathname === '/schedule') {
const now = Date.now()
await this.state.storage.put(LAST_TRIGGER_KEY, now)
const alarmAt = dayjs(now).add(ALARM_DELAY_MINUTES, 'minute').valueOf()
await this.state.storage.setAlarm(alarmAt)
return new Response('OK')
}
if (request.method === 'POST' && url.pathname === '/clear') {
await this.state.storage.deleteAll()
return new Response('OK')
}
return new Response('CacheCreator ready', { status: 200 })
}
async alarm(): Promise<void> {
const lastTrigger = await this.state.storage.get(LAST_TRIGGER_KEY)
if (!lastTrigger) {
return
}
const threshold = dayjs().subtract(ALARM_DELAY_MINUTES, 'minute')
if (dayjs(lastTrigger).isBefore(threshold)) {
await initializeAllStores()
}
}
}

View file

@ -1,8 +1,8 @@
import axiosParent from "axios"; import axiosParent from "axios";
import { phonePeBaseUrl } from "@/src/lib/env-exporter" import { getPhonePeBaseUrl } from "@/src/lib/env-exporter"
export const phonepeAxios = axiosParent.create({ export const phonepeAxios = axiosParent.create({
baseURL: phonePeBaseUrl, baseURL: getPhonePeBaseUrl(),
timeout: 40000, timeout: 40000,
headers: { headers: {
"Content-Type": "application/x-www-form-urlencoded", "Content-Type": "application/x-www-form-urlencoded",

View file

@ -1,3 +1,4 @@
import { Buffer } from 'buffer'
import axios from 'axios' import axios from 'axios'
import { scaffoldProducts } from '@/src/trpc/apis/common-apis/common' import { scaffoldProducts } from '@/src/trpc/apis/common-apis/common'
import { scaffoldEssentialConsts } from '@/src/trpc/apis/common-apis/common-trpc-index' import { scaffoldEssentialConsts } from '@/src/trpc/apis/common-apis/common-trpc-index'
@ -5,202 +6,20 @@ import { scaffoldStores } from '@/src/trpc/apis/user-apis/apis/stores'
import { scaffoldSlotsWithProducts } from '@/src/trpc/apis/user-apis/apis/slots' import { scaffoldSlotsWithProducts } from '@/src/trpc/apis/user-apis/apis/slots'
import { scaffoldBanners } from '@/src/trpc/apis/user-apis/apis/banners' import { scaffoldBanners } from '@/src/trpc/apis/user-apis/apis/banners'
import { scaffoldStoreWithProducts } from '@/src/trpc/apis/user-apis/apis/stores' import { scaffoldStoreWithProducts } from '@/src/trpc/apis/user-apis/apis/stores'
import { getStoresSummary } from '@/src/dbService' import { getStoresSummary, incrementCacheVersion } from '@/src/dbService'
import { imageUploadS3 } from '@/src/lib/s3-client' import { imageUploadS3 } from '@/src/lib/s3-client'
import { apiCacheKey, cloudflareApiToken, cloudflareZoneId, assetsDomain } from '@/src/lib/env-exporter' import { getApiCacheKey, getCloudflareApiToken, getCloudflareZoneId, getAssetsDomain } from '@/src/lib/env-exporter'
import { CACHE_FILENAMES } from '@packages/shared' import { CACHE_FILENAMES } from '@packages/shared'
import { retryWithExponentialBackoff } from '@/src/lib/retry' import { retryWithExponentialBackoff } from '@/src/lib/retry'
function constructCacheUrl(path: string): string { const buildCachePath = (path: string, version: number) => `v-${version}/${path}`
return `${assetsDomain}${apiCacheKey}/${path}`
}
export async function createProductsFile(): Promise<string> { function constructCacheUrl(path: string, version: number): string {
// Get products data from the API method return `${getAssetsDomain()}${getApiCacheKey()}/${buildCachePath(path, version)}`
const productsData = await scaffoldProducts()
// Convert to JSON string with pretty formatting
const jsonContent = JSON.stringify(productsData, null, 2)
// Convert to Buffer for S3 upload
const buffer = Buffer.from(jsonContent, 'utf-8')
// Upload to S3 at the specified path using apiCacheKey
const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.products}`)
// Purge cache with retry
const url = constructCacheUrl(CACHE_FILENAMES.products)
try {
await retryWithExponentialBackoff(() => clearUrlCache([url]))
console.log(`Cache purged for ${url}`)
} catch (error) {
console.error(`Failed to purge cache for ${url} after 3 retries:`, error)
}
return s3Key
}
export async function createEssentialConstsFile(): Promise<string> {
// Get essential consts data from the API method
const essentialConstsData = await scaffoldEssentialConsts()
// Convert to JSON string with pretty formatting
const jsonContent = JSON.stringify(essentialConstsData, null, 2)
// Convert to Buffer for S3 upload
const buffer = Buffer.from(jsonContent, 'utf-8')
// Upload to S3 at the specified path using apiCacheKey
const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.essentialConsts}`)
// Purge cache with retry
const url = constructCacheUrl(CACHE_FILENAMES.essentialConsts)
try {
await retryWithExponentialBackoff(() => clearUrlCache([url]))
console.log(`Cache purged for ${url}`)
} catch (error) {
console.error(`Failed to purge cache for ${url} after 3 retries:`, error)
}
return s3Key
}
export async function createStoresFile(): Promise<string> {
// Get stores data from the API method
const storesData = await scaffoldStores()
// Convert to JSON string with pretty formatting
const jsonContent = JSON.stringify(storesData, null, 2)
// Convert to Buffer for S3 upload
const buffer = Buffer.from(jsonContent, 'utf-8')
// Upload to S3 at the specified path using apiCacheKey
const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.stores}`)
// Purge cache with retry
const url = constructCacheUrl(CACHE_FILENAMES.stores)
try {
await retryWithExponentialBackoff(() => clearUrlCache([url]))
console.log(`Cache purged for ${url}`)
} catch (error) {
console.error(`Failed to purge cache for ${url} after 3 retries:`, error)
}
return s3Key
}
export async function createSlotsFile(): Promise<string> {
// Get slots data from the API method
const slotsData = await scaffoldSlotsWithProducts()
// Convert to JSON string with pretty formatting
const jsonContent = JSON.stringify(slotsData, null, 2)
// Convert to Buffer for S3 upload
const buffer = Buffer.from(jsonContent, 'utf-8')
// Upload to S3 at the specified path using apiCacheKey
const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.slots}`)
// Purge cache with retry
const url = constructCacheUrl(CACHE_FILENAMES.slots)
try {
await retryWithExponentialBackoff(() => clearUrlCache([url]))
console.log(`Cache purged for ${url}`)
} catch (error) {
console.error(`Failed to purge cache for ${url} after 3 retries:`, error)
}
return s3Key
}
export async function createBannersFile(): Promise<string> {
// Get banners data from the API method
const bannersData = await scaffoldBanners()
// Convert to JSON string with pretty formatting
const jsonContent = JSON.stringify(bannersData, null, 2)
// Convert to Buffer for S3 upload
const buffer = Buffer.from(jsonContent, 'utf-8')
// Upload to S3 at the specified path using apiCacheKey
const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.banners}`)
// Purge cache with retry
const url = constructCacheUrl(CACHE_FILENAMES.banners)
try {
await retryWithExponentialBackoff(() => clearUrlCache([url]))
console.log(`Cache purged for ${url}`)
} catch (error) {
console.error(`Failed to purge cache for ${url} after 3 retries:`, error)
}
return s3Key
}
export async function createStoreFile(storeId: number): Promise<string> {
// Get store data from the API method
const storeData = await scaffoldStoreWithProducts(storeId)
// Convert to JSON string with pretty formatting
const jsonContent = JSON.stringify(storeData, null, 2)
// Convert to Buffer for S3 upload
const buffer = Buffer.from(jsonContent, 'utf-8')
// Upload to S3 at the specified path using apiCacheKey
const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/stores/${storeId}.json`)
// Purge cache with retry
const url = constructCacheUrl(`stores/${storeId}.json`)
try {
await retryWithExponentialBackoff(() => clearUrlCache([url]))
console.log(`Cache purged for ${url}`)
} catch (error) {
console.error(`Failed to purge cache for ${url} after 3 retries:`, error)
}
return s3Key
}
export async function createAllStoresFiles(): Promise<string[]> {
/*
// Old implementation - direct DB queries:
import { db } from '@/src/db/db_index'
import { storeInfo } from '@/src/db/schema'
const stores = await db.select({ id: storeInfo.id }).from(storeInfo)
*/
// Fetch all store IDs from database using helper
const stores = await getStoresSummary()
// Create cache files for all stores and collect URLs
const results: string[] = []
const urls: string[] = []
for (const store of stores) {
const s3Key = await createStoreFile(store.id)
results.push(s3Key)
urls.push(constructCacheUrl(`stores/${store.id}.json`))
}
console.log(`Created ${results.length} store cache files`)
// Purge all store caches in one batch with retry
try {
await retryWithExponentialBackoff(() => clearUrlCache(urls))
console.log(`Cache purged for ${urls.length} store files`)
} catch (error) {
console.error(`Failed to purge cache for store files after 3 retries. URLs: ${urls.join(', ')}`, error)
}
return results
} }
export interface CreateAllCacheFilesResult { export interface CreateAllCacheFilesResult {
cacheVersion: number
products: string products: string
essentialConsts: string essentialConsts: string
stores: string stores: string
@ -212,6 +31,8 @@ export interface CreateAllCacheFilesResult {
export async function createAllCacheFiles(): Promise<CreateAllCacheFilesResult> { export async function createAllCacheFiles(): Promise<CreateAllCacheFilesResult> {
console.log('Starting creation of all cache files...') console.log('Starting creation of all cache files...')
const cacheVersion = await incrementCacheVersion()
// Create all global cache files in parallel // Create all global cache files in parallel
const [ const [
productsKey, productsKey,
@ -221,22 +42,24 @@ export async function createAllCacheFiles(): Promise<CreateAllCacheFilesResult>
bannersKey, bannersKey,
individualStoreKeys, individualStoreKeys,
] = await Promise.all([ ] = await Promise.all([
createProductsFileInternal(), createProductsFileInternal(cacheVersion),
createEssentialConstsFileInternal(), createEssentialConstsFileInternal(cacheVersion),
createStoresFileInternal(), createStoresFileInternal(cacheVersion),
createSlotsFileInternal(), createSlotsFileInternal(cacheVersion),
createBannersFileInternal(), createBannersFileInternal(cacheVersion),
createAllStoresFilesInternal(), createAllStoresFilesInternal(cacheVersion),
]) ])
const stores = await getStoresSummary()
// Collect all URLs for batch cache purge // Collect all URLs for batch cache purge
const urls = [ const urls = [
constructCacheUrl(CACHE_FILENAMES.products), constructCacheUrl(CACHE_FILENAMES.products, cacheVersion),
constructCacheUrl(CACHE_FILENAMES.essentialConsts), constructCacheUrl(CACHE_FILENAMES.essentialConsts, cacheVersion),
constructCacheUrl(CACHE_FILENAMES.stores), constructCacheUrl(CACHE_FILENAMES.stores, cacheVersion),
constructCacheUrl(CACHE_FILENAMES.slots), constructCacheUrl(CACHE_FILENAMES.slots, cacheVersion),
constructCacheUrl(CACHE_FILENAMES.banners), constructCacheUrl(CACHE_FILENAMES.banners, cacheVersion),
...individualStoreKeys.map((_, index) => constructCacheUrl(`stores/${index + 1}.json`)), ...stores.map((store) => constructCacheUrl(`stores/${store.id}.json`, cacheVersion)),
] ]
// Purge all caches in one batch with retry // Purge all caches in one batch with retry
@ -250,6 +73,7 @@ export async function createAllCacheFiles(): Promise<CreateAllCacheFilesResult>
console.log('All cache files created successfully') console.log('All cache files created successfully')
return { return {
cacheVersion,
products: productsKey, products: productsKey,
essentialConsts: essentialConstsKey, essentialConsts: essentialConstsKey,
stores: storesKey, stores: storesKey,
@ -260,42 +84,62 @@ export async function createAllCacheFiles(): Promise<CreateAllCacheFilesResult>
} }
// Internal versions that skip cache purging (for batch operations) // Internal versions that skip cache purging (for batch operations)
async function createProductsFileInternal(): Promise<string> { async function createProductsFileInternal(version: number): Promise<string> {
const productsData = await scaffoldProducts() const productsData = await scaffoldProducts()
const jsonContent = JSON.stringify(productsData, null, 2) const jsonContent = JSON.stringify(productsData, null, 2)
const buffer = Buffer.from(jsonContent, 'utf-8') const buffer = Buffer.from(jsonContent, 'utf-8')
return await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.products}`) return await imageUploadS3(
buffer,
'application/json',
`${getApiCacheKey()}/${buildCachePath(CACHE_FILENAMES.products, version)}`
)
} }
async function createEssentialConstsFileInternal(): Promise<string> { async function createEssentialConstsFileInternal(version: number): Promise<string> {
const essentialConstsData = await scaffoldEssentialConsts() const essentialConstsData = await scaffoldEssentialConsts()
const jsonContent = JSON.stringify(essentialConstsData, null, 2) const jsonContent = JSON.stringify(essentialConstsData, null, 2)
const buffer = Buffer.from(jsonContent, 'utf-8') const buffer = Buffer.from(jsonContent, 'utf-8')
return await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.essentialConsts}`) return await imageUploadS3(
buffer,
'application/json',
`${getApiCacheKey()}/${buildCachePath(CACHE_FILENAMES.essentialConsts, version)}`
)
} }
async function createStoresFileInternal(): Promise<string> { async function createStoresFileInternal(version: number): Promise<string> {
const storesData = await scaffoldStores() const storesData = await scaffoldStores()
const jsonContent = JSON.stringify(storesData, null, 2) const jsonContent = JSON.stringify(storesData, null, 2)
const buffer = Buffer.from(jsonContent, 'utf-8') const buffer = Buffer.from(jsonContent, 'utf-8')
return await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.stores}`) return await imageUploadS3(
buffer,
'application/json',
`${getApiCacheKey()}/${buildCachePath(CACHE_FILENAMES.stores, version)}`
)
} }
async function createSlotsFileInternal(): Promise<string> { async function createSlotsFileInternal(version: number): Promise<string> {
const slotsData = await scaffoldSlotsWithProducts() const slotsData = await scaffoldSlotsWithProducts()
const jsonContent = JSON.stringify(slotsData, null, 2) const jsonContent = JSON.stringify(slotsData, null, 2)
const buffer = Buffer.from(jsonContent, 'utf-8') const buffer = Buffer.from(jsonContent, 'utf-8')
return await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.slots}`) return await imageUploadS3(
buffer,
'application/json',
`${getApiCacheKey()}/${buildCachePath(CACHE_FILENAMES.slots, version)}`
)
} }
async function createBannersFileInternal(): Promise<string> { async function createBannersFileInternal(version: number): Promise<string> {
const bannersData = await scaffoldBanners() const bannersData = await scaffoldBanners()
const jsonContent = JSON.stringify(bannersData, null, 2) const jsonContent = JSON.stringify(bannersData, null, 2)
const buffer = Buffer.from(jsonContent, 'utf-8') const buffer = Buffer.from(jsonContent, 'utf-8')
return await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.banners}`) return await imageUploadS3(
buffer,
'application/json',
`${getApiCacheKey()}/${buildCachePath(CACHE_FILENAMES.banners, version)}`
)
} }
async function createAllStoresFilesInternal(): Promise<string[]> { async function createAllStoresFilesInternal(version: number): Promise<string[]> {
/* /*
// Old implementation - direct DB queries: // Old implementation - direct DB queries:
import { db } from '@/src/db/db_index' import { db } from '@/src/db/db_index'
@ -311,7 +155,11 @@ async function createAllStoresFilesInternal(): Promise<string[]> {
const storeData = await scaffoldStoreWithProducts(store.id) const storeData = await scaffoldStoreWithProducts(store.id)
const jsonContent = JSON.stringify(storeData, null, 2) const jsonContent = JSON.stringify(storeData, null, 2)
const buffer = Buffer.from(jsonContent, 'utf-8') const buffer = Buffer.from(jsonContent, 'utf-8')
const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/stores/${store.id}.json`) const s3Key = await imageUploadS3(
buffer,
'application/json',
`${getApiCacheKey()}/${buildCachePath(`stores/${store.id}.json`, version)}`
)
results.push(s3Key) results.push(s3Key)
} }
@ -320,6 +168,8 @@ async function createAllStoresFilesInternal(): Promise<string[]> {
} }
export async function clearUrlCache(urls: string[]): Promise<{ success: boolean; errors?: string[] }> { export async function clearUrlCache(urls: string[]): Promise<{ success: boolean; errors?: string[] }> {
const cloudflareApiToken = getCloudflareApiToken()
const cloudflareZoneId = getCloudflareZoneId()
if (!cloudflareApiToken || !cloudflareZoneId) { if (!cloudflareApiToken || !cloudflareZoneId) {
console.warn('Cloudflare credentials not configured, skipping cache clear') console.warn('Cloudflare credentials not configured, skipping cache clear')
return { success: false, errors: ['Cloudflare credentials not configured'] } return { success: false, errors: ['Cloudflare credentials not configured'] }
@ -356,6 +206,8 @@ export async function clearUrlCache(urls: string[]): Promise<{ success: boolean;
} }
export async function clearAllCache(): Promise<{ success: boolean; errors?: string[] }> { export async function clearAllCache(): Promise<{ success: boolean; errors?: string[] }> {
const cloudflareApiToken = getCloudflareApiToken()
const cloudflareZoneId = getCloudflareZoneId()
if (!cloudflareApiToken || !cloudflareZoneId) { if (!cloudflareApiToken || !cloudflareZoneId) {
console.warn('Cloudflare credentials not configured, skipping cache clear') console.warn('Cloudflare credentials not configured, skipping cache clear')
return { success: false, errors: ['Cloudflare credentials not configured'] } return { success: false, errors: ['Cloudflare credentials not configured'] }

View file

@ -1,5 +1,5 @@
import { deleteImageUtil, getOriginalUrlFromSignedUrl } from "@/src/lib/s3-client" import { deleteImageUtil, getOriginalUrlFromSignedUrl } from "@/src/lib/s3-client"
import { s3Url } from "@/src/lib/env-exporter" import { getS3Url } from "@/src/lib/env-exporter"
function extractS3Key(url: string): string | null { function extractS3Key(url: string): string | null {
try { try {
@ -8,11 +8,11 @@ function extractS3Key(url: string): string | null {
// Find the index of '.com/' in the URL // Find the index of '.com/' in the URL
// const comIndex = originalUrl.indexOf(".com/"); // const comIndex = originalUrl.indexOf(".com/");
const baseUrlIndex = originalUrl.indexOf(s3Url); const baseUrlIndex = originalUrl.indexOf(getS3Url());
// If '.com/' is found, return everything after it // If '.com/' is found, return everything after it
if (baseUrlIndex !== -1) { if (baseUrlIndex !== -1) {
return originalUrl.substring(baseUrlIndex + s3Url.length); // +5 to skip '.com/' return originalUrl.substring(baseUrlIndex + getS3Url().length); // +5 to skip '.com/'
} }
} catch (error) { } catch (error) {
console.error("Error extracting key from URL:", error); console.error("Error extracting key from URL:", error);

View file

@ -59,11 +59,9 @@
const getRuntimeEnv = () => (globalThis as any).ENV || (globalThis as any).process?.env || {} const getRuntimeEnv = () => (globalThis as any).ENV || (globalThis as any).process?.env || {}
const runtimeEnv = getRuntimeEnv() export const getAppUrl = () => getRuntimeEnv().APP_URL as string
export const appUrl = runtimeEnv.APP_URL as string export const getJwtSecret = () => getRuntimeEnv().JWT_SECRET as string
export const jwtSecret: string = runtimeEnv.JWT_SECRET as string
export const defaultRoleName = 'gen_user'; export const defaultRoleName = 'gen_user';
@ -73,50 +71,50 @@ export const getEncodedJwtSecret = () => {
return new TextEncoder().encode(secret) return new TextEncoder().encode(secret)
} }
export const s3AccessKeyId = runtimeEnv.S3_ACCESS_KEY_ID as string export const getS3AccessKeyId = () => getRuntimeEnv().S3_ACCESS_KEY_ID as string
export const s3SecretAccessKey = runtimeEnv.S3_SECRET_ACCESS_KEY as string export const getS3SecretAccessKey = () => getRuntimeEnv().S3_SECRET_ACCESS_KEY as string
export const s3BucketName = runtimeEnv.S3_BUCKET_NAME as string export const getS3BucketName = () => getRuntimeEnv().S3_BUCKET_NAME as string
export const s3Region = runtimeEnv.S3_REGION as string export const getS3Region = () => getRuntimeEnv().S3_REGION as string
export const assetsDomain = runtimeEnv.ASSETS_DOMAIN as string export const getAssetsDomain = () => getRuntimeEnv().ASSETS_DOMAIN as string
export const apiCacheKey = runtimeEnv.API_CACHE_KEY as string export const getApiCacheKey = () => getRuntimeEnv().API_CACHE_KEY as string
export const cloudflareApiToken = runtimeEnv.CLOUDFLARE_API_TOKEN as string export const getCloudflareApiToken = () => getRuntimeEnv().CLOUDFLARE_API_TOKEN as string
export const cloudflareZoneId = runtimeEnv.CLOUDFLARE_ZONE_ID as string export const getCloudflareZoneId = () => getRuntimeEnv().CLOUDFLARE_ZONE_ID as string
export const s3Url = runtimeEnv.S3_URL as string export const getS3Url = () => getRuntimeEnv().S3_URL as string
export const redisUrl = runtimeEnv.REDIS_URL as string export const getRedisUrl = () => getRuntimeEnv().REDIS_URL as string
export const expoAccessToken = runtimeEnv.EXPO_ACCESS_TOKEN as string export const getExpoAccessToken = () => getRuntimeEnv().EXPO_ACCESS_TOKEN as string
export const phonePeBaseUrl = runtimeEnv.PHONE_PE_BASE_URL as string export const getPhonePeBaseUrl = () => getRuntimeEnv().PHONE_PE_BASE_URL as string
export const phonePeClientId = runtimeEnv.PHONE_PE_CLIENT_ID as string export const getPhonePeClientId = () => getRuntimeEnv().PHONE_PE_CLIENT_ID as string
export const phonePeClientVersion = Number(runtimeEnv.PHONE_PE_CLIENT_VERSION as string) export const getPhonePeClientVersion = () => Number(getRuntimeEnv().PHONE_PE_CLIENT_VERSION as string)
export const phonePeClientSecret = runtimeEnv.PHONE_PE_CLIENT_SECRET as string export const getPhonePeClientSecret = () => getRuntimeEnv().PHONE_PE_CLIENT_SECRET as string
export const phonePeMerchantId = runtimeEnv.PHONE_PE_MERCHANT_ID as string export const getPhonePeMerchantId = () => getRuntimeEnv().PHONE_PE_MERCHANT_ID as string
export const razorpayId = runtimeEnv.RAZORPAY_KEY as string export const getRazorpayId = () => getRuntimeEnv().RAZORPAY_KEY as string
export const razorpaySecret = runtimeEnv.RAZORPAY_SECRET as string export const getRazorpaySecret = () => getRuntimeEnv().RAZORPAY_SECRET as string
export const otpSenderAuthToken = runtimeEnv.OTP_SENDER_AUTH_TOKEN as string export const getOtpSenderAuthToken = () => getRuntimeEnv().OTP_SENDER_AUTH_TOKEN as string
export const minOrderValue = Number(runtimeEnv.MIN_ORDER_VALUE as string) export const getMinOrderValue = () => Number(getRuntimeEnv().MIN_ORDER_VALUE as string)
export const deliveryCharge = Number(runtimeEnv.DELIVERY_CHARGE as string) export const getDeliveryCharge = () => Number(getRuntimeEnv().DELIVERY_CHARGE as string)
export const telegramBotToken = runtimeEnv.TELEGRAM_BOT_TOKEN as string export const getTelegramBotToken = () => getRuntimeEnv().TELEGRAM_BOT_TOKEN as string
export const telegramChatIds = (runtimeEnv.TELEGRAM_CHAT_IDS as string)?.split(',').map(id => id.trim()) || [] export const getTelegramChatIds = () => (getRuntimeEnv().TELEGRAM_CHAT_IDS as string)?.split(',').map(id => id.trim()) || []
export const isDevMode = (runtimeEnv.ENV_MODE as string) === 'dev' export const getIsDevMode = () => (getRuntimeEnv().ENV_MODE as string) === 'dev'

View file

@ -1,9 +1,8 @@
import { Expo } from "expo-server-sdk"; import { Expo } from "expo-server-sdk";
import { title } from "process"; import { getExpoAccessToken } from "@/src/lib/env-exporter"
import { expoAccessToken } from "@/src/lib/env-exporter"
const expo = new Expo({ const expo = new Expo({
accessToken: expoAccessToken, accessToken: getExpoAccessToken(),
useFcmV1: true, useFcmV1: true,
}); });

View file

@ -1,8 +1,8 @@
// import { Queue, Worker } from 'bullmq'; // import { Queue, Worker } from 'bullmq';
import { Expo } from 'expo-server-sdk'; import { Expo } from 'expo-server-sdk';
import { redisUrl } from '@/src/lib/env-exporter'
// import { db } from '@/src/db/db_index' // import { db } from '@/src/db/db_index'
import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client' import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client'
import { queueDataPusher } from '@/src/lib/queue-data-pusher'
import { import {
NOTIFS_QUEUE, NOTIFS_QUEUE,
ORDER_PLACED_MESSAGE, ORDER_PLACED_MESSAGE,
@ -44,7 +44,7 @@ export const notificationWorker:any = {};
// concurrency: 5, // concurrency: 5,
// }); // });
async function sendAdminNotification(data: { export async function sendAdminNotification(data: {
token: string; token: string;
title: string; title: string;
body: string; body: string;
@ -97,7 +97,7 @@ async function sendAdminNotification(data: {
export async function scheduleNotification(userId: number, payload: any, options?: { delay?: number; priority?: number }) { export async function scheduleNotification(userId: number, payload: any, options?: { delay?: number; priority?: number }) {
const jobData = { userId, ...payload }; const jobData = { userId, ...payload };
await notificationQueue.add('send-notification', jobData, options); await queueDataPusher.pushNotifQueue({ name: 'send-notification', jobData, options })
} }
// Utility methods for specific notification events // Utility methods for specific notification events

View file

@ -1,5 +1,5 @@
import { ApiError } from '@/src/lib/api-error' import { ApiError } from '@/src/lib/api-error'
import { otpSenderAuthToken } from '@/src/lib/env-exporter' import { getOtpSenderAuthToken } from '@/src/lib/env-exporter'
const otpStore = new Map<string, string>(); const otpStore = new Map<string, string>();
@ -20,7 +20,7 @@ export const sendOtp = async (phone: string) => {
const reqUrl = `https://cpaas.messagecentral.com/verification/v3/send?countryCode=91&flowType=SMS&mobileNumber=${phone}&timeout=300`; const reqUrl = `https://cpaas.messagecentral.com/verification/v3/send?countryCode=91&flowType=SMS&mobileNumber=${phone}&timeout=300`;
const resp = await fetch(reqUrl, { const resp = await fetch(reqUrl, {
headers: { headers: {
authToken: otpSenderAuthToken, authToken: getOtpSenderAuthToken(),
}, },
method: "POST", method: "POST",
}); });
@ -42,7 +42,7 @@ export async function verifyOtpUtil(mobile: string, otp: string, verifId: string
const resp = await fetch(reqUrl, { const resp = await fetch(reqUrl, {
method: "GET", method: "GET",
headers: { headers: {
authToken: otpSenderAuthToken, authToken: getOtpSenderAuthToken(),
}, },
}); });

View file

@ -1,5 +1,4 @@
// import Razorpay from "razorpay"; // import Razorpay from "razorpay";
import { razorpayId, razorpaySecret } from "@/src/lib/env-exporter"
export class RazorpayPaymentService { export class RazorpayPaymentService {
// private static instance = new Razorpay({ // private static instance = new Razorpay({

View file

@ -2,11 +2,8 @@ import {
getOrdersByIdsWithFullData, getOrdersByIdsWithFullData,
getOrderByIdWithFullData, getOrderByIdWithFullData,
} from '@/src/dbService' } from '@/src/dbService'
import redisClient from '@/src/lib/redis-client'
import { sendTelegramMessage } from '@/src/lib/telegram-service' import { sendTelegramMessage } from '@/src/lib/telegram-service'
import { queueDataPusher } from '@/src/lib/queue-data-pusher'
const ORDER_CHANNEL = 'orders:placed';
const CANCELLED_CHANNEL = 'orders:cancelled';
interface OrderIdMessage { interface OrderIdMessage {
orderIds: number[]; orderIds: number[];
@ -29,6 +26,7 @@ const formatDateTime = (dateStr: string | null | undefined): string => {
}; };
const formatOrderMessageWithFullData = (ordersData: any[]): string => { const formatOrderMessageWithFullData = (ordersData: any[]): string => {
console.log('formatting the msg')
let message = '🛒 <b>New Order Placed</b>\n\n'; let message = '🛒 <b>New Order Placed</b>\n\n';
ordersData.forEach((order, index) => { ordersData.forEach((order, index) => {
@ -86,127 +84,55 @@ ${orderData.orderItems?.map((item: any) => ` • ${item.product?.name || 'Unkno
return message; return message;
}; };
export const handleOrderPlaced = async (orderIds: number[], rawMessage?: string): Promise<void> => {
try {
const ordersData = await getOrdersByIdsWithFullData(orderIds)
console.log({ordersData});
const telegramMessage = formatOrderMessageWithFullData(ordersData)
await sendTelegramMessage(telegramMessage)
} catch (error) {
console.error('Failed to process order message:', error)
const fallback = rawMessage ? `⚠️ Error parsing order: ${rawMessage}` : '⚠️ Error parsing order'
await sendTelegramMessage(fallback)
}
}
export const handleOrderCancelled = async (
cancellationData: CancellationMessage,
rawMessage?: string
): Promise<void> => {
try {
console.log('Order cancellation received, sending to Telegram...')
const orderData = await getOrderByIdWithFullData(cancellationData.orderId)
if (!orderData) {
console.error('Order not found for cancellation:', cancellationData.orderId)
await sendTelegramMessage(`⚠️ Order ${cancellationData.orderId} was cancelled but could not be found in database`)
return
}
const refundStatus = orderData.refunds?.[0]?.refundStatus || 'pending'
const telegramMessage = formatCancellationMessage({ ...orderData, refundStatus }, cancellationData)
await sendTelegramMessage(telegramMessage)
} catch (error) {
console.error('Failed to process cancellation message:', error)
const fallback = rawMessage ? `⚠️ Error processing cancellation: ${rawMessage}` : '⚠️ Error processing cancellation'
await sendTelegramMessage(fallback)
}
}
/** /**
* Start the post order handler * Start the post order handler
* Subscribes to the orders:placed channel and sends to Telegram * Subscribes to the orders:placed channel and sends to Telegram
*/ */
export const startOrderHandler = async (): Promise<void> => {
try {
console.log('Starting post order handler...');
await redisClient.subscribe(ORDER_CHANNEL, async (message: string) => {
try {
const { orderIds }: OrderIdMessage = JSON.parse(message);
console.log('New order received, sending to Telegram...');
/*
// Old implementation - direct DB queries:
import { db } from '@/src/db/db_index'
import { orders } from '@/src/db/schema'
import { inArray } from 'drizzle-orm';
const ordersData = await db.query.orders.findMany({
where: inArray(orders.id, orderIds),
with: {
address: true,
orderItems: { with: { product: true } },
slot: true,
},
});
*/
const ordersData = await getOrdersByIdsWithFullData(orderIds);
const telegramMessage = formatOrderMessageWithFullData(ordersData);
await sendTelegramMessage(telegramMessage);
} catch (error) {
console.error('Failed to process order message:', error);
await sendTelegramMessage(`⚠️ Error parsing order: ${message}`);
}
});
console.log('Post order handler started successfully');
} catch (error) {
console.error('Failed to start post order handler:', error);
throw error;
}
};
/**
* Stop the post order handler
*/
export const stopOrderHandler = async (): Promise<void> => {
try {
await redisClient.unsubscribe(ORDER_CHANNEL);
console.log('Post order handler stopped');
} catch (error) {
console.error('Error stopping post order handler:', error);
}
};
export const startCancellationHandler = async (): Promise<void> => {
try {
console.log('Starting cancellation handler...');
await redisClient.subscribe(CANCELLED_CHANNEL, async (message: string) => {
try {
const cancellationData: CancellationMessage = JSON.parse(message);
console.log('Order cancellation received, sending to Telegram...');
/*
// Old implementation - direct DB queries:
import { db } from '@/src/db/db_index'
import { orders } from '@/src/db/schema'
import { eq } from 'drizzle-orm';
const orderData = await db.query.orders.findFirst({
where: eq(orders.id, cancellationData.orderId),
with: {
address: true,
orderItems: { with: { product: true } },
refunds: true,
},
});
*/
const orderData = await getOrderByIdWithFullData(cancellationData.orderId);
if (!orderData) {
console.error('Order not found for cancellation:', cancellationData.orderId);
await sendTelegramMessage(`⚠️ Order ${cancellationData.orderId} was cancelled but could not be found in database`);
return;
}
const refundStatus = orderData.refunds?.[0]?.refundStatus || 'pending';
const telegramMessage = formatCancellationMessage({ ...orderData, refundStatus }, cancellationData);
await sendTelegramMessage(telegramMessage);
} catch (error) {
console.error('Failed to process cancellation message:', error);
await sendTelegramMessage(`⚠️ Error processing cancellation: ${message}`);
}
});
console.log('Cancellation handler started successfully');
} catch (error) {
console.error('Failed to start cancellation handler:', error);
throw error;
}
};
export const stopCancellationHandler = async (): Promise<void> => {
try {
await redisClient.unsubscribe(CANCELLED_CHANNEL);
console.log('Cancellation handler stopped');
} catch (error) {
console.error('Error stopping cancellation handler:', error);
}
};
export const publishOrder = async (orderDetails: OrderIdMessage): Promise<boolean> => { export const publishOrder = async (orderDetails: OrderIdMessage): Promise<boolean> => {
console.log('publishing order')
try { try {
const message = JSON.stringify(orderDetails); await queueDataPusher.pushOrderPlacedQueue({
await redisClient.publish(ORDER_CHANNEL, message); name: 'order-placed',
orderIds: orderDetails.orderIds,
})
return true; return true;
} catch (error) { } catch (error) {
console.error('Failed to publish order:', error); console.error('Failed to publish order:', error);
@ -239,8 +165,11 @@ export const publishCancellation = async (
reason, reason,
cancelledAt: new Date().toISOString(), cancelledAt: new Date().toISOString(),
}; };
await redisClient.publish(CANCELLED_CHANNEL, JSON.stringify(message)); await queueDataPusher.pushOrderCancelledQueue({
console.log('Cancellation published to Redis:', orderId); name: 'order-cancelled',
...message,
})
console.log('Cancellation published to queue:', orderId);
return true; return true;
} catch (error) { } catch (error) {
console.error('Failed to publish cancellation:', error); console.error('Failed to publish cancellation:', error);

View file

@ -0,0 +1,53 @@
import { sendAdminNotification } from '@/src/lib/notif-job'
import { handleOrderCancelled, handleOrderPlaced } from '@/src/lib/post-order-handler'
export const handleNotifQueue = (batch: any) => {
batch.messages.forEach((message: any) => {
const body = message?.body
if (!body) {
console.log('notif_queue message received with empty body')
return
}
if (body.name === 'send-admin-notification' && body.jobData?.token) {
void sendAdminNotification({
token: body.jobData.token,
title: body.jobData.title,
body: body.jobData.body,
imageUrl: body.jobData.imageUrl ?? null,
})
return
}
console.log('notif_queue message received', body)
})
}
export const handleOrderPlacedQueue = async (batch: any) => {
for (const message of batch.messages || []) {
const body = message?.body
if (!body || !Array.isArray(body.orderIds)) {
console.log('order_placed_queue message received', body)
continue
}
await handleOrderPlaced(body.orderIds)
}
}
export const handleOrderCancelledQueue = async (batch: any) => {
for (const message of batch.messages || []) {
const body = message?.body
if (!body || !body.orderId) {
console.log('order_cancelled_queue message received', body)
continue
}
await handleOrderCancelled({
orderId: body.orderId,
cancelledBy: body.cancelledBy,
reason: body.reason,
cancelledAt: body.cancelledAt,
})
}
}

View file

@ -0,0 +1,51 @@
type QueueSender = { send: (message: unknown) => Promise<void> }
export class QueueDataPusher {
private getEnv() {
return (globalThis as {
ENV?: {
NOTIF_QUEUE?: QueueSender
ORDER_PLACED_QUEUE?: QueueSender
ORDER_CANCELLED_QUEUE?: QueueSender
}
}).ENV
}
async pushNotifQueue(message: unknown): Promise<boolean> {
const env = this.getEnv()
if (!env?.NOTIF_QUEUE) {
console.warn('NOTIF_QUEUE binding not available, skipping enqueue')
return false
}
await env.NOTIF_QUEUE.send(message)
return true
}
async pushOrderPlacedQueue(message: { name: 'order-placed'; orderIds: number[] }): Promise<boolean> {
const env = this.getEnv()
if (!env?.ORDER_PLACED_QUEUE) {
console.warn('ORDER_PLACED_QUEUE binding not available, skipping publish')
return false
}
await env.ORDER_PLACED_QUEUE.send(message)
return true
}
async pushOrderCancelledQueue(message: {
name: 'order-cancelled'
orderId: number
cancelledBy: 'user' | 'admin'
reason: string
cancelledAt: string
}): Promise<boolean> {
const env = this.getEnv()
if (!env?.ORDER_CANCELLED_QUEUE) {
console.warn('ORDER_CANCELLED_QUEUE binding not available, skipping publish')
return false
}
await env.ORDER_CANCELLED_QUEUE.send(message)
return true
}
}
export const queueDataPusher = new QueueDataPusher()

View file

@ -1,5 +1,5 @@
// import { createClient, RedisClientType } from 'redis'; // import { createClient, RedisClientType } from 'redis';
import { redisUrl } from '@/src/lib/env-exporter' import { getRedisUrl } from '@/src/lib/env-exporter'
const createClient = (args:any) => {} const createClient = (args:any) => {}
class RedisClient { class RedisClient {
@ -14,7 +14,7 @@ class RedisClient {
constructor() { constructor() {
this.client = createClient({ this.client = createClient({
url: redisUrl, url: getRedisUrl(),
}); });
// this.client.on('error', (err) => { // this.client.on('error', (err) => {

View file

@ -1,23 +1,48 @@
// import { s3A, awsBucketName, awsRegion, awsSecretAccessKey } from "@/src/lib/env-exporter" // import { s3A, awsBucketName, awsRegion, awsSecretAccessKey } from "@/src/lib/env-exporter"
import type { Buffer } from 'buffer'
import { DeleteObjectCommand, DeleteObjectsCommand, PutObjectCommand, S3Client, GetObjectCommand } from "@aws-sdk/client-s3" import { DeleteObjectCommand, DeleteObjectsCommand, PutObjectCommand, S3Client, GetObjectCommand } from "@aws-sdk/client-s3"
import { getSignedUrl } from "@aws-sdk/s3-request-presigner" import { getSignedUrl } from "@aws-sdk/s3-request-presigner"
// import signedUrlCache from "@/src/lib/signed-url-cache" // Disabled for Workers compatibility // import signedUrlCache from "@/src/lib/signed-url-cache" // Disabled for Workers compatibility
import { claimUploadUrlStatus, createUploadUrlStatus } from '@/src/dbService' import { claimUploadUrlStatus, createUploadUrlStatus } from '@/src/dbService'
import { s3AccessKeyId, s3Region, s3Url, s3SecretAccessKey, s3BucketName, assetsDomain } from "@/src/lib/env-exporter" import {
getS3AccessKeyId,
getS3Region,
getS3Url,
getS3SecretAccessKey,
getS3BucketName,
getAssetsDomain,
} from "@/src/lib/env-exporter"
const s3Client = new S3Client({ let s3Client: S3Client | null = null
region: s3Region, let s3ClientKey = ''
endpoint: s3Url,
forcePathStyle: true,
credentials: {
accessKeyId: s3AccessKeyId,
secretAccessKey: s3SecretAccessKey,
},
})
export default s3Client;
export const imageUploadS3 = async(body: Buffer<ArrayBufferLike>, type: string, key:string) => { const getS3Client = () => {
const region = getS3Region()
const endpoint = getS3Url()
const accessKeyId = getS3AccessKeyId()
const secretAccessKey = getS3SecretAccessKey()
const nextKey = `${region}|${endpoint}|${accessKeyId}|${secretAccessKey}`
if (!s3Client || nextKey !== s3ClientKey) {
s3ClientKey = nextKey
s3Client = new S3Client({
region,
endpoint,
forcePathStyle: true,
credentials: {
accessKeyId,
secretAccessKey,
},
})
}
return s3Client
}
export const imageUploadS3 = async(body: Buffer, type: string, key:string) => {
// const key = `${category}/${Date.now()}` // const key = `${category}/${Date.now()}`
const s3BucketName = getS3BucketName()
const s3Client = getS3Client()
const command = new PutObjectCommand({ const command = new PutObjectCommand({
Bucket: s3BucketName, Bucket: s3BucketName,
Key: key, Key: key,
@ -33,7 +58,7 @@ export const imageUploadS3 = async(body: Buffer<ArrayBufferLike>, type: string,
// export async function deleteImageUtil(...keys:string[]):Promise<boolean>; // export async function deleteImageUtil(...keys:string[]):Promise<boolean>;
export async function deleteImageUtil({bucket = s3BucketName, keys}:{bucket?:string, keys: string[]}) { export async function deleteImageUtil({bucket = getS3BucketName(), keys}:{bucket?:string, keys: string[]}) {
if (keys.length === 0) { if (keys.length === 0) {
return true; return true;
@ -48,7 +73,7 @@ export async function deleteImageUtil({bucket = s3BucketName, keys}:{bucket?:str
} }
const deleteCommand = new DeleteObjectsCommand(deleteParams) const deleteCommand = new DeleteObjectsCommand(deleteParams)
await s3Client.send(deleteCommand) await getS3Client().send(deleteCommand)
return true return true
} }
catch (error) { catch (error) {
@ -61,6 +86,7 @@ export async function deleteImageUtil({bucket = s3BucketName, keys}:{bucket?:str
export function scaffoldAssetUrl(input: string | null): string export function scaffoldAssetUrl(input: string | null): string
export function scaffoldAssetUrl(input: (string | null)[]): string[] export function scaffoldAssetUrl(input: (string | null)[]): string[]
export function scaffoldAssetUrl(input: string | null | (string | null)[]): string | string[] { export function scaffoldAssetUrl(input: string | null | (string | null)[]): string | string[] {
const assetsDomain = getAssetsDomain()
if (Array.isArray(input)) { if (Array.isArray(input)) {
return input.map(key => scaffoldAssetUrl(key) as string); return input.map(key => scaffoldAssetUrl(key) as string);
} }
@ -97,12 +123,12 @@ export async function generateSignedUrlFromS3Url(s3UrlRaw: string|null, expiresI
// Create the command to get the object // Create the command to get the object
const command = new GetObjectCommand({ const command = new GetObjectCommand({
Bucket: s3BucketName, Bucket: getS3BucketName(),
Key: s3Url, Key: s3Url,
}); });
// Generate the signed URL // Generate the signed URL
const signedUrl = await getSignedUrl(s3Client, command, { expiresIn }); const signedUrl = await getSignedUrl(getS3Client(), command, { expiresIn });
// Cache disabled for Workers compatibility // Cache disabled for Workers compatibility
// signedUrlCache.set(s3Url, signedUrl, (expiresIn * 1000) - 60000); // signedUrlCache.set(s3Url, signedUrl, (expiresIn * 1000) - 60000);
@ -157,12 +183,12 @@ export async function generateUploadUrl(key: string, mimeType: string, expiresIn
// Generate signed upload URL // Generate signed upload URL
const command = new PutObjectCommand({ const command = new PutObjectCommand({
Bucket: s3BucketName, Bucket: getS3BucketName(),
Key: key, Key: key,
ContentType: mimeType, ContentType: mimeType,
}); });
const signedUrl = await getSignedUrl(s3Client, command, { expiresIn }); const signedUrl = await getSignedUrl(getS3Client(), command, { expiresIn });
return signedUrl; return signedUrl;
} catch (error) { } catch (error) {
console.error('Error generating upload URL:', error); console.error('Error generating upload URL:', error);

View file

@ -10,7 +10,7 @@ import {
type StaffRoleName, type StaffRoleName,
type StaffPermissionName, type StaffPermissionName,
} from '@/src/dbService' } from '@/src/dbService'
import { minOrderValue, deliveryCharge } from '@/src/lib/env-exporter' import { getMinOrderValue, getDeliveryCharge } from '@/src/lib/env-exporter'
import { CONST_KEYS } from '@/src/lib/const-keys' import { CONST_KEYS } from '@/src/lib/const-keys'
export async function seed() { export async function seed() {
@ -50,9 +50,9 @@ export async function seed() {
// Seed key-val store constants // Seed key-val store constants
const constantsToSeed: KeyValSeedData[] = [ const constantsToSeed: KeyValSeedData[] = [
{ key: CONST_KEYS.readableOrderId, value: 0 }, { key: CONST_KEYS.readableOrderId, value: 0 },
{ key: CONST_KEYS.minRegularOrderValue, value: minOrderValue }, { key: CONST_KEYS.minRegularOrderValue, value: getMinOrderValue() },
{ key: CONST_KEYS.freeDeliveryThreshold, value: minOrderValue }, { key: CONST_KEYS.freeDeliveryThreshold, value: getMinOrderValue() },
{ key: CONST_KEYS.deliveryCharge, value: deliveryCharge }, { key: CONST_KEYS.deliveryCharge, value: getDeliveryCharge() },
{ key: CONST_KEYS.flashFreeDeliveryThreshold, value: 500 }, { key: CONST_KEYS.flashFreeDeliveryThreshold, value: 500 },
{ key: CONST_KEYS.flashDeliveryCharge, value: 69 }, { key: CONST_KEYS.flashDeliveryCharge, value: 69 },
{ key: CONST_KEYS.popularItems, value: [] }, { key: CONST_KEYS.popularItems, value: [] },

View file

@ -1,9 +1,5 @@
import axios from 'axios'; import axios from 'axios';
import { isDevMode, telegramBotToken, telegramChatIds } from '@/src/lib/env-exporter' import { getIsDevMode, getTelegramBotToken, getTelegramChatIds } from '@/src/lib/env-exporter'
const BOT_TOKEN = telegramBotToken;
const CHAT_IDS = telegramChatIds;
const TELEGRAM_API_URL = `https://api.telegram.org/bot${BOT_TOKEN}`;
/** /**
* Send a message to Telegram bot * Send a message to Telegram bot
@ -11,14 +7,23 @@ const TELEGRAM_API_URL = `https://api.telegram.org/bot${BOT_TOKEN}`;
* @returns Promise<boolean | null> indicating success, failure, or null if dev mode * @returns Promise<boolean | null> indicating success, failure, or null if dev mode
*/ */
export const sendTelegramMessage = async (message: string): Promise<boolean | null> => { export const sendTelegramMessage = async (message: string): Promise<boolean | null> => {
if (isDevMode) { console.log('from send telegram message')
if (getIsDevMode()) {
return null; return null;
} }
const botToken = getTelegramBotToken()
const chatIds = getTelegramChatIds()
if (!botToken || chatIds.length === 0) {
console.warn('Telegram credentials not configured, skipping notification')
return null
}
const telegramApiUrl = `https://api.telegram.org/bot${botToken}`
console.log(botToken, chatIds, telegramApiUrl)
try { try {
const results = await Promise.all( const results = await Promise.all(
CHAT_IDS.map(async (chatId) => { chatIds.map(async (chatId) => {
try { try {
const response = await axios.post(`${TELEGRAM_API_URL}/sendMessage`, { const response = await axios.post(`${telegramApiUrl}/sendMessage`, {
chat_id: chatId, chat_id: chatId,
text: message, text: message,
parse_mode: 'HTML', parse_mode: 'HTML',

View file

@ -5,9 +5,7 @@ import { initializeProductTagStore } from '@/src/stores/product-tag-store'
import { initializeSlotStore } from '@/src/stores/slot-store' import { initializeSlotStore } from '@/src/stores/slot-store'
import { initializeBannerStore } from '@/src/stores/banner-store' import { initializeBannerStore } from '@/src/stores/banner-store'
import { createAllCacheFiles } from '@/src/lib/cloud_cache' import { createAllCacheFiles } from '@/src/lib/cloud_cache'
import type { DurableObjectNamespace } from '@cloudflare/workers-types'
const STORE_INIT_DELAY_MS = 3 * 60 * 1000
let storeInitializationTimeout: NodeJS.Timeout | null = null
/** /**
* Initialize all application stores * Initialize all application stores
@ -45,15 +43,13 @@ export const initializeAllStores = async (): Promise<void> => {
}; };
export const scheduleStoreInitialization = (): void => { export const scheduleStoreInitialization = (): void => {
if (storeInitializationTimeout) { const env = (globalThis as { ENV?: { CACHE_CREATOR?: DurableObjectNamespace } }).ENV
clearTimeout(storeInitializationTimeout) if (!env?.CACHE_CREATOR) {
storeInitializationTimeout = null console.warn('CACHE_CREATOR durable object binding not available for store initialization')
return
} }
storeInitializationTimeout = setTimeout(() => { const id = env.CACHE_CREATOR.idFromName('store-init')
storeInitializationTimeout = null const stub = env.CACHE_CREATOR.get(id)
initializeAllStores().catch(error => { void stub.fetch('https://cache-creator/schedule', { method: 'POST' })
console.error('Scheduled store initialization failed:', error)
})
}, STORE_INIT_DELAY_MS)
} }

View file

@ -2,7 +2,7 @@ import { router, protectedProcedure } from "@/src/trpc/trpc-index"
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod";
import { ApiError } from "@/src/lib/api-error" import { ApiError } from "@/src/lib/api-error"
import { appUrl } from "@/src/lib/env-exporter" import { getAppUrl } from "@/src/lib/env-exporter"
// import redisClient from "@/src/lib/redis-client" // import redisClient from "@/src/lib/redis-client"
// import { getSlotSequenceKey } from "@/src/lib/redisKeyGetters" // import { getSlotSequenceKey } from "@/src/lib/redisKeyGetters"
import { scheduleStoreInitialization } from '@/src/stores/store-initializer' import { scheduleStoreInitialization } from '@/src/stores/store-initializer'
@ -426,7 +426,7 @@ export const slotsRouter = router({
...slot, ...slot,
vendorSnippets: slot.vendorSnippets.map(snippet => ({ vendorSnippets: slot.vendorSnippets.map(snippet => ({
...snippet, ...snippet,
accessUrl: `${appUrl}/vendor-order-list?id=${snippet.snippetCode}`, accessUrl: `${getAppUrl()}/vendor-order-list?id=${snippet.snippetCode}`,
})), })),
}, },
} }

View file

@ -1,7 +1,6 @@
import { protectedProcedure } from '@/src/trpc/trpc-index'; import { protectedProcedure } from '@/src/trpc/trpc-index';
import { z } from 'zod'; import { z } from 'zod';
import { ApiError } from '@/src/lib/api-error'; import { ApiError } from '@/src/lib/api-error';
import { notificationQueue } from '@/src/lib/notif-job';
import { recomputeUserNegativityScore } from '@/src/stores/user-negativity-store'; import { recomputeUserNegativityScore } from '@/src/stores/user-negativity-store';
import { import {
createUserByMobile, createUserByMobile,
@ -24,6 +23,7 @@ import {
getUserIncidentsWithRelations, getUserIncidentsWithRelations,
createUserIncident, createUserIncident,
} from '@/src/dbService'; } from '@/src/dbService';
import { queueDataPusher } from '@/src/lib/queue-data-pusher'
export const userRouter = { export const userRouter = {
createUserByMobile: protectedProcedure createUserByMobile: protectedProcedure
@ -237,18 +237,22 @@ export const userRouter = {
let queuedCount = 0; let queuedCount = 0;
for (const token of tokens) { for (const token of tokens) {
try { try {
await notificationQueue.add('send-admin-notification', { await queueDataPusher.pushNotifQueue({
token, name: 'send-admin-notification',
title, jobData: {
body: text, token,
imageUrl: imageUrl || null, title,
}, { body: text,
attempts: 3, imageUrl: imageUrl || null,
backoff: {
type: 'exponential',
delay: 2000,
}, },
}); options: {
attempts: 3,
backoff: {
type: 'exponential',
delay: 2000,
},
},
})
queuedCount++; queuedCount++;
} catch (error) { } catch (error) {
console.error(`Failed to queue notification for token:`, error); console.error(`Failed to queue notification for token:`, error);

View file

@ -1,7 +1,7 @@
import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index' import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod' import { z } from 'zod'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { appUrl } from '@/src/lib/env-exporter' import { getAppUrl } from '@/src/lib/env-exporter'
import { import {
checkVendorSnippetExists as checkVendorSnippetExistsInDb, checkVendorSnippetExists as checkVendorSnippetExistsInDb,
getVendorSnippetById as getVendorSnippetByIdInDb, getVendorSnippetById as getVendorSnippetByIdInDb,
@ -138,7 +138,7 @@ export const vendorSnippetsRouter = router({
return { return {
...snippet, ...snippet,
accessUrl: `${appUrl}/vendor-order-list?id=${snippet.snippetCode}`, accessUrl: `${getAppUrl()}/vendor-order-list?id=${snippet.snippetCode}`,
products, products,
} }
}) })
@ -162,7 +162,7 @@ export const vendorSnippetsRouter = router({
return { return {
...snippet, ...snippet,
accessUrl: `${appUrl}/vendor-order-list?id=${snippet.snippetCode}`, accessUrl: `${getAppUrl()}/vendor-order-list?id=${snippet.snippetCode}`,
products: products.map(p => ({ id: p.id, name: p.name })), products: products.map(p => ({ id: p.id, name: p.name })),
}; };
}) })
@ -431,11 +431,11 @@ export const vendorSnippetsRouter = router({
return { return {
orderId: `ORD${order.id}`, orderId: `ORD${order.id}`,
orderDate: order.createdAt.toISOString(), orderDate: order.createdAt ? order.createdAt.toISOString() : new Date(0).toISOString(),
customerName: order.user.name || '', customerName: order.user.name || '',
totalAmount: orderTotal, totalAmount: orderTotal,
slotInfo: order.slot ? { slotInfo: order.slot ? {
time: order.slot.deliveryTime.toISOString(), time: order.slot.deliveryTime ? order.slot.deliveryTime.toISOString() : new Date(0).toISOString(),
sequence: order.slot.deliverySequence, sequence: order.slot.deliverySequence,
} : null, } : null,
products, products,
@ -485,7 +485,7 @@ export const vendorSnippetsRouter = router({
return vendorOrders.map(order => ({ return vendorOrders.map(order => ({
id: order.id, id: order.id,
status: 'pending', status: 'pending',
orderDate: order.createdAt.toISOString(), orderDate: order.createdAt ? order.createdAt.toISOString() : new Date(0).toISOString(),
totalQuantity: order.orderItems.reduce((sum, item) => sum + parseFloat(item.quantity || '0'), 0), totalQuantity: order.orderItems.reduce((sum, item) => sum + parseFloat(item.quantity || '0'), 0),
products: order.orderItems.map(item => ({ products: order.orderItems.map(item => ({
name: item.product.name, name: item.product.name,
@ -616,11 +616,11 @@ export const vendorSnippetsRouter = router({
return { return {
orderId: `ORD${order.id}`, orderId: `ORD${order.id}`,
orderDate: order.createdAt.toISOString(), orderDate: order.createdAt ? order.createdAt.toISOString() : new Date(0).toISOString(),
customerName: order.user.name || '', customerName: order.user.name || '',
totalAmount: orderTotal, totalAmount: orderTotal,
slotInfo: order.slot ? { slotInfo: order.slot ? {
time: order.slot.deliveryTime.toISOString(), time: order.slot.deliveryTime ? order.slot.deliveryTime.toISOString() : new Date(0).toISOString(),
sequence: order.slot.deliverySequence, sequence: order.slot.deliverySequence,
} : null, } : null,
products, products,
@ -638,13 +638,13 @@ export const vendorSnippetsRouter = router({
slotId: snippet.slotId, slotId: snippet.slotId,
productIds: snippet.productIds, productIds: snippet.productIds,
validTill: snippet.validTill?.toISOString(), validTill: snippet.validTill?.toISOString(),
createdAt: snippet.createdAt.toISOString(), createdAt: snippet.createdAt ? snippet.createdAt.toISOString() : new Date(0).toISOString(),
isPermanent: snippet.isPermanent, isPermanent: snippet.isPermanent,
}, },
selectedSlot: { selectedSlot: {
id: slot.id, id: slot.id,
deliveryTime: slot.deliveryTime.toISOString(), deliveryTime: slot.deliveryTime ? slot.deliveryTime.toISOString() : new Date(0).toISOString(),
freezeTime: slot.freezeTime.toISOString(), freezeTime: slot.freezeTime ? slot.freezeTime.toISOString() : new Date(0).toISOString(),
deliverySequence: slot.deliverySequence, deliverySequence: slot.deliverySequence,
}, },
} }

View file

@ -3,6 +3,7 @@ import { commonRouter } from '@/src/trpc/apis/common-apis/common'
import { import {
getStoresSummary, getStoresSummary,
healthCheck, healthCheck,
getCacheVersion,
} from '@/src/dbService' } from '@/src/dbService'
import type { StoresSummaryResponse } from '@packages/shared' import type { StoresSummaryResponse } from '@packages/shared'
import * as turf from '@turf/turf'; import * as turf from '@turf/turf';
@ -12,12 +13,13 @@ import { generateUploadUrl } from '@/src/lib/s3-client'
import { ApiError } from '@/src/lib/api-error' import { ApiError } from '@/src/lib/api-error'
import { getAllConstValues } from '@/src/lib/const-store' import { getAllConstValues } from '@/src/lib/const-store'
import { CONST_KEYS } from '@/src/lib/const-keys' import { CONST_KEYS } from '@/src/lib/const-keys'
import { assetsDomain, apiCacheKey } from '@/src/lib/env-exporter' import { getAssetsDomain, getApiCacheKey } from '@/src/lib/env-exporter'
const polygon = turf.polygon(mbnrGeoJson.features[0].geometry.coordinates); const polygon = turf.polygon(mbnrGeoJson.features[0].geometry.coordinates);
export async function scaffoldEssentialConsts() { export async function scaffoldEssentialConsts() {
const consts = await getAllConstValues(); const consts = await getAllConstValues();
const cacheVersion = await getCacheVersion()
return { return {
freeDeliveryThreshold: consts[CONST_KEYS.freeDeliveryThreshold] ?? 200, freeDeliveryThreshold: consts[CONST_KEYS.freeDeliveryThreshold] ?? 200,
@ -33,8 +35,9 @@ export async function scaffoldEssentialConsts() {
isFlashDeliveryEnabled: consts[CONST_KEYS.isFlashDeliveryEnabled] ?? true, isFlashDeliveryEnabled: consts[CONST_KEYS.isFlashDeliveryEnabled] ?? true,
supportMobile: consts[CONST_KEYS.supportMobile] ?? '', supportMobile: consts[CONST_KEYS.supportMobile] ?? '',
supportEmail: consts[CONST_KEYS.supportEmail] ?? '', supportEmail: consts[CONST_KEYS.supportEmail] ?? '',
assetsDomain, assetsDomain: getAssetsDomain(),
apiCacheKey, apiCacheKey: getApiCacheKey(),
cacheVersion,
}; };
} }

View file

@ -236,10 +236,11 @@ const placeOrderUtil = async (params: {
); );
} }
for (const order of createdOrders) { // for (const order of createdOrders) {
sendOrderPlacedNotification(userId, order.id.toString()); // sendOrderPlacedNotification(userId, order.id.toString());
} // }
console.log('publishing the order')
await publishFormattedOrder(createdOrders, ordersBySlot); await publishFormattedOrder(createdOrders, ordersBySlot);
return { success: true, data: createdOrders }; return { success: true, data: createdOrders };
@ -603,7 +604,7 @@ export const orderRouter = router({
await cancelUserOrderTransaction(id, status.id, reason, order.isCod); await cancelUserOrderTransaction(id, status.id, reason, order.isCod);
await sendOrderCancelledNotification(userId, id.toString()); // await sendOrderCancelledNotification(userId, id.toString());
await publishCancellation(id, 'user', reason); await publishCancellation(id, 'user', reason);

View file

@ -2,8 +2,7 @@
import { router, protectedProcedure } from '@/src/trpc/trpc-index' import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod' import { z } from 'zod'
import { ApiError } from '@/src/lib/api-error' import { ApiError } from '@/src/lib/api-error'
import crypto from 'crypto' import { getRazorpayId, getRazorpaySecret } from "@/src/lib/env-exporter"
import { razorpayId, razorpaySecret } from "@/src/lib/env-exporter"
import { RazorpayPaymentService } from "@/src/lib/payments-utils" import { RazorpayPaymentService } from "@/src/lib/payments-utils"
import { import {
getUserPaymentOrderById as getUserPaymentOrderByIdInDb, getUserPaymentOrderById as getUserPaymentOrderByIdInDb,
@ -60,8 +59,8 @@ export const paymentRouter = router({
if (existingPayment && existingPayment.status === 'pending') { if (existingPayment && existingPayment.status === 'pending') {
return { return {
razorpayOrderId: existingPayment.merchantOrderId, razorpayOrderId: existingPayment.merchantOrderId,
key: razorpayId, key: getRazorpayId(),
}; };
} }
@ -72,10 +71,10 @@ export const paymentRouter = router({
const razorpayOrder = await RazorpayPaymentService.createOrder(parseInt(orderId), order.totalAmount); const razorpayOrder = await RazorpayPaymentService.createOrder(parseInt(orderId), order.totalAmount);
await RazorpayPaymentService.insertPaymentRecord(parseInt(orderId), razorpayOrder); await RazorpayPaymentService.insertPaymentRecord(parseInt(orderId), razorpayOrder);
return { return {
razorpayOrderId: 0, razorpayOrderId: 0,
key: razorpayId, key: getRazorpayId(),
} }
}), }),
@ -90,10 +89,22 @@ export const paymentRouter = router({
const { razorpay_payment_id, razorpay_order_id, razorpay_signature } = input; const { razorpay_payment_id, razorpay_order_id, razorpay_signature } = input;
// Verify signature // Verify signature
const expectedSignature = crypto const encoder = new TextEncoder()
.createHmac('sha256', razorpaySecret) const key = await crypto.subtle.importKey(
.update(razorpay_order_id + '|' + razorpay_payment_id) 'raw',
.digest('hex'); encoder.encode(getRazorpaySecret()),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
)
const signature = await crypto.subtle.sign(
'HMAC',
key,
encoder.encode(`${razorpay_order_id}|${razorpay_payment_id}`)
)
const expectedSignature = Array.from(new Uint8Array(signature))
.map((byte) => byte.toString(16).padStart(2, '0'))
.join('')
if (expectedSignature !== razorpay_signature) { if (expectedSignature !== razorpay_signature) {
throw new ApiError("Invalid payment signature", 400); throw new ApiError("Invalid payment signature", 400);

View file

@ -1,9 +1,33 @@
import type { ExecutionContext, D1Database } from '@cloudflare/workers-types' import type {
ExecutionContext,
D1Database,
DurableObjectNamespace,
} from '@cloudflare/workers-types'
import { CacheCreator } from './src/jobs/cache-creator'
import {
handleNotifQueue,
handleOrderPlacedQueue,
handleOrderCancelledQueue,
} from './src/lib/queue-consumer'
export { CacheCreator }
export default { export default {
async fetch( async fetch(
request: Request, request: Request,
env: Record<string, string> & { DB?: D1Database }, env: Record<string, string> & {
DB?: D1Database
CACHE_CREATOR: DurableObjectNamespace
NOTIF_QUEUE: {
send: (message: unknown) => Promise<void>
}
ORDER_PLACED_QUEUE: {
send: (message: unknown) => Promise<void>
}
ORDER_CANCELLED_QUEUE: {
send: (message: unknown) => Promise<void>
}
},
ctx: ExecutionContext ctx: ExecutionContext
) { ) {
;(globalThis as any).ENV = env ;(globalThis as any).ENV = env
@ -15,4 +39,35 @@ export default {
const app = createApp() const app = createApp()
return app.fetch(request, env, ctx) return app.fetch(request, env, ctx)
}, },
async queue(
batch: any,
env: Record<string, string> & {
NOTIF_QUEUE: {
send: (message: unknown) => Promise<void>
}
ORDER_PLACED_QUEUE: {
send: (message: unknown) => Promise<void>
}
ORDER_CANCELLED_QUEUE: {
send: (message: unknown) => Promise<void>
}
}
) {
if (batch?.queue === 'notif_queue') {
handleNotifQueue(batch)
return
}
if (batch?.queue === 'order_placed_queue') {
await handleOrderPlacedQueue(batch)
return
}
if (batch?.queue === 'order_cancelled_queue') {
await handleOrderCancelledQueue(batch)
return
}
handleNotifQueue(batch)
},
} }

View file

@ -8,6 +8,32 @@ binding = "DB"
database_name = "freshyo-dev" database_name = "freshyo-dev"
database_id = "45e81d12-9043-45ad-a8ba-3b93127dc5ea" database_id = "45e81d12-9043-45ad-a8ba-3b93127dc5ea"
[durable_objects]
bindings = [
{ name = "CACHE_CREATOR", class_name = "CacheCreator" },
]
[[queues.producers]]
binding = "NOTIF_QUEUE"
queue = "notif_queue"
[[queues.producers]]
binding = "ORDER_PLACED_QUEUE"
queue = "order_placed_queue"
[[queues.producers]]
binding = "ORDER_CANCELLED_QUEUE"
queue = "order_cancelled_queue"
[[queues.consumers]]
queue = "notif_queue"
[[queues.consumers]]
queue = "order_placed_queue"
[[queues.consumers]]
queue = "order_cancelled_queue"
[vars] [vars]
ENV_MODE = "PROD" ENV_MODE = "PROD"
DATABASE_URL = "postgresql://postgres:meatfarmer_master_password@57.128.212.174:7447/meatfarmer" DATABASE_URL = "postgresql://postgres:meatfarmer_master_password@57.128.212.174:7447/meatfarmer"

BIN
apps/testdb.sqlite Normal file

Binary file not shown.

View file

@ -387,6 +387,7 @@ export default function Dashboard() {
const products = productsData?.products || []; const products = productsData?.products || [];
React.useEffect(() => { React.useEffect(() => {
if (products.length > 0 && displayedProducts.length === 0) { if (products.length > 0 && displayedProducts.length === 0) {
const initialBatch = products const initialBatch = products

View file

@ -21,19 +21,22 @@ type StoreWithProductsResponse = StoreWithProductsApiType;
function useCacheUrl(filename: string): string | null { function useCacheUrl(filename: string): string | null {
const { data: essentialConsts } = useGetEssentialConsts() const { data: essentialConsts } = useGetEssentialConsts()
console.log(essentialConsts)
const assetsDomain = essentialConsts?.assetsDomain const assetsDomain = essentialConsts?.assetsDomain
const apiCacheKey = essentialConsts?.apiCacheKey const apiCacheKey = essentialConsts?.apiCacheKey
const cacheVersion = essentialConsts?.cacheVersion
return assetsDomain && apiCacheKey if (!assetsDomain || !apiCacheKey || cacheVersion === undefined || cacheVersion === null) {
? `${assetsDomain}${apiCacheKey}/${filename}` return null
: null }
return `${assetsDomain}${apiCacheKey}/v-${cacheVersion}/${filename}`
} }
export function useAllProducts() { export function useAllProducts() {
const cacheUrl = useCacheUrl(CACHE_FILENAMES.products) const cacheUrl = useCacheUrl(CACHE_FILENAMES.products)
console.log({cacheUrl})
return useQuery<ProductsResponse>({ return useQuery<ProductsResponse>({
queryKey: ['all-products', cacheUrl], queryKey: ['all-products', cacheUrl],
queryFn: async () => { queryFn: async () => {
@ -74,7 +77,7 @@ export function useSlots() {
if (!cacheUrl) { if (!cacheUrl) {
throw new Error('Cache URL not available') throw new Error('Cache URL not available')
} }
const response = await axios.get<SlotsResponse>(cacheUrl) const response = await axios.get<SlotsResponse>(cacheUrl+'?v=123')
return response.data return response.data
}, },
staleTime: 60000, // 1 minute staleTime: 60000, // 1 minute

View file

@ -30,6 +30,8 @@ export {
// Constants // Constants
getAllConstants, getAllConstants,
upsertConstants, upsertConstants,
getCacheVersion,
incrementCacheVersion,
} from './src/admin-apis/const'; } from './src/admin-apis/const';
export { export {

View file

@ -1,5 +1,6 @@
import { db } from '../db/db_index'; import { db } from '../db/db_index';
import { keyValStore } from '../db/schema'; import { keyValStore } from '../db/schema';
import { eq } from 'drizzle-orm';
export interface Constant { export interface Constant {
key: string; key: string;
@ -27,3 +28,39 @@ export async function upsertConstants(constants: Constant[]): Promise<void> {
} }
}); });
} }
const CACHE_VERSION_KEY = 'cache_version';
const parseCacheVersion = (value: unknown) => {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : 0;
};
export async function getCacheVersion(): Promise<number> {
const record = await db.query.keyValStore.findFirst({
where: eq(keyValStore.key, CACHE_VERSION_KEY),
columns: { value: true },
});
return record ? parseCacheVersion(record.value) : 0;
}
export async function incrementCacheVersion(): Promise<number> {
return db.transaction(async (tx) => {
const existing = await tx.query.keyValStore.findFirst({
where: eq(keyValStore.key, CACHE_VERSION_KEY),
columns: { value: true },
});
const nextValue = parseCacheVersion(existing?.value) + 1;
await tx.insert(keyValStore)
.values({ key: CACHE_VERSION_KEY, value: nextValue })
.onConflictDoUpdate({
target: keyValStore.key,
set: { value: nextValue },
});
return nextValue;
});
}

View file

@ -29,6 +29,8 @@ export {
// Constants // Constants
getAllConstants, getAllConstants,
upsertConstants, upsertConstants,
getCacheVersion,
incrementCacheVersion,
} from './src/admin-apis/const' } from './src/admin-apis/const'
export { export {

View file

@ -1,5 +1,6 @@
import { db } from '../db/db_index' import { db } from '../db/db_index'
import { keyValStore } from '../db/schema' import { keyValStore } from '../db/schema'
import { eq } from 'drizzle-orm'
export interface Constant { export interface Constant {
key: string key: string
@ -27,3 +28,41 @@ export async function upsertConstants(constants: Constant[]): Promise<void> {
} }
}) })
} }
const CACHE_VERSION_KEY = 'cache_version'
const parseCacheVersion = (value: unknown) => {
const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : 0
}
export async function getCacheVersion(): Promise<number> {
const record = await db.query.keyValStore.findFirst({
where: eq(keyValStore.key, CACHE_VERSION_KEY),
columns: { value: true },
})
return record ? parseCacheVersion(record.value) : 0
}
export async function incrementCacheVersion(): Promise<number> {
return db.transaction(async (tx) => {
const existing = await tx.query.keyValStore.findFirst({
where: eq(keyValStore.key, CACHE_VERSION_KEY),
columns: { value: true },
})
const nextValue = parseCacheVersion(existing?.value) + 1
if (existing) {
await tx.update(keyValStore)
.set({ value: nextValue })
.where(eq(keyValStore.key, CACHE_VERSION_KEY))
} else {
await tx.insert(keyValStore)
.values({ key: CACHE_VERSION_KEY, value: nextValue })
}
return nextValue
})
}

View file

@ -27,6 +27,7 @@ import type {
PaymentStatus, PaymentStatus,
} from '@packages/shared' } from '@packages/shared'
import type { InferSelectModel } from 'drizzle-orm' import type { InferSelectModel } from 'drizzle-orm'
import { coerceDate } from '../lib/date'
const isPaymentStatus = (value: string): value is PaymentStatus => const isPaymentStatus = (value: string): value is PaymentStatus =>
value === 'pending' || value === 'success' || value === 'cod' || value === 'failed' value === 'pending' || value === 'success' || value === 'cod' || value === 'failed'
@ -216,7 +217,7 @@ export async function getOrderDetails(orderId: number): Promise<AdminOrderDetail
}, },
slotInfo: orderData.slot slotInfo: orderData.slot
? { ? {
time: orderData.slot.deliveryTime.toISOString(), time: (coerceDate(orderData.slot.deliveryTime) ?? new Date(0)).toISOString(),
sequence: orderData.slot.deliverySequence, sequence: orderData.slot.deliverySequence,
} }
: null, : null,
@ -392,7 +393,7 @@ export async function getSlotOrders(slotId: string): Promise<AdminGetSlotOrdersR
longitude: order.address.adminLongitude ?? order.address.longitude, longitude: order.address.adminLongitude ?? order.address.longitude,
totalAmount: parseFloat(order.totalAmount), totalAmount: parseFloat(order.totalAmount),
items, items,
deliveryTime: order.slot?.deliveryTime.toISOString() || null, deliveryTime: coerceDate(order.slot?.deliveryTime)?.toISOString() || null,
status, status,
isPackaged: order.orderItems.every((item: any) => item.is_packaged) || false, isPackaged: order.orderItems.every((item: any) => item.is_packaged) || false,
isDelivered: statusRecord?.isDelivered || false, isDelivered: statusRecord?.isDelivered || false,
@ -546,7 +547,7 @@ export async function getAllOrders(input: GetAllOrdersInput): Promise<AdminGetAl
deliveryCharge: parseFloat(order.deliveryCharge || '0'), deliveryCharge: parseFloat(order.deliveryCharge || '0'),
items, items,
createdAt: order.createdAt, createdAt: order.createdAt,
deliveryTime: order.slot?.deliveryTime.toISOString() || null, deliveryTime: coerceDate(order.slot?.deliveryTime)?.toISOString() || null,
status, status,
isPackaged: order.orderItems.every((item: any) => item.is_packaged) || false, isPackaged: order.orderItems.every((item: any) => item.is_packaged) || false,
isDelivered: statusRecord?.isDelivered || false, isDelivered: statusRecord?.isDelivered || false,

View file

@ -17,6 +17,7 @@ import type {
AdminSlotProductSummary, AdminSlotProductSummary,
AdminUpdateSlotCapacityResult, AdminUpdateSlotCapacityResult,
} from '@packages/shared' } from '@packages/shared'
import { coerceDate } from '../lib/date'
type SlotSnippetInput = { type SlotSnippetInput = {
name: string name: string
@ -34,10 +35,45 @@ const getNumberArray = (value: unknown): number[] => {
return value.map((item) => Number(item)) return value.map((item) => Number(item))
} }
const normalizeProductIds = (value: unknown): number[] => {
if (!Array.isArray(value)) return []
const ids = value
.map((item) => Number(item))
.filter((item) => Number.isFinite(item))
return Array.from(new Set(ids))
}
const chunkArray = <T>(items: T[], size: number): T[][] => {
if (size <= 0) return [items]
const chunks: T[][] = []
for (let i = 0; i < items.length; i += size) {
chunks.push(items.slice(i, i + size))
}
return chunks
}
const PRODUCT_ID_CHUNK_SIZE = 40
const PRODUCT_SLOT_CHUNK_SIZE = 40
const fetchExistingProductIds = async (tx: any, productIds: number[]) => {
const existingIds = new Set<number>()
const chunks = chunkArray(productIds, PRODUCT_ID_CHUNK_SIZE)
for (const chunk of chunks) {
if (chunk.length === 0) continue
const products = await tx.query.productInfo.findMany({
where: inArray(productInfo.id, chunk),
columns: { id: true },
})
products.forEach((product: { id: number }) => existingIds.add(product.id))
}
return existingIds
}
const mapDeliverySlot = (slot: typeof deliverySlotInfo.$inferSelect): AdminDeliverySlot => ({ const mapDeliverySlot = (slot: typeof deliverySlotInfo.$inferSelect): AdminDeliverySlot => ({
id: slot.id, id: slot.id,
deliveryTime: slot.deliveryTime, deliveryTime: coerceDate(slot.deliveryTime) ?? new Date(0),
freezeTime: slot.freezeTime, freezeTime: coerceDate(slot.freezeTime) ?? new Date(0),
isActive: slot.isActive, isActive: slot.isActive,
isFlash: slot.isFlash, isFlash: slot.isFlash,
isCapacityFull: slot.isCapacityFull, isCapacityFull: slot.isCapacityFull,
@ -57,8 +93,8 @@ const mapVendorSnippet = (snippet: typeof vendorSnippets.$inferSelect): AdminVen
slotId: snippet.slotId ?? null, slotId: snippet.slotId ?? null,
productIds: snippet.productIds || [], productIds: snippet.productIds || [],
isPermanent: snippet.isPermanent, isPermanent: snippet.isPermanent,
validTill: snippet.validTill ?? null, validTill: coerceDate(snippet.validTill),
createdAt: snippet.createdAt, createdAt: coerceDate(snippet.createdAt) ?? new Date(0),
}) })
export async function getActiveSlotsWithProducts(): Promise<AdminSlotWithProducts[]> { export async function getActiveSlotsWithProducts(): Promise<AdminSlotWithProducts[]> {
@ -161,12 +197,22 @@ export async function createSlotWithRelations(input: {
}) })
.returning() .returning()
if (productIds && productIds.length > 0) { const normalizedProductIds = normalizeProductIds(productIds)
const associations = productIds.map((productId) => ({ if (productIds && normalizedProductIds.length > 0) {
const existingIds = await fetchExistingProductIds(tx, normalizedProductIds)
const missingIds = normalizedProductIds.filter((productId) => !existingIds.has(productId))
if (missingIds.length > 0) {
throw new Error(`Invalid product IDs: ${missingIds.join(', ')}`)
}
const associations = normalizedProductIds.map((productId) => ({
productId, productId,
slotId: newSlot.id, slotId: newSlot.id,
})) }))
await tx.insert(productSlots).values(associations) const associationChunks = chunkArray(associations, PRODUCT_SLOT_CHUNK_SIZE)
for (const chunk of associationChunks) {
await tx.insert(productSlots).values(chunk)
}
} }
let createdSnippets: AdminVendorSnippet[] = [] let createdSnippets: AdminVendorSnippet[] = []
@ -246,12 +292,22 @@ export async function updateSlotWithRelations(input: {
if (productIds !== undefined) { if (productIds !== undefined) {
await tx.delete(productSlots).where(eq(productSlots.slotId, id)) await tx.delete(productSlots).where(eq(productSlots.slotId, id))
if (productIds.length > 0) { const normalizedProductIds = normalizeProductIds(productIds)
const associations = productIds.map((productId) => ({ if (productIds.length > 0 && normalizedProductIds.length > 0) {
const existingIds = await fetchExistingProductIds(tx, normalizedProductIds)
const missingIds = normalizedProductIds.filter((productId) => !existingIds.has(productId))
if (missingIds.length > 0) {
throw new Error(`Invalid product IDs: ${missingIds.join(', ')}`)
}
const associations = normalizedProductIds.map((productId) => ({
productId, productId,
slotId: id, slotId: id,
})) }))
await tx.insert(productSlots).values(associations) const associationChunks = chunkArray(associations, PRODUCT_SLOT_CHUNK_SIZE)
for (const chunk of associationChunks) {
await tx.insert(productSlots).values(chunk)
}
} }
} }

View file

@ -9,6 +9,7 @@ import type {
AdminVendorSnippetProduct, AdminVendorSnippetProduct,
AdminVendorUpdatePackagingResult, AdminVendorUpdatePackagingResult,
} from '@packages/shared' } from '@packages/shared'
import { coerceDate } from '../lib/date'
type VendorSnippetRow = InferSelectModel<typeof vendorSnippets> type VendorSnippetRow = InferSelectModel<typeof vendorSnippets>
type DeliverySlotRow = InferSelectModel<typeof deliverySlotInfo> type DeliverySlotRow = InferSelectModel<typeof deliverySlotInfo>
@ -20,14 +21,14 @@ const mapVendorSnippet = (snippet: VendorSnippetRow): AdminVendorSnippet => ({
slotId: snippet.slotId ?? null, slotId: snippet.slotId ?? null,
productIds: snippet.productIds || [], productIds: snippet.productIds || [],
isPermanent: snippet.isPermanent, isPermanent: snippet.isPermanent,
validTill: snippet.validTill ?? null, validTill: coerceDate(snippet.validTill),
createdAt: snippet.createdAt, createdAt: coerceDate(snippet.createdAt) ?? new Date(0),
}) })
const mapDeliverySlot = (slot: DeliverySlotRow): AdminDeliverySlot => ({ const mapDeliverySlot = (slot: DeliverySlotRow): AdminDeliverySlot => ({
id: slot.id, id: slot.id,
deliveryTime: slot.deliveryTime, deliveryTime: coerceDate(slot.deliveryTime) ?? new Date(0),
freezeTime: slot.freezeTime, freezeTime: coerceDate(slot.freezeTime) ?? new Date(0),
isActive: slot.isActive, isActive: slot.isActive,
isFlash: slot.isFlash, isFlash: slot.isFlash,
isCapacityFull: slot.isCapacityFull, isCapacityFull: slot.isCapacityFull,

View file

@ -9,6 +9,7 @@ import {
customType, customType,
} from 'drizzle-orm/sqlite-core' } from 'drizzle-orm/sqlite-core'
import { relations, sql } from 'drizzle-orm' import { relations, sql } from 'drizzle-orm'
import { coerceDate } from '../lib/date'
const jsonText = <T>(name: string) => const jsonText = <T>(name: string) =>
customType<{ data: T | null; driverData: string | null }>({ customType<{ data: T | null; driverData: string | null }>({
@ -44,6 +45,22 @@ const numericText = (name: string) =>
}, },
})(name) })(name)
const timestampText = (name: string) =>
customType<{ data: Date | null; driverData: string | number | null }>({
dataType() {
return 'text'
},
toDriver(value) {
if (value === undefined || value === null) return null
if (value instanceof Date) return value.toISOString()
return String(value)
},
fromDriver(value) {
if (value === null || value === undefined) return null
return coerceDate(value)
},
})(name)
const staffRoleValues = ['super_admin', 'admin', 'marketer', 'delivery_staff'] as const const staffRoleValues = ['super_admin', 'admin', 'marketer', 'delivery_staff'] as const
const staffPermissionValues = ['crud_product', 'make_coupon', 'crud_staff_users'] as const const staffPermissionValues = ['crud_product', 'make_coupon', 'crud_staff_users'] as const
const uploadStatusValues = ['pending', 'claimed'] as const const uploadStatusValues = ['pending', 'claimed'] as const
@ -59,7 +76,7 @@ export const users = sqliteTable('users', {
name: text(), name: text(),
email: text(), email: text(),
mobile: text(), mobile: text(),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(), createdAt: timestampText('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
}, (t) => ({ }, (t) => ({
unq_email: uniqueIndex('unique_email').on(t.email), unq_email: uniqueIndex('unique_email').on(t.email),
})) }))
@ -68,33 +85,33 @@ export const userDetails = sqliteTable('user_details', {
id: integer().primaryKey({ autoIncrement: true }), id: integer().primaryKey({ autoIncrement: true }),
userId: integer('user_id').notNull().references(() => users.id).unique(), userId: integer('user_id').notNull().references(() => users.id).unique(),
bio: text('bio'), bio: text('bio'),
dateOfBirth: integer('date_of_birth', { mode: 'timestamp' }), dateOfBirth: timestampText('date_of_birth'),
gender: text('gender'), gender: text('gender'),
occupation: text('occupation'), occupation: text('occupation'),
profileImage: text('profile_image'), profileImage: text('profile_image'),
isSuspended: integer('is_suspended', { mode: 'boolean' }).notNull().default(false), isSuspended: integer('is_suspended', { mode: 'boolean' }).notNull().default(false),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(), createdAt: timestampText('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().defaultNow(), updatedAt: timestampText('updated_at').notNull().default(sql`CURRENT_TIMESTAMP`),
}) })
export const userCreds = sqliteTable('user_creds', { export const userCreds = sqliteTable('user_creds', {
id: integer().primaryKey({ autoIncrement: true }), id: integer().primaryKey({ autoIncrement: true }),
userId: integer('user_id').notNull().references(() => users.id), userId: integer('user_id').notNull().references(() => users.id),
userPassword: text('user_password').notNull(), userPassword: text('user_password').notNull(),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(), createdAt: timestampText('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
}) })
export const addressZones = sqliteTable('address_zones', { export const addressZones = sqliteTable('address_zones', {
id: integer().primaryKey({ autoIncrement: true }), id: integer().primaryKey({ autoIncrement: true }),
zoneName: text('zone_name').notNull(), zoneName: text('zone_name').notNull(),
addedAt: integer('added_at', { mode: 'timestamp' }).notNull().defaultNow(), addedAt: timestampText('added_at').notNull().default(sql`CURRENT_TIMESTAMP`),
}) })
export const addressAreas = sqliteTable('address_areas', { export const addressAreas = sqliteTable('address_areas', {
id: integer().primaryKey({ autoIncrement: true }), id: integer().primaryKey({ autoIncrement: true }),
placeName: text('place_name').notNull(), placeName: text('place_name').notNull(),
zoneId: integer('zone_id').references(() => addressZones.id), zoneId: integer('zone_id').references(() => addressZones.id),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(), createdAt: timestampText('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
}) })
export const addresses = sqliteTable('addresses', { export const addresses = sqliteTable('addresses', {
@ -114,13 +131,13 @@ export const addresses = sqliteTable('addresses', {
adminLatitude: real('admin_latitude'), adminLatitude: real('admin_latitude'),
adminLongitude: real('admin_longitude'), adminLongitude: real('admin_longitude'),
zoneId: integer('zone_id').references(() => addressZones.id), zoneId: integer('zone_id').references(() => addressZones.id),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(), createdAt: timestampText('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
}) })
export const staffRoles = sqliteTable('staff_roles', { export const staffRoles = sqliteTable('staff_roles', {
id: integer().primaryKey({ autoIncrement: true }), id: integer().primaryKey({ autoIncrement: true }),
roleName: staffRoleEnum('role_name').notNull(), roleName: staffRoleEnum('role_name').notNull(),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(), createdAt: timestampText('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
}, (t) => ({ }, (t) => ({
unq_role_name: uniqueIndex('unique_role_name').on(t.roleName), unq_role_name: uniqueIndex('unique_role_name').on(t.roleName),
})) }))
@ -128,7 +145,7 @@ export const staffRoles = sqliteTable('staff_roles', {
export const staffPermissions = sqliteTable('staff_permissions', { export const staffPermissions = sqliteTable('staff_permissions', {
id: integer().primaryKey({ autoIncrement: true }), id: integer().primaryKey({ autoIncrement: true }),
permissionName: staffPermissionEnum('permission_name').notNull(), permissionName: staffPermissionEnum('permission_name').notNull(),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(), createdAt: timestampText('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
}, (t) => ({ }, (t) => ({
unq_permission_name: uniqueIndex('unique_permission_name').on(t.permissionName), unq_permission_name: uniqueIndex('unique_permission_name').on(t.permissionName),
})) }))
@ -137,7 +154,7 @@ export const staffRolePermissions = sqliteTable('staff_role_permissions', {
id: integer().primaryKey({ autoIncrement: true }), id: integer().primaryKey({ autoIncrement: true }),
staffRoleId: integer('staff_role_id').notNull().references(() => staffRoles.id), staffRoleId: integer('staff_role_id').notNull().references(() => staffRoles.id),
staffPermissionId: integer('staff_permission_id').notNull().references(() => staffPermissions.id), staffPermissionId: integer('staff_permission_id').notNull().references(() => staffPermissions.id),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(), createdAt: timestampText('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
}, (t) => ({ }, (t) => ({
unq_role_permission: uniqueIndex('unique_role_permission').on(t.staffRoleId, t.staffPermissionId), unq_role_permission: uniqueIndex('unique_role_permission').on(t.staffRoleId, t.staffPermissionId),
})) }))
@ -147,7 +164,7 @@ export const staffUsers = sqliteTable('staff_users', {
name: text().notNull(), name: text().notNull(),
password: text().notNull(), password: text().notNull(),
staffRoleId: integer('staff_role_id').references(() => staffRoles.id), staffRoleId: integer('staff_role_id').references(() => staffRoles.id),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(), createdAt: timestampText('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
}) })
export const storeInfo = sqliteTable('store_info', { export const storeInfo = sqliteTable('store_info', {
@ -155,7 +172,7 @@ export const storeInfo = sqliteTable('store_info', {
name: text().notNull(), name: text().notNull(),
description: text(), description: text(),
imageUrl: text('image_url'), imageUrl: text('image_url'),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(), createdAt: timestampText('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
owner: integer('owner').notNull().references(() => staffUsers.id), owner: integer('owner').notNull().references(() => staffUsers.id),
}) })
@ -180,7 +197,7 @@ export const productInfo = sqliteTable('product_info', {
isSuspended: integer('is_suspended', { mode: 'boolean' }).notNull().default(false), isSuspended: integer('is_suspended', { mode: 'boolean' }).notNull().default(false),
isFlashAvailable: integer('is_flash_available', { mode: 'boolean' }).notNull().default(false), isFlashAvailable: integer('is_flash_available', { mode: 'boolean' }).notNull().default(false),
flashPrice: numericText('flash_price'), flashPrice: numericText('flash_price'),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(), createdAt: timestampText('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
incrementStep: real('increment_step').notNull().default(1), incrementStep: real('increment_step').notNull().default(1),
productQuantity: real('product_quantity').notNull().default(1), productQuantity: real('product_quantity').notNull().default(1),
storeId: integer('store_id').references(() => storeInfo.id), storeId: integer('store_id').references(() => storeInfo.id),
@ -190,13 +207,13 @@ export const productGroupInfo = sqliteTable('product_group_info', {
id: integer().primaryKey({ autoIncrement: true }), id: integer().primaryKey({ autoIncrement: true }),
groupName: text('group_name').notNull(), groupName: text('group_name').notNull(),
description: text(), description: text(),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(), createdAt: timestampText('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
}) })
export const productGroupMembership = sqliteTable('product_group_membership', { export const productGroupMembership = sqliteTable('product_group_membership', {
productId: integer('product_id').notNull().references(() => productInfo.id), productId: integer('product_id').notNull().references(() => productInfo.id),
groupId: integer('group_id').notNull().references(() => productGroupInfo.id), groupId: integer('group_id').notNull().references(() => productGroupInfo.id),
addedAt: integer('added_at', { mode: 'timestamp' }).notNull().defaultNow(), addedAt: timestampText('added_at').notNull().default(sql`CURRENT_TIMESTAMP`),
}, (t) => ({ }, (t) => ({
pk: primaryKey({ columns: [t.productId, t.groupId], name: 'product_group_membership_pk' }), pk: primaryKey({ columns: [t.productId, t.groupId], name: 'product_group_membership_pk' }),
})) }))
@ -210,8 +227,8 @@ export const homeBanners = sqliteTable('home_banners', {
redirectUrl: text('redirect_url'), redirectUrl: text('redirect_url'),
serialNum: integer('serial_num'), serialNum: integer('serial_num'),
isActive: integer('is_active', { mode: 'boolean' }).notNull().default(false), isActive: integer('is_active', { mode: 'boolean' }).notNull().default(false),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(), createdAt: timestampText('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
lastUpdated: integer('last_updated', { mode: 'timestamp' }).notNull().defaultNow(), lastUpdated: timestampText('last_updated').notNull().default(sql`CURRENT_TIMESTAMP`),
}) })
export const productReviews = sqliteTable('product_reviews', { export const productReviews = sqliteTable('product_reviews', {
@ -220,7 +237,7 @@ export const productReviews = sqliteTable('product_reviews', {
productId: integer('product_id').notNull().references(() => productInfo.id), productId: integer('product_id').notNull().references(() => productInfo.id),
reviewBody: text('review_body').notNull(), reviewBody: text('review_body').notNull(),
imageUrls: jsonText<string[]>('image_urls').$defaultFn(() => []), imageUrls: jsonText<string[]>('image_urls').$defaultFn(() => []),
reviewTime: integer('review_time', { mode: 'timestamp' }).notNull().defaultNow(), reviewTime: timestampText('review_time').notNull().default(sql`CURRENT_TIMESTAMP`),
ratings: real('ratings').notNull(), ratings: real('ratings').notNull(),
adminResponse: text('admin_response'), adminResponse: text('admin_response'),
adminResponseImages: jsonText<string[]>('admin_response_images').$defaultFn(() => []), adminResponseImages: jsonText<string[]>('admin_response_images').$defaultFn(() => []),
@ -230,7 +247,7 @@ export const productReviews = sqliteTable('product_reviews', {
export const uploadUrlStatus = sqliteTable('upload_url_status', { export const uploadUrlStatus = sqliteTable('upload_url_status', {
id: integer().primaryKey({ autoIncrement: true }), id: integer().primaryKey({ autoIncrement: true }),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(), createdAt: timestampText('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
key: text('key').notNull(), key: text('key').notNull(),
status: uploadStatusEnum('status').notNull().default('pending'), status: uploadStatusEnum('status').notNull().default('pending'),
}) })
@ -242,22 +259,22 @@ export const productTagInfo = sqliteTable('product_tag_info', {
imageUrl: text('image_url'), imageUrl: text('image_url'),
isDashboardTag: integer('is_dashboard_tag', { mode: 'boolean' }).notNull().default(false), isDashboardTag: integer('is_dashboard_tag', { mode: 'boolean' }).notNull().default(false),
relatedStores: jsonText<number[]>('related_stores').$defaultFn(() => []), relatedStores: jsonText<number[]>('related_stores').$defaultFn(() => []),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(), createdAt: timestampText('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
}) })
export const productTags = sqliteTable('product_tags', { export const productTags = sqliteTable('product_tags', {
id: integer().primaryKey({ autoIncrement: true }), id: integer().primaryKey({ autoIncrement: true }),
productId: integer('product_id').notNull().references(() => productInfo.id), productId: integer('product_id').notNull().references(() => productInfo.id),
tagId: integer('tag_id').notNull().references(() => productTagInfo.id), tagId: integer('tag_id').notNull().references(() => productTagInfo.id),
assignedAt: integer('assigned_at', { mode: 'timestamp' }).notNull().defaultNow(), assignedAt: timestampText('assigned_at').notNull().default(sql`CURRENT_TIMESTAMP`),
}, (t) => ({ }, (t) => ({
unq_product_tag: uniqueIndex('unique_product_tag').on(t.productId, t.tagId), unq_product_tag: uniqueIndex('unique_product_tag').on(t.productId, t.tagId),
})) }))
export const deliverySlotInfo = sqliteTable('delivery_slot_info', { export const deliverySlotInfo = sqliteTable('delivery_slot_info', {
id: integer().primaryKey({ autoIncrement: true }), id: integer().primaryKey({ autoIncrement: true }),
deliveryTime: integer('delivery_time', { mode: 'timestamp' }).notNull(), deliveryTime: timestampText('delivery_time').notNull(),
freezeTime: integer('freeze_time', { mode: 'timestamp' }).notNull(), freezeTime: timestampText('freeze_time').notNull(),
isActive: integer('is_active', { mode: 'boolean' }).notNull().default(true), isActive: integer('is_active', { mode: 'boolean' }).notNull().default(true),
isFlash: integer('is_flash', { mode: 'boolean' }).notNull().default(false), isFlash: integer('is_flash', { mode: 'boolean' }).notNull().default(false),
isCapacityFull: integer('is_capacity_full', { mode: 'boolean' }).notNull().default(false), isCapacityFull: integer('is_capacity_full', { mode: 'boolean' }).notNull().default(false),
@ -271,8 +288,8 @@ export const vendorSnippets = sqliteTable('vendor_snippets', {
slotId: integer('slot_id').references(() => deliverySlotInfo.id), slotId: integer('slot_id').references(() => deliverySlotInfo.id),
isPermanent: integer('is_permanent', { mode: 'boolean' }).notNull().default(false), isPermanent: integer('is_permanent', { mode: 'boolean' }).notNull().default(false),
productIds: jsonText<number[]>('product_ids').notNull(), productIds: jsonText<number[]>('product_ids').notNull(),
validTill: integer('valid_till', { mode: 'timestamp' }), validTill: timestampText('valid_till'),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(), createdAt: timestampText('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
}) })
export const productSlots = sqliteTable('product_slots', { export const productSlots = sqliteTable('product_slots', {
@ -287,7 +304,7 @@ export const specialDeals = sqliteTable('special_deals', {
productId: integer('product_id').notNull().references(() => productInfo.id), productId: integer('product_id').notNull().references(() => productInfo.id),
quantity: numericText('quantity').notNull(), quantity: numericText('quantity').notNull(),
price: numericText('price').notNull(), price: numericText('price').notNull(),
validTill: integer('valid_till', { mode: 'timestamp' }).notNull(), validTill: timestampText('valid_till').notNull(),
}) })
export const paymentInfoTable = sqliteTable('payment_info', { export const paymentInfoTable = sqliteTable('payment_info', {
@ -316,7 +333,7 @@ export const orders = sqliteTable('orders', {
orderGroupId: text('order_group_id'), orderGroupId: text('order_group_id'),
orderGroupProportion: numericText('order_group_proportion'), orderGroupProportion: numericText('order_group_proportion'),
isFlashDelivery: integer('is_flash_delivery', { mode: 'boolean' }).notNull().default(false), isFlashDelivery: integer('is_flash_delivery', { mode: 'boolean' }).notNull().default(false),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(), createdAt: timestampText('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
}) })
export const orderItems = sqliteTable('order_items', { export const orderItems = sqliteTable('order_items', {
@ -332,7 +349,7 @@ export const orderItems = sqliteTable('order_items', {
export const orderStatus = sqliteTable('order_status', { export const orderStatus = sqliteTable('order_status', {
id: integer().primaryKey({ autoIncrement: true }), id: integer().primaryKey({ autoIncrement: true }),
orderTime: integer('order_time', { mode: 'timestamp' }).notNull().defaultNow(), orderTime: timestampText('order_time').notNull().default(sql`CURRENT_TIMESTAMP`),
userId: integer('user_id').notNull().references(() => users.id), userId: integer('user_id').notNull().references(() => users.id),
orderId: integer('order_id').notNull().references(() => orders.id), orderId: integer('order_id').notNull().references(() => orders.id),
isPackaged: integer('is_packaged', { mode: 'boolean' }).notNull().default(false), isPackaged: integer('is_packaged', { mode: 'boolean' }).notNull().default(false),
@ -344,7 +361,7 @@ export const orderStatus = sqliteTable('order_status', {
cancellationUserNotes: text('cancellation_user_notes'), cancellationUserNotes: text('cancellation_user_notes'),
cancellationAdminNotes: text('cancellation_admin_notes'), cancellationAdminNotes: text('cancellation_admin_notes'),
cancellationReviewed: integer('cancellation_reviewed', { mode: 'boolean' }).notNull().default(false), cancellationReviewed: integer('cancellation_reviewed', { mode: 'boolean' }).notNull().default(false),
cancellationReviewedAt: integer('cancellation_reviewed_at', { mode: 'timestamp' }), cancellationReviewedAt: timestampText('cancellation_reviewed_at'),
refundCouponId: integer('refund_coupon_id').references(() => coupons.id), refundCouponId: integer('refund_coupon_id').references(() => coupons.id),
}) })
@ -364,8 +381,8 @@ export const refunds = sqliteTable('refunds', {
refundAmount: numericText('refund_amount'), refundAmount: numericText('refund_amount'),
refundStatus: text('refund_status').default('none'), refundStatus: text('refund_status').default('none'),
merchantRefundId: text('merchant_refund_id'), merchantRefundId: text('merchant_refund_id'),
refundProcessedAt: integer('refund_processed_at', { mode: 'timestamp' }), refundProcessedAt: timestampText('refund_processed_at'),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(), createdAt: timestampText('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
}) })
export const keyValStore = sqliteTable('key_val_store', { export const keyValStore = sqliteTable('key_val_store', {
@ -380,7 +397,7 @@ export const notifications = sqliteTable('notifications', {
body: text().notNull(), body: text().notNull(),
type: text(), type: text(),
isRead: integer('is_read', { mode: 'boolean' }).notNull().default(false), isRead: integer('is_read', { mode: 'boolean' }).notNull().default(false),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(), createdAt: timestampText('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
}) })
export const productCategories = sqliteTable('product_categories', { export const productCategories = sqliteTable('product_categories', {
@ -394,7 +411,7 @@ export const cartItems = sqliteTable('cart_items', {
userId: integer('user_id').notNull().references(() => users.id), userId: integer('user_id').notNull().references(() => users.id),
productId: integer('product_id').notNull().references(() => productInfo.id), productId: integer('product_id').notNull().references(() => productInfo.id),
quantity: numericText('quantity').notNull(), quantity: numericText('quantity').notNull(),
addedAt: integer('added_at', { mode: 'timestamp' }).notNull().defaultNow(), addedAt: timestampText('added_at').notNull().default(sql`CURRENT_TIMESTAMP`),
}, (t) => ({ }, (t) => ({
unq_user_product: uniqueIndex('unique_user_product').on(t.userId, t.productId), unq_user_product: uniqueIndex('unique_user_product').on(t.userId, t.productId),
})) }))
@ -407,7 +424,7 @@ export const complaints = sqliteTable('complaints', {
images: jsonText<string[] | null>('images'), images: jsonText<string[] | null>('images'),
response: text('response'), response: text('response'),
isResolved: integer('is_resolved', { mode: 'boolean' }).notNull().default(false), isResolved: integer('is_resolved', { mode: 'boolean' }).notNull().default(false),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(), createdAt: timestampText('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
}) })
export const coupons = sqliteTable('coupons', { export const coupons = sqliteTable('coupons', {
@ -421,11 +438,11 @@ export const coupons = sqliteTable('coupons', {
createdBy: integer('created_by').notNull().references(() => staffUsers.id), createdBy: integer('created_by').notNull().references(() => staffUsers.id),
maxValue: numericText('max_value'), maxValue: numericText('max_value'),
isApplyForAll: integer('is_apply_for_all', { mode: 'boolean' }).notNull().default(false), isApplyForAll: integer('is_apply_for_all', { mode: 'boolean' }).notNull().default(false),
validTill: integer('valid_till', { mode: 'timestamp' }), validTill: timestampText('valid_till'),
maxLimitForUser: integer('max_limit_for_user'), maxLimitForUser: integer('max_limit_for_user'),
isInvalidated: integer('is_invalidated', { mode: 'boolean' }).notNull().default(false), isInvalidated: integer('is_invalidated', { mode: 'boolean' }).notNull().default(false),
exclusiveApply: integer('exclusive_apply', { mode: 'boolean' }).notNull().default(false), exclusiveApply: integer('exclusive_apply', { mode: 'boolean' }).notNull().default(false),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(), createdAt: timestampText('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
}) })
export const couponUsage = sqliteTable('coupon_usage', { export const couponUsage = sqliteTable('coupon_usage', {
@ -434,7 +451,7 @@ export const couponUsage = sqliteTable('coupon_usage', {
couponId: integer('coupon_id').notNull().references(() => coupons.id), couponId: integer('coupon_id').notNull().references(() => coupons.id),
orderId: integer('order_id').references(() => orders.id), orderId: integer('order_id').references(() => orders.id),
orderItemId: integer('order_item_id').references(() => orderItems.id), orderItemId: integer('order_item_id').references(() => orderItems.id),
usedAt: integer('used_at', { mode: 'timestamp' }).notNull().defaultNow(), usedAt: timestampText('used_at').notNull().default(sql`CURRENT_TIMESTAMP`),
}) })
export const couponApplicableUsers = sqliteTable('coupon_applicable_users', { export const couponApplicableUsers = sqliteTable('coupon_applicable_users', {
@ -457,7 +474,7 @@ export const userIncidents = sqliteTable('user_incidents', {
id: integer().primaryKey({ autoIncrement: true }), id: integer().primaryKey({ autoIncrement: true }),
userId: integer('user_id').notNull().references(() => users.id), userId: integer('user_id').notNull().references(() => users.id),
orderId: integer('order_id').references(() => orders.id), orderId: integer('order_id').references(() => orders.id),
dateAdded: integer('date_added', { mode: 'timestamp' }).notNull().defaultNow(), dateAdded: timestampText('date_added').notNull().default(sql`CURRENT_TIMESTAMP`),
adminComment: text('admin_comment'), adminComment: text('admin_comment'),
addedBy: integer('added_by').references(() => staffUsers.id), addedBy: integer('added_by').references(() => staffUsers.id),
negativityScore: integer('negativity_score'), negativityScore: integer('negativity_score'),
@ -472,36 +489,36 @@ export const reservedCoupons = sqliteTable('reserved_coupons', {
minOrder: numericText('min_order'), minOrder: numericText('min_order'),
productIds: jsonText<number[] | null>('product_ids'), productIds: jsonText<number[] | null>('product_ids'),
maxValue: numericText('max_value'), maxValue: numericText('max_value'),
validTill: integer('valid_till', { mode: 'timestamp' }), validTill: timestampText('valid_till'),
maxLimitForUser: integer('max_limit_for_user'), maxLimitForUser: integer('max_limit_for_user'),
exclusiveApply: integer('exclusive_apply', { mode: 'boolean' }).notNull().default(false), exclusiveApply: integer('exclusive_apply', { mode: 'boolean' }).notNull().default(false),
isRedeemed: integer('is_redeemed', { mode: 'boolean' }).notNull().default(false), isRedeemed: integer('is_redeemed', { mode: 'boolean' }).notNull().default(false),
redeemedBy: integer('redeemed_by').references(() => users.id), redeemedBy: integer('redeemed_by').references(() => users.id),
redeemedAt: integer('redeemed_at', { mode: 'timestamp' }), redeemedAt: timestampText('redeemed_at'),
createdBy: integer('created_by').notNull().references(() => staffUsers.id), createdBy: integer('created_by').notNull().references(() => staffUsers.id),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(), createdAt: timestampText('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
}) })
export const notifCreds = sqliteTable('notif_creds', { export const notifCreds = sqliteTable('notif_creds', {
id: integer().primaryKey({ autoIncrement: true }), id: integer().primaryKey({ autoIncrement: true }),
token: text().notNull().unique(), token: text().notNull().unique(),
addedAt: integer('added_at', { mode: 'timestamp' }).notNull().defaultNow(), addedAt: timestampText('added_at').notNull().default(sql`CURRENT_TIMESTAMP`),
userId: integer('user_id').notNull().references(() => users.id), userId: integer('user_id').notNull().references(() => users.id),
lastVerified: integer('last_verified', { mode: 'timestamp' }), lastVerified: timestampText('last_verified'),
}) })
export const unloggedUserTokens = sqliteTable('unlogged_user_tokens', { export const unloggedUserTokens = sqliteTable('unlogged_user_tokens', {
id: integer().primaryKey({ autoIncrement: true }), id: integer().primaryKey({ autoIncrement: true }),
token: text().notNull().unique(), token: text().notNull().unique(),
addedAt: integer('added_at', { mode: 'timestamp' }).notNull().defaultNow(), addedAt: timestampText('added_at').notNull().default(sql`CURRENT_TIMESTAMP`),
lastVerified: integer('last_verified', { mode: 'timestamp' }), lastVerified: timestampText('last_verified'),
}) })
export const userNotifications = sqliteTable('user_notifications', { export const userNotifications = sqliteTable('user_notifications', {
id: integer().primaryKey({ autoIncrement: true }), id: integer().primaryKey({ autoIncrement: true }),
title: text('title').notNull(), title: text('title').notNull(),
imageUrl: text('image_url'), imageUrl: text('image_url'),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(), createdAt: timestampText('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
body: text('body').notNull(), body: text('body').notNull(),
applicableUsers: jsonText<number[] | null>('applicable_users'), applicableUsers: jsonText<number[] | null>('applicable_users'),
}) })

View file

@ -7,21 +7,59 @@ export function coerceDate(value: unknown): Date | null {
return null return null
} }
if (typeof value === 'number') { const fromNumber = (raw: number): Date | null => {
const date = new Date(value) if (Number.isNaN(raw)) {
return null
}
const ms = raw < 1e12 ? raw * 1000 : raw
const date = new Date(ms)
return Number.isNaN(date.getTime()) ? null : date return Number.isNaN(date.getTime()) ? null : date
} }
if (typeof value === 'number') {
return fromNumber(value)
}
if (typeof value === 'bigint') {
return fromNumber(Number(value))
}
if (typeof value === 'string') { if (typeof value === 'string') {
const parsed = Date.parse(value) const trimmed = value.trim()
if (!trimmed) {
return null
}
const numeric = Number(trimmed)
if (!Number.isNaN(numeric)) {
return fromNumber(numeric)
}
const parsed = Date.parse(trimmed)
if (!Number.isNaN(parsed)) { if (!Number.isNaN(parsed)) {
return new Date(parsed) return new Date(parsed)
} }
}
const asNumber = Number(value) if (typeof value === 'object') {
if (!Number.isNaN(asNumber)) { const record = value as { value?: unknown; valueOf?: () => unknown; toString?: () => string }
const date = new Date(asNumber) if (record.value !== undefined) {
return Number.isNaN(date.getTime()) ? null : date return coerceDate(record.value)
}
if (record.valueOf) {
const valueOf = record.valueOf()
if (valueOf !== value) {
return coerceDate(valueOf)
}
}
if (record.toString) {
const asString = record.toString()
if (asString && asString !== '[object Object]') {
return coerceDate(asString)
}
} }
} }

View file

@ -692,8 +692,10 @@ export interface OrderWithFullData {
export async function getOrdersByIdsWithFullData( export async function getOrdersByIdsWithFullData(
orderIds: number[] orderIds: number[]
): Promise<OrderWithFullData[]> { ): Promise<OrderWithFullData[]> {
return db.query.orders.findMany({ console.log('getting orders byid')
where: inArray(orders.id, orderIds),
const ordersResp = await db.query.orders.findMany({
where: inArray(orders.id, orderIds),
with: { with: {
address: { address: {
columns: { columns: {
@ -721,7 +723,10 @@ export async function getOrdersByIdsWithFullData(
}, },
}, },
}, },
}) as Promise<OrderWithFullData[]> })
// as Promise<OrderWithFullData[]>
return ordersResp as OrderWithFullData[];
} }
export interface OrderWithCancellationData extends OrderWithFullData { export interface OrderWithCancellationData extends OrderWithFullData {