enh
This commit is contained in:
parent
7432f8dfd5
commit
b86fa8a2e0
49 changed files with 26899 additions and 26208 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 => ({
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -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 it is too large
Load diff
8
apps/backend/.wrangler/tmp/dev-IIUlyx/worker.js.map
Normal file
8
apps/backend/.wrangler/tmp/dev-IIUlyx/worker.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
43
apps/backend/src/jobs/cache-creator.ts
Normal file
43
apps/backend/src/jobs/cache-creator.ts
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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'] }
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
53
apps/backend/src/lib/queue-consumer.ts
Normal file
53
apps/backend/src/lib/queue-consumer.ts
Normal 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
51
apps/backend/src/lib/queue-data-pusher.ts
Normal file
51
apps/backend/src/lib/queue-data-pusher.ts
Normal 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()
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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: [] },
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}`,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
BIN
apps/testdb.sqlite
Normal file
Binary file not shown.
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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'),
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue