This commit is contained in:
shafi54 2026-03-26 17:16:56 +05:30
parent 5e9bc3e38e
commit ca7d8df1c8
89 changed files with 10704 additions and 1148 deletions

View file

@ -1,14 +1,32 @@
import React from 'react';
import { Alert } from 'react-native';
import { AppContainer } from 'common-ui';
import { AppContainer, ImageUploaderNeoPayload } from 'common-ui';
import ProductForm from '@/src/components/ProductForm';
import { useCreateProduct, CreateProductPayload } from '@/src/api-hooks/product.api';
import { trpc } from '@/src/trpc-client';
import { useUploadToObjectStorage } from '@/hooks/useUploadToObjectStore';
export default function AddProduct() {
const { mutate: createProduct, isPending: isCreating } = useCreateProduct();
const createProduct = trpc.admin.product.createProduct.useMutation();
const { upload, isUploading } = useUploadToObjectStorage();
const handleSubmit = (values: any, images?: { uri?: string, mimeType?: string }[]) => {
const payload: CreateProductPayload = {
const handleSubmit = async (values: any, images: ImageUploaderNeoPayload[]) => {
try {
let uploadUrls: string[] = [];
if (images.length > 0) {
const blobs = await Promise.all(
images.map(async (img) => {
const response = await fetch(img.url);
const blob = await response.blob();
return { blob, mimeType: img.mimeType || 'image/jpeg' };
})
);
const result = await upload({ images: blobs, contextString: 'product_info' });
uploadUrls = result.presignedUrls;
}
await createProduct.mutateAsync({
name: values.name,
shortDescription: values.shortDescription,
longDescription: values.longDescription,
@ -18,45 +36,17 @@ export default function AddProduct() {
marketPrice: values.marketPrice ? parseFloat(values.marketPrice) : undefined,
incrementStep: 1,
productQuantity: values.productQuantity || 1,
};
const formData = new FormData();
Object.entries(payload).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
formData.append(key, value as string);
}
isSuspended: values.isSuspended || false,
isFlashAvailable: values.isFlashAvailable || false,
flashPrice: values.flashPrice ? parseFloat(values.flashPrice) : undefined,
uploadUrls,
tagIds: values.tagIds || [],
});
// Append tag IDs
if (values.tagIds && values.tagIds.length > 0) {
values.tagIds.forEach((tagId: number) => {
formData.append('tagIds', tagId.toString());
});
}
// Append images
if (images) {
images.forEach((image, index) => {
if (image.uri) {
formData.append('images', {
uri: image.uri,
name: `image-${index}.jpg`,
// type: 'image/jpeg',
type: image.mimeType as any,
} as any);
}
});
}
createProduct(formData, {
onSuccess: (data) => {
Alert.alert('Success', 'Product created successfully!');
// Reset form or navigate
},
onError: (error: any) => {
} catch (error: any) {
Alert.alert('Error', error.message || 'Failed to create product');
},
});
}
};
const initialValues = {
@ -81,8 +71,7 @@ export default function AddProduct() {
mode="create"
initialValues={initialValues}
onSubmit={handleSubmit}
isLoading={isCreating}
existingImages={[]}
isLoading={createProduct.isPending || isUploading}
/>
</AppContainer>
);

View file

@ -6,6 +6,7 @@ import { tw, AppContainer, MyText, useMarkDataFetchers, BottomDialog, ImageUploa
import { MaterialIcons, FontAwesome5, Ionicons, Feather, MaterialCommunityIcons } from '@expo/vector-icons';
import { trpc } from '@/src/trpc-client';
import usePickImage from 'common-ui/src/components/use-pick-image';
import { useUploadToObjectStorage } from '@/hooks/useUploadToObjectStore';
import { Formik } from 'formik';
import { LinearGradient } from 'expo-linear-gradient';
import { BlurView } from 'expo-blur';
@ -23,10 +24,9 @@ const ReviewResponseForm: React.FC<ReviewResponseFormProps> = ({ reviewId, onClo
const [adminResponse, setAdminResponse] = useState('');
const [selectedImages, setSelectedImages] = useState<{ blob: Blob; mimeType: string }[]>([]);
const [displayImages, setDisplayImages] = useState<{ uri?: string }[]>([]);
const [uploadUrls, setUploadUrls] = useState<string[]>([]);
const respondToReview = trpc.admin.product.respondToReview.useMutation();
const generateUploadUrls = trpc.user.fileUpload.generateUploadUrls.useMutation();
const { upload } = useUploadToObjectStorage();
const handleImagePick = usePickImage({
setFile: async (assets: any) => {
@ -62,37 +62,16 @@ const ReviewResponseForm: React.FC<ReviewResponseFormProps> = ({ reviewId, onClo
const handleSubmit = async (adminResponse: string) => {
try {
const mimeTypes = selectedImages.map(s => s.mimeType);
const { uploadUrls: generatedUrls } = await generateUploadUrls.mutateAsync({
const { keys, presignedUrls } = await upload({
images: selectedImages,
contextString: 'review',
mimeTypes,
});
const keys = generatedUrls.map(url => {
const u = new URL(url);
const rawKey = u.pathname.replace(/^\/+/, "");
const decodedKey = decodeURIComponent(rawKey);
const parts = decodedKey.split('/');
parts.shift();
return parts.join('/');
});
setUploadUrls(generatedUrls);
for (let i = 0; i < generatedUrls.length; i++) {
const uploadUrl = generatedUrls[i];
const { blob, mimeType } = selectedImages[i];
const uploadResponse = await fetch(uploadUrl, {
method: 'PUT',
body: blob,
headers: { 'Content-Type': mimeType },
});
if (!uploadResponse.ok) throw new Error(`Upload failed with status ${uploadResponse.status}`);
}
await respondToReview.mutateAsync({
reviewId,
adminResponse,
adminResponseImages: keys,
uploadUrls: generatedUrls,
uploadUrls: presignedUrls,
});
Alert.alert('Success', 'Response submitted');
@ -100,9 +79,7 @@ const ReviewResponseForm: React.FC<ReviewResponseFormProps> = ({ reviewId, onClo
setAdminResponse('');
setSelectedImages([]);
setDisplayImages([]);
setUploadUrls([]);
} catch (error:any) {
Alert.alert('Error', error.message || 'Failed to submit response.');
}
};

View file

@ -1,28 +1,47 @@
import React, { useRef } from 'react';
import { View, Text, Alert } from 'react-native';
import { View, Alert } from 'react-native';
import { useLocalSearchParams } from 'expo-router';
import { AppContainer, useManualRefresh, MyText, tw } from 'common-ui';
import { AppContainer, useManualRefresh, MyText, tw, ImageUploaderNeoItem, ImageUploaderNeoPayload } from 'common-ui';
import ProductForm, { ProductFormRef } from '@/src/components/ProductForm';
import { useUpdateProduct } from '@/src/api-hooks/product.api';
import { trpc } from '@/src/trpc-client';
import { useUploadToObjectStorage } from '@/hooks/useUploadToObjectStore';
export default function EditProduct() {
const { id } = useLocalSearchParams();
const productId = Number(id);
const productFormRef = useRef<ProductFormRef>(null);
// const { data: product, isLoading: isFetching, refetch } = useGetProduct(productId);
const { data: product, isLoading: isFetching, refetch } = trpc.admin.product.getProductById.useQuery(
{ id: productId },
{ enabled: !!productId }
);
//
const { mutate: updateProduct, isPending: isUpdating } = useUpdateProduct();
const updateProduct = trpc.admin.product.updateProduct.useMutation();
const { upload, isUploading } = useUploadToObjectStorage();
useManualRefresh(() => refetch());
const handleSubmit = (values: any, newImages?: { uri?: string }[], imagesToDelete?: string[]) => {
const payload = {
const handleSubmit = async (values: any, images: ImageUploaderNeoPayload[], imagesToDelete: string[]) => {
try {
// New images have mimeType !== null, existing images have mimeType === null
const newImages = images.filter(img => img.mimeType !== null);
let uploadUrls: string[] = [];
if (newImages.length > 0) {
const blobs = await Promise.all(
newImages.map(async (img) => {
const response = await fetch(img.url);
const blob = await response.blob();
return { blob, mimeType: img.mimeType || 'image/jpeg' };
})
);
const result = await upload({ images: blobs, contextString: 'product_info' });
uploadUrls = result.presignedUrls;
}
await updateProduct.mutateAsync({
id: productId,
name: values.name,
shortDescription: values.shortDescription,
longDescription: values.longDescription,
@ -32,64 +51,19 @@ export default function EditProduct() {
marketPrice: values.marketPrice ? parseFloat(values.marketPrice) : undefined,
incrementStep: 1,
productQuantity: values.productQuantity || 1,
deals: values.deals?.filter((deal: any) =>
deal.quantity && deal.price && deal.validTill
).map((deal: any) => ({
quantity: parseInt(deal.quantity),
price: parseFloat(deal.price),
validTill: deal.validTill instanceof Date
? deal.validTill.toISOString().split('T')[0]
: deal.validTill, // Convert Date to YYYY-MM-DD string
})),
tagIds: values.tagIds,
};
const formData = new FormData();
Object.entries(payload).forEach(([key, value]) => {
if (key === 'deals' && Array.isArray(value)) {
formData.append(key, JSON.stringify(value));
} else if (key === 'tagIds' && Array.isArray(value)) {
value.forEach(tagId => {
formData.append('tagIds', tagId.toString());
});
} else if (value !== undefined && value !== null) {
formData.append(key, value as string);
}
isSuspended: values.isSuspended || false,
isFlashAvailable: values.isFlashAvailable || false,
flashPrice: values.flashPrice ? parseFloat(values.flashPrice) : null,
uploadUrls,
imagesToDelete,
tagIds: values.tagIds || [],
});
// Add new images
if (newImages && newImages.length > 0) {
newImages.forEach((image, index) => {
if (image.uri) {
const fileName = image.uri.split('/').pop() || `image_${index}.jpg`;
formData.append('images', {
uri: image.uri,
name: fileName,
type: 'image/jpeg',
} as any);
}
});
}
// Add images to delete
if (imagesToDelete && imagesToDelete.length > 0) {
formData.append('imagesToDelete', JSON.stringify(imagesToDelete));
}
updateProduct(
{ id: productId, formData },
{
onSuccess: (data) => {
Alert.alert('Success', 'Product updated successfully!');
// Clear newly added images after successful update
productFormRef.current?.clearImages();
},
onError: (error: any) => {
} catch (error: any) {
Alert.alert('Error', error.message || 'Failed to update product');
},
}
);
};
if (isFetching) {
@ -112,7 +86,13 @@ export default function EditProduct() {
);
}
const productData = product.product; // The API returns { product: Product }
const productData = product.product;
const existingImages: ImageUploaderNeoItem[] = (productData.images || []).map((url) => ({
imgUrl: url,
mimeType: null,
}));
const existingImageKeys = productData.imageKeys || [];
const initialValues = {
name: productData.name,
@ -125,7 +105,7 @@ export default function EditProduct() {
deals: productData.deals?.map(deal => ({
quantity: deal.quantity,
price: deal.price,
validTill: deal.validTill ? new Date(deal.validTill) : null, // Convert to Date object
validTill: deal.validTill ? new Date(deal.validTill) : null,
})) || [{ quantity: '', price: '', validTill: null }],
tagIds: productData.tags?.map((tag: any) => tag.id) || [],
isSuspended: productData.isSuspended || false,
@ -141,8 +121,9 @@ export default function EditProduct() {
mode="edit"
initialValues={initialValues}
onSubmit={handleSubmit}
isLoading={isUpdating}
existingImages={productData.images || []}
isLoading={updateProduct.isPending || isUploading}
existingImages={existingImages}
existingImageKeys={existingImageKeys}
/>
</AppContainer>
);

View file

@ -6,7 +6,7 @@ import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { AppContainer, MyText, tw, MyButton, useManualRefresh, MyTextInput, SearchBar, useMarkDataFetchers } from 'common-ui';
import { trpc } from '@/src/trpc-client';
import { Product } from '@/src/api-hooks/product.api';
import type { AdminProduct } from '@packages/shared';
type FilterType = 'all' | 'in-stock' | 'out-of-stock';
@ -54,7 +54,7 @@ export default function Products() {
// const handleToggleStock = (product: any) => {
const handleToggleStock = (product: Pick<Product, 'id' | 'name' | 'isOutOfStock'>) => {
const handleToggleStock = (product: Pick<AdminProduct, 'id' | 'name' | 'isOutOfStock'>) => {
const action = product.isOutOfStock ? 'mark as in stock' : 'mark as out of stock';
Alert.alert(
'Update Stock Status',

View file

@ -18,6 +18,7 @@ import {
} from 'common-ui';
import { trpc } from '@/src/trpc-client';
import usePickImage from 'common-ui/src/components/use-pick-image';
import { useUploadToObjectStorage } from '@/hooks/useUploadToObjectStore';
interface User {
id: number;
@ -26,12 +27,6 @@ interface User {
isEligibleForNotif: boolean;
}
const extractKeyFromUrl = (url: string): string => {
const u = new URL(url);
const rawKey = u.pathname.replace(/^\/+/, '');
return decodeURIComponent(rawKey);
};
export default function SendNotifications() {
const router = useRouter();
const [selectedUserIds, setSelectedUserIds] = useState<number[]>([]);
@ -46,8 +41,7 @@ export default function SendNotifications() {
search: searchQuery,
});
// Generate upload URLs mutation
const generateUploadUrls = trpc.user.fileUpload.generateUploadUrls.useMutation();
const { uploadSingle } = useUploadToObjectStorage();
// Send notification mutation
const sendNotification = trpc.admin.user.sendNotification.useMutation({
@ -127,28 +121,8 @@ export default function SendNotifications() {
// Upload image if selected
if (selectedImage) {
const { uploadUrls } = await generateUploadUrls.mutateAsync({
contextString: 'notification',
mimeTypes: [selectedImage.mimeType],
});
if (uploadUrls.length > 0) {
const uploadUrl = uploadUrls[0];
imageUrl = extractKeyFromUrl(uploadUrl);
// Upload image
const uploadResponse = await fetch(uploadUrl, {
method: 'PUT',
body: selectedImage.blob,
headers: {
'Content-Type': selectedImage.mimeType,
},
});
if (!uploadResponse.ok) {
throw new Error(`Upload failed with status ${uploadResponse.status}`);
}
}
const { key } = await uploadSingle(selectedImage.blob, selectedImage.mimeType, 'notification');
imageUrl = key;
}
// Send notification

View file

@ -7,6 +7,7 @@ import { DropdownOption } from 'common-ui/src/components/bottom-dropdown';
import ProductsSelector from './ProductsSelector';
import { trpc } from '../src/trpc-client';
import usePickImage from 'common-ui/src/components/use-pick-image';
import { useUploadToObjectStorage } from '../hooks/useUploadToObjectStore';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
export interface BannerFormData {
@ -52,10 +53,10 @@ export default function BannerForm({
const [selectedImages, setSelectedImages] = useState<{ blob: Blob; mimeType: string }[]>([]);
const [displayImages, setDisplayImages] = useState<{ uri?: string }[]>([]);
const generateUploadUrls = trpc.common.generateUploadUrls.useMutation();
const { uploadSingle } = useUploadToObjectStorage();
// Fetch products for dropdown
const { data: productsData } = trpc.common.product.getAllProductsSummary.useQuery({});
const { data: productsData } = trpc.common.product.getAllProductsSummary.useQuery();
const products = productsData?.products || [];
@ -97,33 +98,11 @@ export default function BannerForm({
let imageUrl: string | undefined;
if (selectedImages.length > 0) {
// Generate upload URLs
const mimeTypes = selectedImages.map(s => s.mimeType);
const { uploadUrls } = await generateUploadUrls.mutateAsync({
contextString: 'store', // Using 'store' for now
mimeTypes,
});
// Upload image
const uploadUrl = uploadUrls[0];
const { blob, mimeType } = selectedImages[0];
const uploadResponse = await fetch(uploadUrl, {
method: 'PUT',
body: blob,
headers: {
'Content-Type': mimeType,
},
});
if (!uploadResponse.ok) {
throw new Error(`Upload failed with status ${uploadResponse.status}`);
const { presignedUrl } = await uploadSingle(blob, mimeType, 'store');
imageUrl = presignedUrl;
}
imageUrl = uploadUrl;
}
// Call onSubmit with form values and imageUrl
await onSubmit(values, imageUrl);
} catch (error) {
console.error('Upload error:', error);

View file

@ -6,6 +6,7 @@ import { MyTextInput, BottomDropdown, MyText, tw, ImageUploader } from 'common-u
import ProductsSelector from './ProductsSelector';
import { trpc } from '../src/trpc-client';
import usePickImage from 'common-ui/src/components/use-pick-image';
import { useUploadToObjectStorage } from '../hooks/useUploadToObjectStore';
export interface StoreFormData {
name: string;
@ -66,7 +67,7 @@ const StoreForm = forwardRef<StoreFormRef, StoreFormProps>((props, ref) => {
const generateUploadUrls = trpc.common.generateUploadUrls.useMutation();
const { uploadSingle, isUploading } = useUploadToObjectStorage();
const handleImagePick = usePickImage({
setFile: async (assets: any) => {
@ -113,39 +114,11 @@ const StoreForm = forwardRef<StoreFormRef, StoreFormProps>((props, ref) => {
let imageUrl: string | undefined;
if (selectedImages.length > 0) {
// Generate upload URLs
const mimeTypes = selectedImages.map(s => s.mimeType);
const { uploadUrls } = await generateUploadUrls.mutateAsync({
contextString: 'store',
mimeTypes,
});
// Upload images
for (let i = 0; i < uploadUrls.length; i++) {
const uploadUrl = uploadUrls[i];
const { blob, mimeType } = selectedImages[i];
const uploadResponse = await fetch(uploadUrl, {
method: 'PUT',
body: blob,
headers: {
'Content-Type': mimeType,
},
});
if (!uploadResponse.ok) {
throw new Error(`Upload failed with status ${uploadResponse.status}`);
}
const { blob, mimeType } = selectedImages[0];
const { presignedUrl } = await uploadSingle(blob, mimeType, 'store');
imageUrl = presignedUrl;
}
// Extract key from first upload URL
// const u = new URL(uploadUrls[0]);
// const rawKey = u.pathname.replace(/^\/+/, "");
// imageUrl = decodeURIComponent(rawKey);
imageUrl = uploadUrls[0];
}
// Submit form with imageUrl
onSubmit({ ...values, imageUrl });
} catch (error) {
console.error('Upload error:', error);
@ -204,11 +177,11 @@ const StoreForm = forwardRef<StoreFormRef, StoreFormProps>((props, ref) => {
</View>
<TouchableOpacity
onPress={submit}
disabled={isLoading || generateUploadUrls.isPending}
style={tw`px-4 py-2 rounded-lg shadow-lg items-center mt-2 ${isLoading || generateUploadUrls.isPending ? 'bg-gray-400' : 'bg-blue-500'}`}
disabled={isLoading || isUploading}
style={tw`px-4 py-2 rounded-lg shadow-lg items-center mt-2 ${isLoading || isUploading ? 'bg-gray-400' : 'bg-blue-500'}`}
>
<MyText style={tw`text-white text-lg font-bold`}>
{generateUploadUrls.isPending ? 'Uploading...' : isLoading ? (mode === 'create' ? 'Creating...' : 'Updating...') : (mode === 'create' ? 'Create Store' : 'Update Store')}
{isUploading ? 'Uploading...' : isLoading ? (mode === 'create' ? 'Creating...' : 'Updating...') : (mode === 'create' ? 'Create Store' : 'Update Store')}
</MyText>
</TouchableOpacity>
</View>

View file

@ -0,0 +1,118 @@
import { useState } from 'react';
import { trpc } from '../src/trpc-client';
type ContextString = 'review' | 'product_info' | 'notification' | 'store' | 'complaint' | 'profile';
interface UploadInput {
blob: Blob;
mimeType: string;
}
interface UploadBatchInput {
images: UploadInput[];
contextString: ContextString;
}
interface UploadResult {
keys: string[];
presignedUrls: string[];
}
export function useUploadToObjectStorage() {
const [isUploading, setIsUploading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [progress, setProgress] = useState<{ completed: number; total: number } | null>(null);
const generateUploadUrls = trpc.common.generateUploadUrls.useMutation();
const upload = async (input: UploadBatchInput): Promise<UploadResult> => {
setIsUploading(true);
setError(null);
setProgress({ completed: 0, total: input.images.length });
try {
const { images, contextString } = input;
if (images.length === 0) {
return { keys: [], presignedUrls: [] };
}
// 1. Get presigned URLs from backend (one call for all images)
const mimeTypes = images.map(img => img.mimeType);
const { uploadUrls } = await generateUploadUrls.mutateAsync({
contextString,
mimeTypes,
});
if (uploadUrls.length !== images.length) {
throw new Error(`Expected ${images.length} URLs, got ${uploadUrls.length}`);
}
// 2. Upload all images in parallel
const uploadPromises = images.map(async (image, index) => {
const presignedUrl = uploadUrls[index];
const { blob, mimeType } = image;
const response = await fetch(presignedUrl, {
method: 'PUT',
body: blob,
headers: { 'Content-Type': mimeType },
});
if (!response.ok) {
throw new Error(`Upload ${index + 1} failed with status ${response.status}`);
}
// Update progress
setProgress(prev => prev ? { ...prev, completed: prev.completed + 1 } : null);
return {
key: extractKeyFromPresignedUrl(presignedUrl),
presignedUrl,
};
});
// Use Promise.all - if any fails, entire batch fails
const results = await Promise.all(uploadPromises);
return {
keys: results.map(r => r.key),
presignedUrls: results.map(r => r.presignedUrl),
};
} catch (err) {
const uploadError = err instanceof Error ? err : new Error('Upload failed');
setError(uploadError);
throw uploadError;
} finally {
setIsUploading(false);
setProgress(null);
}
};
const uploadSingle = async (blob: Blob, mimeType: string, contextString: ContextString): Promise<{ key: string; presignedUrl: string }> => {
const result = await upload({
images: [{ blob, mimeType }],
contextString,
});
return {
key: result.keys[0],
presignedUrl: result.presignedUrls[0],
};
};
return {
upload,
uploadSingle,
isUploading,
error,
progress,
isPending: generateUploadUrls.isPending
};
}
function extractKeyFromPresignedUrl(url: string): string {
const u = new URL(url);
let rawKey = u.pathname.replace(/^\/+/, '');
rawKey = rawKey.split('/').slice(1).join('/'); // make meatfarmer/product-images/asdf as product-images/asdf
return decodeURIComponent(rawKey);
}

View file

@ -1,111 +0,0 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import axios from '../../services/axios-admin-ui';
// Types
export interface CreateProductPayload {
name: string;
shortDescription?: string;
longDescription?: string;
unitId: number;
storeId: number;
price: number;
marketPrice?: number;
incrementStep?: number;
productQuantity?: number;
isOutOfStock?: boolean;
deals?: {
quantity: number;
price: number;
validTill: string;
}[];
}
export interface UpdateProductPayload {
name: string;
shortDescription?: string;
longDescription?: string;
unitId: number;
storeId: number;
price: number;
marketPrice?: number;
incrementStep?: number;
productQuantity?: number;
isOutOfStock?: boolean;
deals?: {
quantity: number;
price: number;
validTill: string;
}[];
}
export interface Product {
id: number;
name: string;
shortDescription?: string | null;
longDescription?: string;
unitId: number;
storeId: number;
price: number;
marketPrice?: number;
productQuantity?: number;
isOutOfStock?: boolean;
images?: string[];
createdAt: string;
unit?: {
id: number;
shortNotation: string;
fullName: string;
};
deals?: {
id: number;
quantity: string;
price: string;
validTill: string;
}[];
}
export interface CreateProductResponse {
product: Product;
deals?: any[];
message: string;
}
// API functions
const createProductApi = async (formData: FormData): Promise<CreateProductResponse> => {
const response = await axios.post('/av/products', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return response.data;
};
const updateProductApi = async ({ id, formData }: { id: number; formData: FormData }): Promise<CreateProductResponse> => {
const response = await axios.put(`/av/products/${id}`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return response.data;
};
// Hooks
export const useCreateProduct = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: createProductApi,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['products'] });
},
});
};
export const useUpdateProduct = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateProductApi,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['products'] });
},
});
};

View file

@ -1,10 +1,8 @@
import React, { useState, useEffect, useCallback, forwardRef, useImperativeHandle } from 'react';
import React, { useState, useEffect, useCallback, forwardRef, useImperativeHandle, useMemo } from 'react';
import { View, TouchableOpacity } from 'react-native';
import { Image } from 'expo-image';
import { Formik, FieldArray } from 'formik';
import * as Yup from 'yup';
import { MyTextInput, BottomDropdown, MyText, ImageUploader, ImageGalleryWithDelete, useTheme, DatePicker, tw, useFocusCallback, Checkbox } from 'common-ui';
import usePickImage from 'common-ui/src/components/use-pick-image';
import { MyTextInput, BottomDropdown, MyText, useTheme, DatePicker, tw, useFocusCallback, Checkbox, ImageUploaderNeo, ImageUploaderNeoItem, ImageUploaderNeoPayload } from 'common-ui';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { trpc } from '../trpc-client';
import { useGetTags } from '../api-hooks/tag.api';
@ -38,9 +36,10 @@ export interface ProductFormRef {
interface ProductFormProps {
mode: 'create' | 'edit';
initialValues: ProductFormData;
onSubmit: (values: ProductFormData, images?: { uri?: string }[], imagesToDelete?: string[]) => void;
onSubmit: (values: ProductFormData, images: ImageUploaderNeoPayload[], imagesToDelete: string[]) => void;
isLoading: boolean;
existingImages?: string[];
existingImages?: ImageUploaderNeoItem[];
existingImageKeys?: string[];
}
const unitOptions = [
@ -50,18 +49,21 @@ const unitOptions = [
{ label: 'Unit Piece', value: 4 },
];
const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
mode,
initialValues,
onSubmit,
isLoading,
existingImages = []
existingImages = [],
existingImageKeys = [],
}, ref) => {
const { theme } = useTheme();
const [images, setImages] = useState<{ uri?: string }[]>([]);
const [existingImagesState, setExistingImagesState] = useState<string[]>(existingImages);
const [images, setImages] = useState<ImageUploaderNeoItem[]>(existingImages);
// Sync images state when existingImages prop changes (e.g., when async query data arrives)
useEffect(() => {
setImages(existingImages);
}, [existingImages]);
const { data: storesData } = trpc.common.getStoresSummary.useQuery();
const storeOptions = storesData?.stores.map(store => ({
@ -75,38 +77,44 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
value: tag.id.toString(),
})) || [];
// Initialize existing images state when existingImages prop changes
useEffect(() => {
console.log('changing existing imaes statte')
setExistingImagesState(existingImages);
}, [existingImages]);
const pickImage = usePickImage({
setFile: (files) => setImages(prev => [...prev, ...files]),
multiple: true,
// Build signed URL -> S3 key mapping for existing images
const signedUrlToKey = useMemo(() => {
const map: Record<string, string> = {};
existingImages.forEach((img, i) => {
if (existingImageKeys[i]) {
map[img.imgUrl] = existingImageKeys[i];
}
});
// Calculate which existing images were deleted
const deletedImages = existingImages.filter(img => !existingImagesState.includes(img));
return map;
}, [existingImages, existingImageKeys]);
return (
<Formik
initialValues={initialValues}
onSubmit={(values) => onSubmit(values, images, deletedImages)}
onSubmit={(values) => {
// New images have mimeType set, existing images have mimeType === null
const newImages = images.filter(img => img.mimeType !== null);
const deletedImageKeys = existingImages
.filter(existing => !images.some(current => current.imgUrl === existing.imgUrl))
.map(deleted => signedUrlToKey[deleted.imgUrl])
.filter(Boolean);
onSubmit(
values,
newImages.map(img => ({ url: img.imgUrl, mimeType: img.mimeType })),
deletedImageKeys,
);
}}
enableReinitialize
>
{({ handleChange, handleSubmit, values, setFieldValue, resetForm }) => {
// Clear form when screen comes into focus
const clearForm = useCallback(() => {
setImages([]);
setExistingImagesState([]);
resetForm();
}, [resetForm]);
useFocusCallback(clearForm);
// Update ref with current clearForm function
useImperativeHandle(ref, () => ({
clearImages: clearForm,
}), [clearForm]);
@ -141,44 +149,18 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
style={{ marginBottom: 16 }}
/>
{mode === 'create' && (
<ImageUploader
<ImageUploaderNeo
images={images}
onAddImage={pickImage}
onRemoveImage={(uri) => setImages(prev => prev.filter(img => img.uri !== uri))}
onImageAdd={(payloads) => setImages(prev => [...prev, ...payloads.map(p => ({ imgUrl: p.url, mimeType: p.mimeType }))])}
onImageRemove={(payload) => setImages(prev => prev.filter(img => img.imgUrl !== payload.url))}
allowMultiple={true}
/>
)}
{mode === 'edit' && existingImagesState.length > 0 && (
<View style={{ marginBottom: 16 }}>
<MyText style={tw`text-lg font-bold mb-2 text-gray-800`}>Current Images</MyText>
<ImageGalleryWithDelete
imageUrls={existingImagesState}
setImageUrls={setExistingImagesState}
imageHeight={100}
imageWidth={100}
columns={3}
/>
</View>
)}
{mode === 'edit' && (
<View style={{ marginBottom: 16 }}>
<MyText style={tw`text-lg font-bold mb-2 text-gray-800`}>Add New Images</MyText>
<ImageUploader
images={images}
onAddImage={pickImage}
onRemoveImage={(uri) => setImages(prev => prev.filter(img => img.uri !== uri))}
/>
</View>
)}
<BottomDropdown
topLabel='Unit'
label="Unit"
value={values.unitId}
options={unitOptions}
// onValueChange={(value) => handleChange('unitId')(value+'')}
onValueChange={(value) => setFieldValue('unitId', value)}
placeholder="Select unit"
style={{ marginBottom: 16 }}
@ -188,18 +170,7 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
placeholder="Enter product quantity"
keyboardType="numeric"
value={values.productQuantity.toString()}
onChangeText={(text) => {
// if(text)
// setFieldValue('productQuantity', text);
// else
setFieldValue('productQuantity', text);
// if (text === '' || text === null || text === undefined) {
// setFieldValue('productQuantity', 1);
// } else {
// const num = parseFloat(text);
// setFieldValue('productQuantity', isNaN(num) ? 1 : num);
// }
}}
onChangeText={(text) => setFieldValue('productQuantity', text)}
style={{ marginBottom: 16 }}
/>
<BottomDropdown
@ -238,8 +209,6 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
style={{ marginBottom: 16 }}
/>
<View style={tw`flex-row items-center mb-4`}>
<Checkbox
checked={values.isSuspended}
@ -254,7 +223,7 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
checked={values.isFlashAvailable}
onPress={() => {
setFieldValue('isFlashAvailable', !values.isFlashAvailable);
if (values.isFlashAvailable) setFieldValue('flashPrice', ''); // Clear price when disabled
if (values.isFlashAvailable) setFieldValue('flashPrice', '');
}}
style={tw`mr-3`}
/>
@ -272,87 +241,6 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
/>
)}
{/* <FieldArray name="deals">
{({ push, remove, form }) => (
<View style={{ marginBottom: 16 }}>
<View style={tw`flex-row items-center mb-4`}>
<MaterialIcons name="local-offer" size={20} color="#3B82F6" />
<MyText style={tw`text-lg font-bold text-gray-800 ml-2`}>
Special Package Deals
</MyText>
<MyText style={tw`text-sm text-gray-500 ml-1`}>(Optional)</MyText>
</View>
{(form.values.deals || []).map((deal: any, index: number) => (
<View key={index} style={tw`bg-white p-4 rounded-2xl shadow-lg mb-4 border border-gray-100`}>
<View style={tw`mb-3`}>
<View style={tw`flex-row items-end gap-3 mb-3`}>
<View style={tw`flex-1`}>
<MyTextInput
topLabel="Quantity"
placeholder="Enter quantity"
keyboardType="numeric"
value={deal.quantity || ''}
onChangeText={form.handleChange(`deals.${index}.quantity`)}
fullWidth={false}
/>
</View>
<View style={tw`flex-1`}>
<MyTextInput
topLabel="Price"
placeholder="Enter price"
keyboardType="numeric"
value={deal.price || ''}
onChangeText={form.handleChange(`deals.${index}.price`)}
fullWidth={false}
/>
</View>
</View>
<View style={tw`flex-row items-end gap-3`}>
<View style={tw`flex-1`}>
<DatePicker
value={deal.validTill}
setValue={(date) => form.setFieldValue(`deals.${index}.validTill`, date)}
showLabel={true}
placeholder="Valid Till"
/>
</View>
<View style={tw`flex-1`}>
<TouchableOpacity
onPress={() => remove(index)}
style={tw`bg-red-500 p-3 rounded-lg shadow-md flex-row items-center justify-center`}
>
<MaterialIcons name="delete" size={16} color="white" />
<MyText style={tw`text-white font-semibold ml-1`}>Remove</MyText>
</TouchableOpacity>
</View>
</View>
</View>
</View>
))}
{(form.values.deals || []).length === 0 && (
<View style={tw`bg-gray-50 p-6 rounded-2xl border-2 border-dashed border-gray-300 items-center mb-4`}>
<MaterialIcons name="local-offer" size={32} color="#9CA3AF" />
<MyText style={tw`text-gray-500 text-center mt-2`}>
No package deals added yet
</MyText>
<MyText style={tw`text-gray-400 text-sm text-center mt-1`}>
Add special pricing for bulk purchases
</MyText>
</View>
)}
<TouchableOpacity
onPress={() => push({ quantity: '', price: '', validTill: null })}
style={tw`bg-green-500 px-4 py-2 rounded-lg shadow-lg flex-row items-center justify-center mt-4`}
>
<MaterialIcons name="add" size={20} color="white" />
<MyText style={tw`text-white font-bold text-lg ml-2`}>Add Package Deal</MyText>
</TouchableOpacity>
</View>
)}
</FieldArray> */}
<TouchableOpacity
onPress={submit}
disabled={isLoading}

View file

@ -2,7 +2,6 @@ import 'dotenv/config';
import express, { NextFunction, Request, Response } from "express";
import cors from "cors";
// import bodyParser from "body-parser";
import multer from "multer";
import path from "path";
import fs from "fs";
import { getStaffUserById, getUserDetailsByUserId, isUserSuspended } from '@/src/dbService';

View file

@ -4,8 +4,6 @@ import { ApiError } from "@/src/lib/api-error"
import v1Router from "@/src/v1-router"
import testController from "@/src/test-controller"
import { authenticateUser } from "@/src/middleware/auth.middleware"
import { raiseComplaint } from "@/src/uv-apis/user-rest.controller"
import uploadHandler from "@/src/lib/upload-handler"
const router = Router();
@ -34,12 +32,6 @@ router.use('/v1', v1Router);
// router.use('/av', avRouter);
router.use('/test', testController);
// User REST APIs
router.post('/uv/complaints/raise',
uploadHandler.array('images', 5),
raiseComplaint
);
// Global error handling middleware
router.use((err: Error, req: Request, res: Response, next: NextFunction) => {
console.error('Error:', err);

View file

@ -1,7 +1,7 @@
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'
import { ApiError } from '@/src/lib/api-error'
import { generateSignedUrlsFromS3Urls, claimUploadUrl } from '@/src/lib/s3-client'
import { generateSignedUrlsFromS3Urls, claimUploadUrl, extractKeyFromPresignedUrl, deleteImageUtil } from '@/src/lib/s3-client'
import { scheduleStoreInitialization } from '@/src/stores/store-initializer'
import {
getAllProducts as getAllProductsInDb,
@ -18,8 +18,18 @@ import {
updateProductGroup as updateProductGroupInDb,
deleteProductGroup as deleteProductGroupInDb,
updateProductPrices as updateProductPricesInDb,
checkProductExistsByName,
checkUnitExists,
createProduct as createProductInDb,
createSpecialDealsForProduct,
replaceProductTags,
getProductImagesById,
updateProduct as updateProductInDb,
updateProductDeals,
} from '@/src/dbService'
import type {
AdminProduct,
AdminSpecialDeal,
AdminProductGroupsResult,
AdminProductGroupResponse,
AdminProductReviewsResult,
@ -200,6 +210,168 @@ export const productRouter = router({
}
}),
createProduct: protectedProcedure
.input(z.object({
name: z.string().min(1, 'Name is required'),
shortDescription: z.string().optional(),
longDescription: z.string().optional(),
unitId: z.number().min(1, 'Unit is required'),
storeId: z.number().min(1, 'Store is required'),
price: z.number().positive('Price must be positive'),
marketPrice: z.number().optional(),
incrementStep: z.number().optional().default(1),
productQuantity: z.number().optional().default(1),
isSuspended: z.boolean().optional().default(false),
isFlashAvailable: z.boolean().optional().default(false),
flashPrice: z.number().optional(),
uploadUrls: z.array(z.string()).optional().default([]),
deals: z.array(z.object({
quantity: z.number(),
price: z.number(),
validTill: z.string(),
})).optional(),
tagIds: z.array(z.number()).optional().default([]),
}))
.mutation(async ({ input }): Promise<{ product: AdminProduct; deals: AdminSpecialDeal[]; message: string }> => {
const { name, shortDescription, longDescription, unitId, storeId, price, marketPrice, incrementStep, productQuantity, isSuspended, isFlashAvailable, flashPrice, uploadUrls, deals, tagIds } = input
const existingProduct = await checkProductExistsByName(name.trim())
if (existingProduct) {
throw new ApiError('A product with this name already exists', 400)
}
const unitExists = await checkUnitExists(unitId)
if (!unitExists) {
throw new ApiError('Invalid unit ID', 400)
}
const imageKeys = uploadUrls.map(url => extractKeyFromPresignedUrl(url))
const newProduct = await createProductInDb({
name,
shortDescription,
longDescription,
unitId,
storeId,
price: price.toString(),
marketPrice: marketPrice?.toString(),
incrementStep,
productQuantity,
isSuspended,
isFlashAvailable,
flashPrice: flashPrice?.toString(),
images: imageKeys,
})
let createdDeals: AdminSpecialDeal[] = []
if (deals && deals.length > 0) {
createdDeals = await createSpecialDealsForProduct(newProduct.id, deals)
}
if (tagIds.length > 0) {
await replaceProductTags(newProduct.id, tagIds)
}
if (uploadUrls.length > 0) {
await Promise.all(uploadUrls.map(url => claimUploadUrl(url)))
}
scheduleStoreInitialization()
return {
product: newProduct,
deals: createdDeals,
message: 'Product created successfully',
}
}),
updateProduct: protectedProcedure
.input(z.object({
id: z.number(),
name: z.string().min(1, 'Name is required'),
shortDescription: z.string().optional(),
longDescription: z.string().optional(),
unitId: z.number().min(1, 'Unit is required'),
storeId: z.number().min(1, 'Store is required'),
price: z.number().positive('Price must be positive'),
marketPrice: z.number().optional(),
incrementStep: z.number().optional().default(1),
productQuantity: z.number().optional().default(1),
isSuspended: z.boolean().optional().default(false),
isFlashAvailable: z.boolean().optional().default(false),
flashPrice: z.number().nullable().optional(),
uploadUrls: z.array(z.string()).optional().default([]),
imagesToDelete: z.array(z.string()).optional().default([]),
deals: z.array(z.object({
quantity: z.number(),
price: z.number(),
validTill: z.string(),
})).optional(),
tagIds: z.array(z.number()).optional().default([]),
}))
.mutation(async ({ input }): Promise<{ product: AdminProduct; message: string }> => {
const { id, name, shortDescription, longDescription, unitId, storeId, price, marketPrice, incrementStep, productQuantity, isSuspended, isFlashAvailable, flashPrice, uploadUrls, imagesToDelete, deals, tagIds } = input
const unitExists = await checkUnitExists(unitId)
if (!unitExists) {
throw new ApiError('Invalid unit ID', 400)
}
const currentImages = await getProductImagesById(id)
if (!currentImages) {
throw new ApiError('Product not found', 404)
}
let updatedImages = currentImages || []
if (imagesToDelete.length > 0) {
const imagesToRemove = updatedImages.filter(img => imagesToDelete.includes(img))
await deleteImageUtil({ keys: imagesToRemove })
updatedImages = updatedImages.filter(img => !imagesToRemove.includes(img))
}
const newImageKeys = uploadUrls.map(url => extractKeyFromPresignedUrl(url))
const finalImages = [...updatedImages, ...newImageKeys]
const updatedProduct = await updateProductInDb(id, {
name,
shortDescription,
longDescription,
unitId,
storeId,
price: price.toString(),
marketPrice: marketPrice?.toString(),
incrementStep,
productQuantity,
isSuspended,
isFlashAvailable,
flashPrice: flashPrice?.toString() ?? null,
images: finalImages,
})
if (!updatedProduct) {
throw new ApiError('Product not found', 404)
}
if (deals && deals.length > 0) {
await updateProductDeals(id, deals)
}
if (tagIds.length > 0) {
await replaceProductTags(id, tagIds)
}
if (uploadUrls.length > 0) {
await Promise.all(uploadUrls.map(url => claimUploadUrl(url)))
}
scheduleStoreInitialization()
return {
product: updatedProduct,
message: 'Product updated successfully',
}
}),
updateSlotProducts: protectedProcedure
.input(z.object({
slotId: z.string(),
@ -484,7 +656,7 @@ export const productRouter = router({
groups: groups.map(group => ({
...group,
products: group.memberships.map(m => ({
...m.product,
...(m.product as AdminProduct),
images: (m.product.images as string[]) || null,
})),
productCount: group.memberships.length,

View file

@ -82,7 +82,7 @@ export const commonApiRouter = router({
generateUploadUrls: protectedProcedure
.input(z.object({
contextString: z.enum(['review', 'product_info', 'store']),
contextString: z.enum(['review', 'review_response', 'product_info', 'notification', 'store', 'complaint', 'profile']),
mimeTypes: z.array(z.string()),
}))
.mutation(async ({ input }): Promise<{ uploadUrls: string[] }> => {
@ -102,6 +102,10 @@ export const commonApiRouter = router({
folder = 'store-images';
} else if (contextString === 'review_response') {
folder = 'review-response-images';
} else if (contextString === 'complaint') {
folder = 'complaint-images';
} else if (contextString === 'profile') {
folder = 'profile-images';
} else {
folder = '';
}

View file

@ -12,10 +12,12 @@ import {
getUserAuthById as getUserAuthByIdInDb,
getUserAuthCreds as getUserAuthCredsInDb,
getUserAuthDetails as getUserAuthDetailsInDb,
createUserAuthWithCreds as createUserAuthWithCredsInDb,
createUserAuthWithMobile as createUserAuthWithMobileInDb,
upsertUserAuthPassword as upsertUserAuthPasswordInDb,
deleteUserAuthAccount as deleteUserAuthAccountInDb,
createUserWithProfile as createUserWithProfileInDb,
updateUserProfile as updateUserProfileInDb,
getUserDetailsByUserId as getUserDetailsByUserIdInDb,
} from '@/src/dbService'
import type {
UserAuthResult,
@ -36,6 +38,7 @@ interface RegisterRequest {
email: string;
mobile: string;
password: string;
profileImageUrl?: string | null;
}
const generateToken = (userId: number): string => {
@ -127,9 +130,10 @@ export const authRouter = router({
email: z.string().email('Invalid email format'),
mobile: z.string().min(1, 'Mobile is required'),
password: z.string().min(1, 'Password is required'),
profileImageUrl: z.string().nullable().optional(),
}))
.mutation(async ({ input }): Promise<UserAuthResult> => {
const { name, email, mobile, password }: RegisterRequest = input;
const { name, email, mobile, password, profileImageUrl }: RegisterRequest = input;
if (!name || !email || !mobile || !password) {
throw new ApiError('All fields are required', 400);
@ -165,15 +169,20 @@ export const authRouter = router({
const hashedPassword = await bcrypt.hash(password, 12);
// Create user and credentials in a transaction
const newUser = await createUserAuthWithCredsInDb({
const newUser = await createUserWithProfileInDb({
name: name.trim(),
email: email.toLowerCase().trim(),
mobile: cleanMobile,
hashedPassword,
profileImage: profileImageUrl ?? null,
})
const token = generateToken(newUser.id);
const profileImageSignedUrl = profileImageUrl
? await generateSignedUrlFromS3Url(profileImageUrl)
: null
const response: UserAuthResponse = {
token,
user: {
@ -182,7 +191,7 @@ export const authRouter = router({
email: newUser.email,
mobile: newUser.mobile,
createdAt: newUser.createdAt.toISOString(),
profileImage: null,
profileImage: profileImageSignedUrl,
},
};
@ -278,6 +287,102 @@ export const authRouter = router({
return { success: true, message: 'Password updated successfully' }
}),
updateProfile: protectedProcedure
.input(z.object({
name: z.string().min(1).optional(),
email: z.string().email('Invalid email format').optional(),
mobile: z.string().min(1).optional(),
password: z.string().min(6, 'Password must be at least 6 characters').optional(),
bio: z.string().optional().nullable(),
dateOfBirth: z.string().optional().nullable(),
gender: z.string().optional().nullable(),
occupation: z.string().optional().nullable(),
profileImageUrl: z.string().optional().nullable(),
}))
.mutation(async ({ input, ctx }): Promise<UserAuthResult> => {
const userId = ctx.user.userId;
if (!userId) {
throw new ApiError('User not authenticated', 401);
}
const { name, email, mobile, password, bio, dateOfBirth, gender, occupation, profileImageUrl } = input
if (email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
throw new ApiError('Invalid email format', 400);
}
}
if (mobile) {
const cleanMobile = mobile.replace(/\D/g, '');
if (cleanMobile.length !== 10 || !/^[6-9]/.test(cleanMobile)) {
throw new ApiError('Invalid mobile number', 400);
}
}
if (email) {
const existingEmail = await getUserAuthByEmailInDb(email.toLowerCase())
if (existingEmail && existingEmail.id !== userId) {
throw new ApiError('Email already registered', 409)
}
}
if (mobile) {
const cleanMobile = mobile.replace(/\D/g, '')
const existingMobile = await getUserAuthByMobileInDb(cleanMobile)
if (existingMobile && existingMobile.id !== userId) {
throw new ApiError('Mobile number already registered', 409)
}
}
let hashedPassword: string | undefined;
if (password) {
hashedPassword = await bcrypt.hash(password, 12)
}
const updatedUser = await updateUserProfileInDb(userId, {
name: name?.trim(),
email: email?.toLowerCase().trim(),
mobile: mobile?.replace(/\D/g, ''),
hashedPassword,
profileImage: profileImageUrl ?? undefined,
bio: bio ?? undefined,
dateOfBirth: dateOfBirth ? new Date(dateOfBirth) : undefined,
gender: gender ?? undefined,
occupation: occupation ?? undefined,
})
const userDetail = await getUserDetailsByUserIdInDb(userId)
const profileImageSignedUrl = userDetail?.profileImage
? await generateSignedUrlFromS3Url(userDetail.profileImage)
: null
const token = ctx.req.headers.authorization?.replace('Bearer ', '') || ''
const response: UserAuthResponse = {
token,
user: {
id: updatedUser.id,
name: updatedUser.name,
email: updatedUser.email,
mobile: updatedUser.mobile,
createdAt: updatedUser.createdAt?.toISOString?.() || new Date().toISOString(),
profileImage: profileImageSignedUrl,
bio: userDetail?.bio || null,
dateOfBirth: userDetail?.dateOfBirth || null,
gender: userDetail?.gender || null,
occupation: userDetail?.occupation || null,
},
}
return {
success: true,
data: response,
}
}),
getProfile: protectedProcedure
.query(async ({ ctx }): Promise<UserProfileResponse> => {
const userId = ctx.user.userId;

View file

@ -49,10 +49,11 @@ export const complaintRouter = router({
.input(z.object({
orderId: z.string().optional(),
complaintBody: z.string().min(1, 'Complaint body is required'),
imageUrls: z.array(z.string()).optional(),
}))
.mutation(async ({ input, ctx }): Promise<UserRaiseComplaintResponse> => {
const userId = ctx.user.userId;
const { orderId, complaintBody } = input;
const { orderId, complaintBody, imageUrls } = input;
let orderIdNum: number | null = null;
@ -63,7 +64,12 @@ export const complaintRouter = router({
}
}
await createUserComplaintInDb(userId, orderIdNum, complaintBody.trim())
await createUserComplaintInDb(
userId,
orderIdNum,
complaintBody.trim(),
imageUrls && imageUrls.length > 0 ? imageUrls : null
)
/*
// Old implementation - direct DB query:

View file

@ -6,7 +6,7 @@ import { ApiError } from '@/src/lib/api-error';
export const fileUploadRouter = router({
generateUploadUrls: protectedProcedure
.input(z.object({
contextString: z.enum(['review', 'product_info', 'notification']),
contextString: z.enum(['review', 'product_info', 'notification', 'complaint', 'profile']),
mimeTypes: z.array(z.string()),
}))
.mutation(async ({ input }): Promise<{ uploadUrls: string[] }> => {
@ -28,6 +28,10 @@ export const fileUploadRouter = router({
// }
else if(contextString === 'notification') {
folder = 'notification-images'
} else if (contextString === 'complaint') {
folder = 'complaint-images'
} else if (contextString === 'profile') {
folder = 'profile-images'
} else {
folder = '';
}

View file

@ -1,374 +0,0 @@
import { Request, Response, NextFunction } from 'express';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import {
getUserAuthByEmail,
getUserAuthByMobile,
createUserWithProfile,
getUserAuthById,
getUserDetailsByUserId,
updateUserProfile,
} from '@/src/dbService';
import { ApiError } from '@/src/lib/api-error'
import catchAsync from '@/src/lib/catch-async'
import { jwtSecret } from '@/src/lib/env-exporter';
import uploadHandler from '@/src/lib/upload-handler'
import { imageUploadS3, generateSignedUrlFromS3Url } from '@/src/lib/s3-client'
interface RegisterRequest {
name: string;
email: string;
mobile: string;
password: string;
profileImage?: string;
}
interface UpdateProfileRequest {
name?: string;
email?: string;
mobile?: string;
password?: string;
bio?: string;
dateOfBirth?: string;
gender?: string;
occupation?: string;
}
interface AuthResponse {
token: string;
user: {
id: number;
name: string | null;
email: string | null;
mobile: string | null;
profileImage?: string | null;
bio?: string | null;
dateOfBirth?: string | null;
gender?: string | null;
occupation?: string | null;
};
}
const generateToken = (userId: number): string => {
const secret = jwtSecret;
if (!secret) {
throw new ApiError('JWT secret not configured', 500);
}
return jwt.sign({ userId }, secret, { expiresIn: '7d' });
};
export const register = catchAsync(async (req: Request, res: Response, next: NextFunction) => {
const { name, email, mobile, password }: RegisterRequest = req.body;
// Handle profile image upload
let profileImageUrl: string | undefined;
if (req.file) {
const key = `profile-images/${Date.now()}-${req.file.originalname}`;
profileImageUrl = await imageUploadS3(req.file.buffer, req.file.mimetype, key);
}
if (!name || !email || !mobile || !password) {
throw new ApiError('All fields are required', 400);
}
// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
throw new ApiError('Invalid email format', 400);
}
// Validate mobile format (Indian mobile numbers)
const cleanMobile = mobile.replace(/\D/g, '');
if (cleanMobile.length !== 10 || !/^[6-9]/.test(cleanMobile)) {
throw new ApiError('Invalid mobile number', 400);
}
/*
// Old implementation - direct DB queries:
import { db } from '@/src/db/db_index'
import { users } from '@/src/db/schema'
import { eq } from 'drizzle-orm';
const [existingEmail] = await db
.select()
.from(users)
.where(eq(users.email, email.toLowerCase()))
.limit(1);
*/
// Check if email already exists
const existingEmail = await getUserAuthByEmail(email.toLowerCase());
if (existingEmail) {
throw new ApiError('Email already registered', 409);
}
/*
// Old implementation - direct DB queries:
const [existingMobile] = await db
.select()
.from(users)
.where(eq(users.mobile, cleanMobile))
.limit(1);
*/
// Check if mobile already exists
const existingMobile = await getUserAuthByMobile(cleanMobile);
if (existingMobile) {
throw new ApiError('Mobile number already registered', 409);
}
// Hash password
const hashedPassword = await bcrypt.hash(password, 12);
/*
// Old implementation - direct DB queries:
import { userCreds, userDetails } from '@/src/db/schema'
const newUser = await db.transaction(async (tx) => {
const [user] = await tx
.insert(users)
.values({
name: name.trim(),
email: email.toLowerCase().trim(),
mobile: cleanMobile,
})
.returning();
await tx.insert(userCreds).values({
userId: user.id,
userPassword: hashedPassword,
});
await tx.insert(userDetails).values({
userId: user.id,
profileImage: profileImageUrl,
});
return user;
});
*/
// Create user with profile in transaction
const newUser = await createUserWithProfile({
name: name.trim(),
email: email.toLowerCase().trim(),
mobile: cleanMobile,
hashedPassword,
profileImage: profileImageUrl,
});
const token = generateToken(newUser.id);
// Generate signed URL for profile image if it was uploaded
const profileImageSignedUrl = profileImageUrl
? await generateSignedUrlFromS3Url(profileImageUrl)
: null;
const response: AuthResponse = {
token,
user: {
id: newUser.id,
name: newUser.name,
email: newUser.email,
mobile: newUser.mobile,
profileImage: profileImageSignedUrl,
bio: null,
dateOfBirth: null,
gender: null,
occupation: null,
},
};
res.status(201).json({
success: true,
data: response,
});
});
export const updateProfile = catchAsync(async (req: Request, res: Response, next: NextFunction) => {
const userId = req.user?.userId;
if (!userId) {
throw new ApiError('User not authenticated', 401);
}
const { name, email, mobile, password, bio, dateOfBirth, gender, occupation }: UpdateProfileRequest = req.body;
// Handle profile image upload
let profileImageUrl: string | undefined;
if (req.file) {
const key = `profile-images/${Date.now()}-${req.file.originalname}`;
profileImageUrl = await imageUploadS3(req.file.buffer, req.file.mimetype, key);
}
// Validate email format if provided
if (email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
throw new ApiError('Invalid email format', 400);
}
}
// Validate mobile format if provided
if (mobile) {
const cleanMobile = mobile.replace(/\D/g, '');
if (cleanMobile.length !== 10 || !/^[6-9]/.test(cleanMobile)) {
throw new ApiError('Invalid mobile number', 400);
}
}
/*
// Old implementation - direct DB queries:
import { db } from '@/src/db/db_index'
import { users, userCreds, userDetails } from '@/src/db/schema'
import { eq } from 'drizzle-orm';
if (email) {
const [existingEmail] = await db
.select()
.from(users)
.where(eq(users.email, email.toLowerCase()))
.limit(1);
if (existingEmail && existingEmail.id !== userId) {
throw new ApiError('Email already registered', 409);
}
}
*/
// Check if email already exists (if changing email)
if (email) {
const existingEmail = await getUserAuthByEmail(email.toLowerCase());
if (existingEmail && existingEmail.id !== userId) {
throw new ApiError('Email already registered', 409);
}
}
/*
// Old implementation - direct DB queries:
if (mobile) {
const cleanMobile = mobile.replace(/\D/g, '');
const [existingMobile] = await db
.select()
.from(users)
.where(eq(users.mobile, cleanMobile))
.limit(1);
if (existingMobile && existingMobile.id !== userId) {
throw new ApiError('Mobile number already registered', 409);
}
}
*/
// Check if mobile already exists (if changing mobile)
if (mobile) {
const cleanMobile = mobile.replace(/\D/g, '');
const existingMobile = await getUserAuthByMobile(cleanMobile);
if (existingMobile && existingMobile.id !== userId) {
throw new ApiError('Mobile number already registered', 409);
}
}
// Hash password if provided
let hashedPassword: string | undefined;
if (password) {
hashedPassword = await bcrypt.hash(password, 12);
}
/*
// Old implementation - direct DB queries:
const updatedUser = await db.transaction(async (tx) => {
// Update user table
const updateData: any = {};
if (name) updateData.name = name.trim();
if (email) updateData.email = email.toLowerCase().trim();
if (mobile) updateData.mobile = mobile.replace(/\D/g, '');
if (Object.keys(updateData).length > 0) {
await tx.update(users).set(updateData).where(eq(users.id, userId));
}
// Update password if provided
if (password) {
const hashedPassword = await bcrypt.hash(password, 12);
await tx.update(userCreds).set({ userPassword: hashedPassword }).where(eq(userCreds.userId, userId));
}
// Update or insert user details
const userDetailsUpdate: any = {};
if (bio !== undefined) userDetailsUpdate.bio = bio;
if (dateOfBirth !== undefined) userDetailsUpdate.dateOfBirth = dateOfBirth ? new Date(dateOfBirth) : null;
if (gender !== undefined) userDetailsUpdate.gender = gender;
if (occupation !== undefined) userDetailsUpdate.occupation = occupation;
if (profileImageUrl) userDetailsUpdate.profileImage = profileImageUrl;
userDetailsUpdate.updatedAt = new Date();
const [existingDetails] = await tx.select().from(userDetails).where(eq(userDetails.userId, userId)).limit(1);
if (existingDetails) {
await tx.update(userDetails).set(userDetailsUpdate).where(eq(userDetails.userId, userId));
} else {
userDetailsUpdate.userId = userId;
userDetailsUpdate.createdAt = new Date();
await tx.insert(userDetails).values(userDetailsUpdate);
}
const [user] = await tx.select().from(users).where(eq(users.id, userId)).limit(1);
return user;
});
*/
// Update user profile in transaction
const updatedUser = await updateUserProfile(userId, {
name: name?.trim(),
email: email?.toLowerCase().trim(),
mobile: mobile?.replace(/\D/g, ''),
hashedPassword,
profileImage: profileImageUrl,
bio,
dateOfBirth: dateOfBirth ? new Date(dateOfBirth) : undefined,
gender,
occupation,
});
/*
// Old implementation - direct DB queries:
const [userDetail] = await db.select().from(userDetails).where(eq(userDetails.userId, userId)).limit(1);
*/
// Get updated user details for response
const userDetail = await getUserDetailsByUserId(userId);
// Generate signed URL for profile image if it exists
const profileImageSignedUrl = userDetail?.profileImage
? await generateSignedUrlFromS3Url(userDetail.profileImage)
: null;
const response: AuthResponse = {
token: req.headers.authorization?.replace('Bearer ', '') || '', // Keep existing token
user: {
id: updatedUser.id,
name: updatedUser.name,
email: updatedUser.email,
mobile: updatedUser.mobile,
profileImage: profileImageSignedUrl,
bio: userDetail?.bio || null,
dateOfBirth: userDetail?.dateOfBirth || null,
gender: userDetail?.gender || null,
occupation: userDetail?.occupation || null,
},
};
res.status(200).json({
success: true,
data: response,
});
});
/*
// Old implementation - direct DB queries:
import { db } from '@/src/db/db_index'
import { users, userCreds, userDetails } from '@/src/db/schema'
import { eq } from 'drizzle-orm';
*/

View file

@ -1,12 +0,0 @@
import { Router } from 'express';
import { register, updateProfile } from '@/src/uv-apis/auth.controller'
import { verifyToken } from '@/src/middleware/auth'
import uploadHandler from '@/src/lib/upload-handler'
const router = Router();
router.post('/register', uploadHandler.single('profileImage'), register);
router.put('/profile', verifyToken, uploadHandler.single('profileImage'), updateProfile);
const authRouter = router;
export default authRouter;

View file

@ -1,75 +0,0 @@
import { Request, Response, NextFunction } from 'express';
import { createUserComplaint } from '@/src/dbService';
import { ApiError } from '@/src/lib/api-error'
import catchAsync from '@/src/lib/catch-async'
import { imageUploadS3 } from '@/src/lib/s3-client'
interface RaiseComplaintRequest {
orderId?: string;
complaintBody: string;
}
export const raiseComplaint = catchAsync(async (req: Request, res: Response, next: NextFunction) => {
console.log('raising complaint')
const userId = req.user?.userId;
if (!userId) {
throw new ApiError('User not authenticated', 401);
}
const { orderId, complaintBody }: RaiseComplaintRequest = req.body;
let orderIdNum: number | null = null;
if (orderId) {
const readableIdMatch = orderId.match(/^ORD(\d+)$/);
if (readableIdMatch) {
orderIdNum = parseInt(readableIdMatch[1]);
}
}
// Handle image uploads
const images = (req.files as Express.Multer.File[])?.filter(item => item.fieldname === 'images');
let uploadedImageUrls: string[] = [];
if (images && Array.isArray(images)) {
const imageUploadPromises = images.map((file, index) => {
const key = `complaint-images/${Date.now()}-${index}`;
return imageUploadS3(file.buffer, file.mimetype, key);
});
uploadedImageUrls = await Promise.all(imageUploadPromises);
}
/*
// Old implementation - direct DB queries:
import { db } from '@/src/db/db_index'
import { complaints } from '@/src/db/schema'
await db.insert(complaints).values({
userId,
orderId: orderIdNum,
complaintBody: complaintBody.trim(),
images: uploadedImageUrls.length > 0 ? uploadedImageUrls : null,
});
*/
await createUserComplaint(
userId,
orderIdNum,
complaintBody.trim(),
uploadedImageUrls.length > 0 ? uploadedImageUrls : null
);
res.status(200).json({
success: true,
message: 'Complaint raised successfully'
});
});
/*
// Old implementation - direct DB queries:
import { db } from '@/src/db/db_index'
import { complaints } from '@/src/db/schema'
*/

View file

@ -1,12 +0,0 @@
import { Router } from "express";
import authRouter from "@/src/uv-apis/auth.router"
import { raiseComplaint } from "@/src/uv-apis/user-rest.controller"
import uploadHandler from "@/src/lib/upload-handler";
const router = Router();
router.use("/auth", authRouter);
router.use("/complaints/raise", uploadHandler.array('images'),raiseComplaint)
const uvRouter = router;
export default uvRouter;

View file

@ -1,13 +1,11 @@
import { Router } from "express";
import avRouter from "@/src/apis/admin-apis/apis/av-router"
import commonRouter from "@/src/apis/common-apis/apis/common.router"
import uvRouter from "@/src/uv-apis/uv-router"
import avRouter from "@/src/apis/admin-apis/apis/av-router"
import commonRouter from "@/src/apis/common-apis/apis/common.router"
const router = Router();
router.use('/av', avRouter);
router.use('/cm', commonRouter);
router.use('/uv', uvRouter);
router.use('/av', avRouter);
router.use('/cm', commonRouter);
const v1Router = router;

View file

@ -5,16 +5,17 @@ import { useRouter } from "expo-router";
import { MyText, tw, MyTouchableOpacity } from "common-ui";
import { useAuth } from "@/src/contexts/AuthContext";
import RegistrationForm from "@/components/registration-form";
import type { RegisterData, UpdateProfileData } from "@/src/types/auth";
function Register() {
const router = useRouter();
const { register } = useAuth();
const [isLoading, setIsLoading] = useState(false);
const handleRegister = async (formData: FormData) => {
const handleRegister = async (formData: RegisterData | UpdateProfileData) => {
setIsLoading(true);
try {
await register(formData);
await register(formData as RegisterData);
// Auth context will handle navigation on successful registration
} catch (error: any) {
Alert.alert(

View file

@ -4,6 +4,7 @@ import { AppContainer, MyButton, MyText, tw , BottomDialog } from "common-ui";
import RegistrationForm from "@/components/registration-form";
import { useUserDetails, useAuth } from "@/src/contexts/AuthContext";
import { useUpdateProfile } from "@/src/api-hooks/auth.api";
import type { RegisterData, UpdateProfileData } from "@/src/types/auth";
import { router } from "expo-router";
import { trpc } from '@/src/trpc-client';
@ -20,9 +21,9 @@ function EditProfile() {
// Prevent unnecessary re-renders
const mobileInputValue = useMemo(() => enteredMobile, [enteredMobile]);
const handleUpdate = async (data: FormData) => {
const handleUpdate = async (data: RegisterData | UpdateProfileData) => {
try {
const response = await updateProfileMutation.mutateAsync(data);
const response = await updateProfileMutation.mutateAsync(data as UpdateProfileData);
// Update the context with new user details
if (response.user) {

View file

@ -1,11 +1,9 @@
import React, { useState } from 'react';
import { View, TextInput, ActivityIndicator, KeyboardAvoidingView, Platform, Alert } from 'react-native';
import { MaterialIcons } from '@expo/vector-icons';
import { useMutation } from "@tanstack/react-query";
import { MyText, ImageUploader, tw, MyTouchableOpacity } from 'common-ui';
import usePickImage from 'common-ui/src/components/use-pick-image';
import axios from '../services/axios-user-ui';
// import axios from 'common-ui/src/services/axios';
import { MaterialIcons } from '@expo/vector-icons'
import { MyText, ImageUploaderNeo, tw, MyTouchableOpacity, type ImageUploaderNeoItem, type ImageUploaderNeoPayload } from 'common-ui'
import { trpc } from '@/src/trpc-client'
import { useUploadToObjectStorage } from '../hooks/useUploadToObjectStore'
interface ComplaintFormProps {
open: boolean;
@ -15,71 +13,66 @@ interface ComplaintFormProps {
export default function ComplaintForm({ open, onClose, orderId }: ComplaintFormProps) {
const [complaintBody, setComplaintBody] = useState('');
const [complaintImages, setComplaintImages] = useState<{ uri?: string }[]>([]);
const [complaintImages, setComplaintImages] = useState<ImageUploaderNeoItem[]>([])
// API function
const raiseComplaintApi = async (payload: { complaintBody: string; images: { uri?: string }[] }) => {
const formData = new FormData();
const raiseComplaintMutation = trpc.user.complaint.raise.useMutation()
formData.append('orderId', orderId.toString());
formData.append('complaintBody', payload.complaintBody);
const { upload, isUploading } = useUploadToObjectStorage()
// Add images if provided
if (payload.images && payload.images.length > 0) {
payload.images.forEach((image, index) => {
if (image.uri) {
const fileName = `complaint-image-${index}.jpg`;
formData.append('images', {
uri: image.uri,
name: fileName,
type: 'image/jpeg',
} as any);
}
});
const handleAddImages = (images: ImageUploaderNeoPayload[]) => {
setComplaintImages((prev) => [
...prev,
...images.map((image) => ({
imgUrl: image.url,
mimeType: image.mimeType,
})),
])
}
const response = await axios.post('/uv/complaints/raise', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return response.data;
};
const handleRemoveImage = (image: ImageUploaderNeoPayload) => {
setComplaintImages((prev) => prev.filter((item) => item.imgUrl !== image.url))
}
// Hook
const raiseComplaintMutation = useMutation({
mutationFn: raiseComplaintApi,
});
const pickComplaintImage = usePickImage({
setFile: (files) => setComplaintImages(prev => [...prev, ...files]),
multiple: true,
});
const handleSubmit = () => {
const handleSubmit = async () => {
if (!complaintBody.trim()) {
Alert.alert('Error', 'Please enter complaint details');
return;
}
raiseComplaintMutation.mutate(
{
complaintBody: complaintBody.trim(),
images: complaintImages,
},
{
onSuccess: () => {
Alert.alert('Success', 'Complaint raised successfully');
setComplaintBody('');
setComplaintImages([]);
onClose();
},
onError: (error: any) => {
Alert.alert('Error', error.message || 'Failed to raise complaint');
},
try {
let imageUrls: string[] = []
if (complaintImages.length > 0) {
const uploadImages = await Promise.all(
complaintImages.map(async (image) => {
const response = await fetch(image.imgUrl)
const blob = await response.blob()
return { blob, mimeType: image.mimeType || 'image/jpeg' }
})
)
const { keys } = await upload({
images: uploadImages,
contextString: 'complaint',
})
imageUrls = keys
}
await raiseComplaintMutation.mutateAsync({
orderId: orderId.toString(),
complaintBody: complaintBody.trim(),
imageUrls,
})
Alert.alert('Success', 'Complaint raised successfully')
setComplaintBody('')
setComplaintImages([])
onClose()
} catch (error: any) {
Alert.alert('Error', error.message || 'Failed to raise complaint')
}
}
);
};
if (!open) return null;
@ -105,18 +98,18 @@ export default function ComplaintForm({ open, onClose, orderId }: ComplaintFormP
textAlignVertical="top"
/>
<ImageUploader
<ImageUploaderNeo
images={complaintImages}
onAddImage={pickComplaintImage}
onRemoveImage={(uri) => setComplaintImages(prev => prev.filter(img => img.uri !== uri))}
onImageAdd={handleAddImages}
onImageRemove={handleRemoveImage}
/>
<MyTouchableOpacity
style={tw`bg-yellow-500 py-4 rounded-xl shadow-sm items-center mt-4 ${raiseComplaintMutation.isPending ? 'opacity-70' : ''}`}
style={tw`bg-yellow-500 py-4 rounded-xl shadow-sm items-center mt-4 ${raiseComplaintMutation.isPending || isUploading ? 'opacity-70' : ''}`}
onPress={handleSubmit}
disabled={raiseComplaintMutation.isPending}
disabled={raiseComplaintMutation.isPending || isUploading}
>
{raiseComplaintMutation.isPending ? (
{raiseComplaintMutation.isPending || isUploading ? (
<ActivityIndicator color="white" />
) : (
<MyText style={tw`text-white font-bold text-lg`}>Submit Complaint</MyText>

View file

@ -2,8 +2,10 @@ import React, { useState } from "react";
import { View, TextInput, Alert } from "react-native";
import { useForm, Controller } from "react-hook-form";
import { MyButton, MyText, MyTextInput, ProfileImage, tw, BottomDialog } from "common-ui";
import { MyButton, MyText, MyTextInput, ProfileImage, tw, BottomDialog, MyTouchableOpacity } from "common-ui";
import { trpc } from "@/src/trpc-client";
import { useUploadToObjectStorage } from "../hooks/useUploadToObjectStore";
import type { RegisterData, UpdateProfileData } from "@/src/types/auth";
interface RegisterFormInputs {
name: string;
@ -16,7 +18,7 @@ interface RegisterFormInputs {
}
interface RegistrationFormProps {
onSubmit: (data: FormData) => void | Promise<void>;
onSubmit: (data: RegisterData | UpdateProfileData) => void | Promise<void>;
isLoading?: boolean;
initialValues?: Partial<RegisterFormInputs>;
isEdit?: boolean;
@ -29,6 +31,7 @@ function RegistrationForm({ onSubmit, isLoading = false, initialValues, isEdit =
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const updatePasswordMutation = trpc.user.auth.updatePassword.useMutation();
const { uploadSingle, isUploading } = useUploadToObjectStorage()
// Set initial profile image URI for edit mode
React.useEffect(() => {
@ -161,27 +164,39 @@ function RegistrationForm({ onSubmit, isLoading = false, initialValues, isEdit =
return;
}
// Create FormData
const formData = new FormData();
formData.append('name', data.name.trim());
formData.append('email', data.email.trim().toLowerCase());
formData.append('mobile', data.mobile.replace(/\D/g, ''));
// Only include password if provided (for edit mode)
if (data.password) {
formData.append('password', data.password);
let profileImageUrl: string | undefined;
if (profileImageFile?.uri) {
const response = await fetch(profileImageFile.uri)
const blob = await response.blob()
const mimeType = profileImageFile.mimeType || 'image/jpeg'
const { key } = await uploadSingle(blob, mimeType, 'profile')
profileImageUrl = key
}
if (profileImageFile) {
formData.append('profileImage', {
uri: profileImageFile.uri,
type: profileImageFile.mimeType || 'image/jpeg',
name: profileImageFile.name || 'profile.jpg',
} as any);
const basePayload = {
name: data.name.trim(),
email: data.email.trim().toLowerCase(),
mobile: data.mobile.replace(/\D/g, ''),
}
await onSubmit(formData);
if (isEdit) {
const updatePayload: UpdateProfileData = {
...basePayload,
...(data.password ? { password: data.password } : {}),
...(profileImageUrl ? { profileImageUrl } : {}),
}
await onSubmit(updatePayload)
return
}
const registerPayload: RegisterData = {
...basePayload,
password: data.password,
...(profileImageUrl ? { profileImageUrl } : {}),
}
await onSubmit(registerPayload)
};
const handleUpdatePassword = async () => {
@ -407,10 +422,14 @@ function RegistrationForm({ onSubmit, isLoading = false, initialValues, isEdit =
fillColor="brand500"
textColor="white1"
fullWidth
disabled={isLoading}
disabled={isLoading || isUploading}
style={tw` rounded-lg`}
>
{isLoading ? (isEdit ? "Updating..." : "Creating Account...") : (isEdit ? "Update Profile" : "Create Account")}
{isUploading
? 'Uploading...'
: isLoading
? (isEdit ? "Updating..." : "Creating Account...")
: (isEdit ? "Update Profile" : "Create Account")}
</MyButton>
{isEdit && (

View file

@ -0,0 +1,119 @@
import { useState } from 'react'
import { trpc } from '@/src/trpc-client'
type ContextString = 'review' | 'product_info' | 'notification' | 'store' | 'complaint' | 'profile'
interface UploadInput {
blob: Blob
mimeType: string
}
interface UploadBatchInput {
images: UploadInput[]
contextString: ContextString
}
interface UploadResult {
keys: string[]
presignedUrls: string[]
}
export function useUploadToObjectStorage() {
const [isUploading, setIsUploading] = useState(false)
const [error, setError] = useState<Error | null>(null)
const [progress, setProgress] = useState<{ completed: number; total: number } | null>(null)
const generateUploadUrls = trpc.common.generateUploadUrls.useMutation()
const upload = async (input: UploadBatchInput): Promise<UploadResult> => {
setIsUploading(true)
setError(null)
setProgress({ completed: 0, total: input.images.length })
try {
const { images, contextString } = input
if (images.length === 0) {
return { keys: [], presignedUrls: [] }
}
const mimeTypes = images.map((img) => img.mimeType)
const { uploadUrls } = await generateUploadUrls.mutateAsync({
contextString: contextString as any,
mimeTypes,
})
if (uploadUrls.length !== images.length) {
throw new Error(`Expected ${images.length} URLs, got ${uploadUrls.length}`)
}
const uploadPromises = images.map(async (image, index) => {
const presignedUrl = uploadUrls[index]
const { blob, mimeType } = image
const response = await fetch(presignedUrl, {
method: 'PUT',
body: blob,
headers: { 'Content-Type': mimeType },
})
if (!response.ok) {
throw new Error(`Upload ${index + 1} failed with status ${response.status}`)
}
setProgress((prev) => (prev ? { ...prev, completed: prev.completed + 1 } : null))
return {
key: extractKeyFromPresignedUrl(presignedUrl),
presignedUrl,
}
})
const results = await Promise.all(uploadPromises)
return {
keys: results.map((result) => result.key),
presignedUrls: results.map((result) => result.presignedUrl),
}
} catch (err) {
const uploadError = err instanceof Error ? err : new Error('Upload failed')
setError(uploadError)
throw uploadError
} finally {
setIsUploading(false)
setProgress(null)
}
}
const uploadSingle = async (
blob: Blob,
mimeType: string,
contextString: ContextString
): Promise<{ key: string; presignedUrl: string }> => {
const result = await upload({
images: [{ blob, mimeType }],
contextString,
})
return {
key: result.keys[0],
presignedUrl: result.presignedUrls[0],
}
}
return {
upload,
uploadSingle,
isUploading,
error,
progress,
isPending: generateUploadUrls.isPending,
}
}
function extractKeyFromPresignedUrl(url: string): string {
const parsedUrl = new URL(url)
let rawKey = parsedUrl.pathname.replace(/^\/+/, '')
rawKey = rawKey.split('/').slice(1).join('/')
return decodeURIComponent(rawKey)
}

View file

@ -1,16 +1,15 @@
import { useMutation } from "@tanstack/react-query";
import axios from 'common-ui/src/services/axios';
import { LoginCredentials, RegisterData } from '@/src/types/auth';
import { trpc } from '@/src/trpc-client'
import { LoginCredentials, RegisterData, UpdateProfileData } from '@/src/types/auth'
// API response types
interface RegisterResponse {
token: string;
user: {
id: number;
name: string;
email: string;
mobile: string;
profileImage?: string;
name?: string | null;
email: string | null;
mobile: string | null;
profileImage?: string | null;
createdAt: string;
};
}
@ -19,7 +18,7 @@ interface UpdateProfileResponse {
token: string;
user: {
id: number;
name: string;
name?: string | null;
email: string | null;
mobile: string | null;
profileImage?: string | null;
@ -30,35 +29,27 @@ interface UpdateProfileResponse {
};
}
// API functions
const registerApi = async (data: FormData): Promise<RegisterResponse> => {
const response = await axios.post('/uv/auth/register', data, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return response.data.data; // response.data is {success, data}, we want the inner data
};
const updateProfileApi = async (data: FormData): Promise<UpdateProfileResponse> => {
const response = await axios.put('/uv/auth/profile', data, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return response.data.data; // response.data is {success, data}, we want the inner data
};
// React Query hooks
export const useRegister = () => {
return useMutation({
mutationFn: registerApi,
});
const mutation = trpc.user.auth.register.useMutation()
return {
...mutation,
mutateAsync: async (data: RegisterData): Promise<RegisterResponse> => {
const response = await mutation.mutateAsync(data)
return response.data
},
}
};
export const useUpdateProfile = () => {
return useMutation({
mutationFn: updateProfileApi,
onError: () => {}
});
const mutation = trpc.user.auth.updateProfile.useMutation()
return {
...mutation,
mutateAsync: async (data: UpdateProfileData): Promise<UpdateProfileResponse> => {
const response = await mutation.mutateAsync(data)
return response.data
},
}
};

View file

@ -228,7 +228,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
};
const register = async (data: FormData): Promise<void> => {
const register = async (data: RegisterData): Promise<void> => {
try {
setAuthState(prev => ({ ...prev, isLoading: true }));

View file

@ -37,13 +37,25 @@ export interface RegisterData {
email: string;
mobile: string;
password: string;
profileImage?: string;
profileImageUrl?: string | null;
}
export interface UpdateProfileData {
name?: string;
email?: string;
mobile?: string;
password?: string;
bio?: string | null;
dateOfBirth?: string | null;
gender?: string | null;
occupation?: string | null;
profileImageUrl?: string | null;
}
export interface AuthContextType extends AuthState {
login: (credentials: LoginCredentials) => Promise<void>;
loginWithToken: (token: string, user: User) => Promise<void>;
register: (data: FormData) => Promise<void>;
register: (data: RegisterData) => Promise<void>;
logout: () => Promise<void>;
updateUser: (user: Partial<User>) => void;
updateUserDetails: (userDetails: Partial<UserDetails>) => void;

View file

@ -68,6 +68,7 @@ const mapProduct = (product: ProductRow): AdminProduct => ({
price: product.price.toString(),
marketPrice: product.marketPrice ? product.marketPrice.toString() : null,
images: getStringArray(product.images),
imageKeys: getStringArray(product.images),
isOutOfStock: product.isOutOfStock,
isSuspended: product.isSuspended,
isFlashAvailable: product.isFlashAvailable,

View file

@ -0,0 +1,2 @@
# Copy this to .env and fill in your D1 database URL or local path
DATABASE_URL=file:./dev.db

View file

@ -0,0 +1,43 @@
# Database Helper - SQLite (Cloudflare D1)
This package contains database helpers and schema definitions for Cloudflare D1.
## Structure
- `src/db/` - Database source files
- `schema.ts` - Drizzle ORM schema definitions (SQLite)
- `db_index.ts` - D1 database initializer and client
- `types.ts` - Database types
- `seed.ts` - Database seeding script
- `porter.ts` - Data migration utilities
- `drizzle.config.ts` - Drizzle Kit configuration
## Environment Variables
Create a `.env` file with:
```
DATABASE_URL=file:./dev.db
```
## Initialization (Workers)
Use `initDb` with your D1 binding before calling helpers:
```typescript
import { initDb } from '@packages/db_helper_sqlite'
export default {
async fetch(request: Request, env: Env) {
initDb(env.DB)
// ... call helper methods
},
}
```
## Scripts
- `npm run migrate` - Generate new migration files (SQLite)
- `npm run db:push` - Push schema changes to database
- `npm run db:seed` - Run database seeding
- `npm run db:studio` - Open Drizzle Studio

View file

@ -0,0 +1,11 @@
import 'dotenv/config'
import { defineConfig } from 'drizzle-kit'
export default defineConfig({
out: './drizzle',
schema: './src/db/schema.ts',
dialect: 'sqlite',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
})

View file

@ -0,0 +1,393 @@
// Database Helper - SQLite (Cloudflare D1)
// Main entry point for the package
// Re-export database connection
export { db, initDb } from './src/db/db_index'
// Re-export schema
export * from './src/db/schema'
// Export enum types for type safety
export { staffRoleEnum, staffPermissionEnum } from './src/db/schema'
// Admin API helpers - explicitly namespaced exports to avoid duplicates
export {
// Banner
getBanners,
getBannerById,
createBanner,
updateBanner,
deleteBanner,
} from './src/admin-apis/banner'
export {
// Complaint
getComplaints,
resolveComplaint,
} from './src/admin-apis/complaint'
export {
// Constants
getAllConstants,
upsertConstants,
} from './src/admin-apis/const'
export {
// Coupon
getAllCoupons,
getCouponById,
invalidateCoupon,
validateCoupon,
getReservedCoupons,
getUsersForCoupon,
createCouponWithRelations,
updateCouponWithRelations,
generateCancellationCoupon,
createReservedCouponWithProducts,
createCouponForUser,
checkUsersExist,
checkCouponExists,
checkReservedCouponExists,
getOrderWithUser,
} from './src/admin-apis/coupon'
export {
// Order
updateOrderNotes,
getOrderDetails,
updateOrderPackaged,
updateOrderDelivered,
updateOrderItemPackaging,
removeDeliveryCharge,
getSlotOrders,
updateAddressCoords,
getAllOrders,
rebalanceSlots,
cancelOrder,
deleteOrderById,
} from './src/admin-apis/order'
export {
// Product
getAllProducts,
getProductById,
deleteProduct,
createProduct,
updateProduct,
checkProductExistsByName,
checkUnitExists,
getProductImagesById,
createSpecialDealsForProduct,
updateProductDeals,
replaceProductTags,
toggleProductOutOfStock,
updateSlotProducts,
getSlotProductIds,
getSlotsProductIds,
getAllUnits,
getAllProductTags,
getAllProductTagInfos,
getProductTagInfoById,
createProductTag,
getProductTagById,
updateProductTag,
deleteProductTag,
checkProductTagExistsByName,
getProductReviews,
respondToReview,
getAllProductGroups,
createProductGroup,
updateProductGroup,
deleteProductGroup,
addProductToGroup,
removeProductFromGroup,
updateProductPrices,
} from './src/admin-apis/product'
export {
// Slots
getActiveSlotsWithProducts,
getActiveSlots,
getSlotsAfterDate,
getSlotByIdWithRelations,
createSlotWithRelations,
updateSlotWithRelations,
deleteSlotById,
updateSlotCapacity,
getSlotDeliverySequence,
updateSlotDeliverySequence,
} from './src/admin-apis/slots'
export {
// Staff User
getStaffUserByName,
getStaffUserById,
getAllStaff,
getAllUsers,
getUserWithDetails,
updateUserSuspensionStatus,
checkStaffUserExists,
checkStaffRoleExists,
createStaffUser,
getAllRoles,
} from './src/admin-apis/staff-user'
export {
// Store
getAllStores,
getStoreById,
createStore,
updateStore,
deleteStore,
} from './src/admin-apis/store'
export {
// User
createUserByMobile,
getUserByMobile,
getUnresolvedComplaintsCount,
getAllUsersWithFilters,
getOrderCountsByUserIds,
getLastOrdersByUserIds,
getSuspensionStatusesByUserIds,
getUserBasicInfo,
getUserSuspensionStatus,
getUserOrders,
getOrderStatusesByOrderIds,
getItemCountsByOrderIds,
upsertUserSuspension,
searchUsers,
getAllNotifCreds,
getAllUnloggedTokens,
getNotifTokensByUserIds,
getUserIncidentsWithRelations,
createUserIncident,
} from './src/admin-apis/user'
export {
// Vendor Snippets
checkVendorSnippetExists,
getVendorSnippetById,
getVendorSnippetByCode,
getAllVendorSnippets,
createVendorSnippet,
updateVendorSnippet,
deleteVendorSnippet,
getProductsByIds,
getVendorSlotById,
getVendorOrdersBySlotId,
getOrderItemsByOrderIds,
getOrderStatusByOrderIds,
updateVendorOrderItemPackaging,
getVendorOrders,
} from './src/admin-apis/vendor-snippets'
export {
// User Address
getDefaultAddress as getUserDefaultAddress,
getUserAddresses,
getUserAddressById,
clearDefaultAddress as clearUserDefaultAddress,
createUserAddress,
updateUserAddress,
deleteUserAddress,
hasOngoingOrdersForAddress,
} from './src/user-apis/address'
export {
// User Banners
getActiveBanners as getUserActiveBanners,
} from './src/user-apis/banners'
export {
// User Cart
getCartItemsWithProducts as getUserCartItemsWithProducts,
getProductById as getUserProductById,
getCartItemByUserProduct as getUserCartItemByUserProduct,
incrementCartItemQuantity as incrementUserCartItemQuantity,
insertCartItem as insertUserCartItem,
updateCartItemQuantity as updateUserCartItemQuantity,
deleteCartItem as deleteUserCartItem,
clearUserCart,
} from './src/user-apis/cart'
export {
// User Complaint
getUserComplaints as getUserComplaints,
createComplaint as createUserComplaint,
} from './src/user-apis/complaint'
export {
// User Stores
getStoreSummaries as getUserStoreSummaries,
getStoreDetail as getUserStoreDetail,
} from './src/user-apis/stores'
export {
// User Product
getProductDetailById as getUserProductDetailById,
getProductReviews as getUserProductReviews,
getProductById as getUserProductByIdBasic,
createProductReview as createUserProductReview,
getAllProductsWithUnits,
type ProductSummaryData,
} from './src/user-apis/product'
export {
// User Slots
getActiveSlotsList as getUserActiveSlotsList,
getProductAvailability as getUserProductAvailability,
} from './src/user-apis/slots'
export {
// User Payments
getOrderById as getUserPaymentOrderById,
getPaymentByOrderId as getUserPaymentByOrderId,
getPaymentByMerchantOrderId as getUserPaymentByMerchantOrderId,
updatePaymentSuccess as updateUserPaymentSuccess,
updateOrderPaymentStatus as updateUserOrderPaymentStatus,
markPaymentFailed as markUserPaymentFailed,
} from './src/user-apis/payments'
export {
// User Auth
getUserByEmail as getUserAuthByEmail,
getUserByMobile as getUserAuthByMobile,
getUserById as getUserAuthById,
getUserCreds as getUserAuthCreds,
getUserDetails as getUserAuthDetails,
isUserSuspended,
createUserWithCreds as createUserAuthWithCreds,
createUserWithMobile as createUserAuthWithMobile,
upsertUserPassword as upsertUserAuthPassword,
deleteUserAccount as deleteUserAuthAccount,
// UV API helpers
createUserWithProfile,
getUserDetailsByUserId,
updateUserProfile,
} from './src/user-apis/auth'
export {
// User Coupon
getActiveCouponsWithRelations as getUserActiveCouponsWithRelations,
getAllCouponsWithRelations as getUserAllCouponsWithRelations,
getReservedCouponByCode as getUserReservedCouponByCode,
redeemReservedCoupon as redeemUserReservedCoupon,
} from './src/user-apis/coupon'
export {
// User Profile
getUserById as getUserProfileById,
getUserDetailByUserId as getUserProfileDetailById,
getUserWithCreds as getUserWithCreds,
getNotifCred as getUserNotifCred,
upsertNotifCred as upsertUserNotifCred,
deleteUnloggedToken as deleteUserUnloggedToken,
getUnloggedToken as getUserUnloggedToken,
upsertUnloggedToken as upsertUserUnloggedToken,
} from './src/user-apis/user'
export {
// User Order
validateAndGetCoupon as validateAndGetUserCoupon,
applyDiscountToOrder as applyDiscountToUserOrder,
getAddressByIdAndUser as getUserAddressByIdAndUser,
getProductById as getOrderProductById,
checkUserSuspended,
getSlotCapacityStatus as getUserSlotCapacityStatus,
placeOrderTransaction as placeUserOrderTransaction,
deleteCartItemsForOrder as deleteUserCartItemsForOrder,
recordCouponUsage as recordUserCouponUsage,
getOrdersWithRelations as getUserOrdersWithRelations,
getOrderCount as getUserOrderCount,
getOrderByIdWithRelations as getUserOrderByIdWithRelations,
getCouponUsageForOrder as getUserCouponUsageForOrder,
getOrderBasic as getUserOrderBasic,
cancelOrderTransaction as cancelUserOrderTransaction,
updateOrderNotes as updateUserOrderNotes,
getRecentlyDeliveredOrderIds as getUserRecentlyDeliveredOrderIds,
getProductIdsFromOrders as getUserProductIdsFromOrders,
getProductsForRecentOrders as getUserProductsForRecentOrders,
// Post-order handler helpers
getOrdersByIdsWithFullData,
getOrderByIdWithFullData,
type OrderWithFullData,
type OrderWithCancellationData,
} from './src/user-apis/order'
// Store Helpers (for cache initialization)
export {
// Banner Store
getAllBannersForCache,
type BannerData,
// Product Store
getAllProductsForCache,
getAllStoresForCache,
getAllDeliverySlotsForCache,
getAllSpecialDealsForCache,
getAllProductTagsForCache,
type ProductBasicData,
type StoreBasicData,
type DeliverySlotData,
type SpecialDealData,
type ProductTagData,
// Product Tag Store
getAllTagsForCache,
getAllTagProductMappings,
type TagBasicData,
type TagProductMapping,
// Slot Store
getAllSlotsWithProductsForCache,
type SlotWithProductsData,
// User Negativity Store
getAllUserNegativityScores,
getUserNegativityScore,
type UserNegativityData,
} from './src/stores/store-helpers'
// Automated Jobs Helpers
export {
toggleFlashDeliveryForItems,
toggleKeyVal,
getAllKeyValStore,
} from './src/lib/automated-jobs'
// Health Check
export {
healthCheck,
} from './src/lib/health-check'
// Common API Helpers
export {
getSuspendedProductIds,
getNextDeliveryDateWithCapacity,
} from './src/user-apis/product'
export {
getStoresSummary,
} from './src/user-apis/stores'
// Delete Orders Helper
export {
deleteOrdersWithRelations,
} from './src/lib/delete-orders'
// Upload URL Helpers
export {
createUploadUrlStatus,
claimUploadUrlStatus,
} from './src/helper_methods/upload-url'
// Seed Helpers
export {
seedUnits,
seedStaffRoles,
seedStaffPermissions,
seedRolePermissions,
seedKeyValStore,
type UnitSeedData,
type RolePermissionAssignment,
type KeyValSeedData,
type StaffRoleName,
type StaffPermissionName,
} from './src/lib/seed'

View file

@ -0,0 +1,24 @@
{
"name": "@packages/db_helper_sqlite",
"version": "1.0.0",
"main": "index.ts",
"types": "index.ts",
"private": true,
"scripts": {
"migrate": "drizzle-kit generate:sqlite",
"db:push": "drizzle-kit push:sqlite",
"db:seed": "tsx src/db/seed.ts",
"db:studio": "drizzle-kit studio"
},
"dependencies": {
"dotenv": "^17.2.1",
"drizzle-orm": "^0.44.5"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20260304.0",
"@types/node": "^24.5.2",
"drizzle-kit": "^0.31.4",
"tsx": "^4.20.5",
"typescript": "^5.9.2"
}
}

View file

@ -0,0 +1,114 @@
import { db } from '../db/db_index'
import { homeBanners } from '../db/schema'
import { eq, desc } from 'drizzle-orm'
export interface Banner {
id: number
name: string
imageUrl: string
description: string | null
productIds: number[] | null
redirectUrl: string | null
serialNum: number | null
isActive: boolean
createdAt: Date
lastUpdated: Date
}
type BannerRow = typeof homeBanners.$inferSelect
export async function getBanners(): Promise<Banner[]> {
const banners = await db.query.homeBanners.findMany({
orderBy: desc(homeBanners.createdAt),
}) as BannerRow[]
return banners.map((banner) => ({
id: banner.id,
name: banner.name,
imageUrl: banner.imageUrl,
description: banner.description,
productIds: banner.productIds || [],
redirectUrl: banner.redirectUrl,
serialNum: banner.serialNum,
isActive: banner.isActive,
createdAt: banner.createdAt,
lastUpdated: banner.lastUpdated,
}))
}
export async function getBannerById(id: number): Promise<Banner | null> {
const banner = await db.query.homeBanners.findFirst({
where: eq(homeBanners.id, id),
})
if (!banner) return null
return {
id: banner.id,
name: banner.name,
imageUrl: banner.imageUrl,
description: banner.description,
productIds: banner.productIds || [],
redirectUrl: banner.redirectUrl,
serialNum: banner.serialNum,
isActive: banner.isActive,
createdAt: banner.createdAt,
lastUpdated: banner.lastUpdated,
}
}
export type CreateBannerInput = Omit<Banner, 'id' | 'createdAt' | 'lastUpdated'>
export async function createBanner(input: CreateBannerInput): Promise<Banner> {
const [banner] = await db.insert(homeBanners).values({
name: input.name,
imageUrl: input.imageUrl,
description: input.description,
productIds: input.productIds || [],
redirectUrl: input.redirectUrl,
serialNum: input.serialNum,
isActive: input.isActive,
}).returning()
return {
id: banner.id,
name: banner.name,
imageUrl: banner.imageUrl,
description: banner.description,
productIds: banner.productIds || [],
redirectUrl: banner.redirectUrl,
serialNum: banner.serialNum,
isActive: banner.isActive,
createdAt: banner.createdAt,
lastUpdated: banner.lastUpdated,
}
}
export type UpdateBannerInput = Partial<Omit<Banner, 'id' | 'createdAt'>>
export async function updateBanner(id: number, input: UpdateBannerInput): Promise<Banner> {
const [banner] = await db.update(homeBanners)
.set({
...input,
lastUpdated: new Date(),
})
.where(eq(homeBanners.id, id))
.returning()
return {
id: banner.id,
name: banner.name,
imageUrl: banner.imageUrl,
description: banner.description,
productIds: banner.productIds || [],
redirectUrl: banner.redirectUrl,
serialNum: banner.serialNum,
isActive: banner.isActive,
createdAt: banner.createdAt,
lastUpdated: banner.lastUpdated,
}
}
export async function deleteBanner(id: number): Promise<void> {
await db.delete(homeBanners).where(eq(homeBanners.id, id))
}

View file

@ -0,0 +1,74 @@
import { db } from '../db/db_index'
import { complaints, users } from '../db/schema'
import { eq, desc, lt } from 'drizzle-orm'
export interface Complaint {
id: number
complaintBody: string
userId: number
orderId: number | null
isResolved: boolean
response: string | null
createdAt: Date
images: string[] | null
}
export interface ComplaintWithUser extends Complaint {
userName: string | null
userMobile: string | null
}
export async function getComplaints(
cursor?: number,
limit: number = 20
): Promise<{ complaints: ComplaintWithUser[]; hasMore: boolean }> {
const whereCondition = cursor ? lt(complaints.id, cursor) : undefined
const complaintsData = await db
.select({
id: complaints.id,
complaintBody: complaints.complaintBody,
userId: complaints.userId,
orderId: complaints.orderId,
isResolved: complaints.isResolved,
response: complaints.response,
createdAt: complaints.createdAt,
images: complaints.images,
userName: users.name,
userMobile: users.mobile,
})
.from(complaints)
.leftJoin(users, eq(complaints.userId, users.id))
.where(whereCondition)
.orderBy(desc(complaints.id))
.limit(limit + 1)
const hasMore = complaintsData.length > limit
const complaintsToReturn = hasMore ? complaintsData.slice(0, limit) : complaintsData
return {
complaints: complaintsToReturn.map((c) => ({
id: c.id,
complaintBody: c.complaintBody,
userId: c.userId,
orderId: c.orderId,
isResolved: c.isResolved,
response: c.response,
createdAt: c.createdAt,
images: c.images as string[],
userName: c.userName,
userMobile: c.userMobile,
})),
hasMore,
}
}
export async function resolveComplaint(
id: number,
response?: string
): Promise<void> {
await db
.update(complaints)
.set({ isResolved: true, response })
.where(eq(complaints.id, id))
}

View file

@ -0,0 +1,29 @@
import { db } from '../db/db_index'
import { keyValStore } from '../db/schema'
export interface Constant {
key: string
value: any
}
export async function getAllConstants(): Promise<Constant[]> {
const constants = await db.select().from(keyValStore)
return constants.map(c => ({
key: c.key,
value: c.value,
}))
}
export async function upsertConstants(constants: Constant[]): Promise<void> {
await db.transaction(async (tx) => {
for (const { key, value } of constants) {
await tx.insert(keyValStore)
.values({ key, value })
.onConflictDoUpdate({
target: keyValStore.key,
set: { value },
})
}
})
}

View file

@ -0,0 +1,496 @@
import { db } from '../db/db_index'
import { coupons, reservedCoupons, users, orders, orderStatus, couponApplicableUsers, couponApplicableProducts } from '../db/schema'
import { eq, and, like, or, inArray, lt, desc, asc } from 'drizzle-orm'
export interface Coupon {
id: number
couponCode: string
isUserBased: boolean
discountPercent: string | null
flatDiscount: string | null
minOrder: string | null
productIds: number[] | null
maxValue: string | null
isApplyForAll: boolean
validTill: Date | null
maxLimitForUser: number | null
exclusiveApply: boolean
isInvalidated: boolean
createdAt: Date
createdBy: number
}
export async function getAllCoupons(
cursor?: number,
limit: number = 50,
search?: string
): Promise<{ coupons: any[]; hasMore: boolean }> {
let whereCondition = undefined
const conditions = []
if (cursor) {
conditions.push(lt(coupons.id, cursor))
}
if (search && search.trim()) {
conditions.push(like(coupons.couponCode, `%${search}%`))
}
if (conditions.length > 0) {
whereCondition = and(...conditions)
}
const result = await db.query.coupons.findMany({
where: whereCondition,
with: {
creator: true,
applicableUsers: {
with: {
user: true,
},
},
applicableProducts: {
with: {
product: true,
},
},
},
orderBy: desc(coupons.createdAt),
limit: limit + 1,
})
const hasMore = result.length > limit
const couponsList = hasMore ? result.slice(0, limit) : result
return { coupons: couponsList, hasMore }
}
export async function getCouponById(id: number): Promise<any | null> {
return await db.query.coupons.findFirst({
where: eq(coupons.id, id),
with: {
creator: true,
applicableUsers: {
with: {
user: true,
},
},
applicableProducts: {
with: {
product: true,
},
},
},
})
}
export interface CreateCouponInput {
couponCode: string
isUserBased: boolean
discountPercent?: string
flatDiscount?: string
minOrder?: string
productIds?: number[] | null
maxValue?: string
isApplyForAll: boolean
validTill?: Date
maxLimitForUser?: number
exclusiveApply: boolean
createdBy: number
}
export async function createCouponWithRelations(
input: CreateCouponInput,
applicableUsers?: number[],
applicableProducts?: number[]
): Promise<Coupon> {
return await db.transaction(async (tx) => {
const [coupon] = await tx.insert(coupons).values({
couponCode: input.couponCode,
isUserBased: input.isUserBased,
discountPercent: input.discountPercent,
flatDiscount: input.flatDiscount,
minOrder: input.minOrder,
productIds: input.productIds,
createdBy: input.createdBy,
maxValue: input.maxValue,
isApplyForAll: input.isApplyForAll,
validTill: input.validTill,
maxLimitForUser: input.maxLimitForUser,
exclusiveApply: input.exclusiveApply,
}).returning()
if (applicableUsers && applicableUsers.length > 0) {
await tx.insert(couponApplicableUsers).values(
applicableUsers.map(userId => ({
couponId: coupon.id,
userId,
}))
)
}
if (applicableProducts && applicableProducts.length > 0) {
await tx.insert(couponApplicableProducts).values(
applicableProducts.map(productId => ({
couponId: coupon.id,
productId,
}))
)
}
return coupon as Coupon
})
}
export interface UpdateCouponInput {
couponCode?: string
isUserBased?: boolean
discountPercent?: string
flatDiscount?: string
minOrder?: string
productIds?: number[] | null
maxValue?: string
isApplyForAll?: boolean
validTill?: Date | null
maxLimitForUser?: number
exclusiveApply?: boolean
isInvalidated?: boolean
}
export async function updateCouponWithRelations(
id: number,
input: UpdateCouponInput,
applicableUsers?: number[],
applicableProducts?: number[]
): Promise<Coupon> {
return await db.transaction(async (tx) => {
const [coupon] = await tx.update(coupons)
.set({
...input,
})
.where(eq(coupons.id, id))
.returning()
if (applicableUsers !== undefined) {
await tx.delete(couponApplicableUsers).where(eq(couponApplicableUsers.couponId, id))
if (applicableUsers.length > 0) {
await tx.insert(couponApplicableUsers).values(
applicableUsers.map(userId => ({
couponId: id,
userId,
}))
)
}
}
if (applicableProducts !== undefined) {
await tx.delete(couponApplicableProducts).where(eq(couponApplicableProducts.couponId, id))
if (applicableProducts.length > 0) {
await tx.insert(couponApplicableProducts).values(
applicableProducts.map(productId => ({
couponId: id,
productId,
}))
)
}
}
return coupon as Coupon
})
}
export async function invalidateCoupon(id: number): Promise<Coupon> {
const result = await db.update(coupons)
.set({ isInvalidated: true })
.where(eq(coupons.id, id))
.returning()
return result[0] as Coupon
}
export interface CouponValidationResult {
valid: boolean
message?: string
discountAmount?: number
coupon?: Partial<Coupon>
}
export async function validateCoupon(
code: string,
userId: number,
orderAmount: number
): Promise<CouponValidationResult> {
const coupon = await db.query.coupons.findFirst({
where: and(
eq(coupons.couponCode, code.toUpperCase()),
eq(coupons.isInvalidated, false)
),
})
if (!coupon) {
return { valid: false, message: 'Coupon not found or invalidated' }
}
if (coupon.validTill && new Date(coupon.validTill) < new Date()) {
return { valid: false, message: 'Coupon has expired' }
}
if (!coupon.isApplyForAll && !coupon.isUserBased) {
return { valid: false, message: 'Coupon is not available for use' }
}
const minOrderValue = coupon.minOrder ? parseFloat(coupon.minOrder) : 0
if (minOrderValue > 0 && orderAmount < minOrderValue) {
return { valid: false, message: `Minimum order amount is ${minOrderValue}` }
}
let discountAmount = 0
if (coupon.discountPercent) {
const percent = parseFloat(coupon.discountPercent)
discountAmount = (orderAmount * percent) / 100
} else if (coupon.flatDiscount) {
discountAmount = parseFloat(coupon.flatDiscount)
}
const maxValueLimit = coupon.maxValue ? parseFloat(coupon.maxValue) : 0
if (maxValueLimit > 0 && discountAmount > maxValueLimit) {
discountAmount = maxValueLimit
}
return {
valid: true,
discountAmount,
coupon: {
id: coupon.id,
discountPercent: coupon.discountPercent,
flatDiscount: coupon.flatDiscount,
maxValue: coupon.maxValue,
}
}
}
export async function getReservedCoupons(
cursor?: number,
limit: number = 50,
search?: string
): Promise<{ coupons: any[]; hasMore: boolean }> {
let whereCondition = undefined
const conditions = []
if (cursor) {
conditions.push(lt(reservedCoupons.id, cursor))
}
if (search && search.trim()) {
conditions.push(or(
like(reservedCoupons.secretCode, `%${search}%`),
like(reservedCoupons.couponCode, `%${search}%`)
))
}
if (conditions.length > 0) {
whereCondition = and(...conditions)
}
const result = await db.query.reservedCoupons.findMany({
where: whereCondition,
with: {
redeemedUser: true,
creator: true,
},
orderBy: desc(reservedCoupons.createdAt),
limit: limit + 1,
})
const hasMore = result.length > limit
const couponsList = hasMore ? result.slice(0, limit) : result
return { coupons: couponsList, hasMore }
}
export async function createReservedCouponWithProducts(
input: any,
applicableProducts?: number[]
): Promise<any> {
return await db.transaction(async (tx) => {
const [coupon] = await tx.insert(reservedCoupons).values({
secretCode: input.secretCode,
couponCode: input.couponCode,
discountPercent: input.discountPercent,
flatDiscount: input.flatDiscount,
minOrder: input.minOrder,
productIds: input.productIds,
maxValue: input.maxValue,
validTill: input.validTill,
maxLimitForUser: input.maxLimitForUser,
exclusiveApply: input.exclusiveApply,
createdBy: input.createdBy,
}).returning()
if (applicableProducts && applicableProducts.length > 0) {
await tx.insert(couponApplicableProducts).values(
applicableProducts.map(productId => ({
couponId: coupon.id,
productId,
}))
)
}
return coupon
})
}
export async function checkUsersExist(userIds: number[]): Promise<boolean> {
const existingUsers = await db.query.users.findMany({
where: inArray(users.id, userIds),
columns: { id: true },
})
return existingUsers.length === userIds.length
}
export async function checkCouponExists(couponCode: string): Promise<boolean> {
const existing = await db.query.coupons.findFirst({
where: eq(coupons.couponCode, couponCode),
})
return !!existing
}
export async function checkReservedCouponExists(secretCode: string): Promise<boolean> {
const existing = await db.query.reservedCoupons.findFirst({
where: eq(reservedCoupons.secretCode, secretCode),
})
return !!existing
}
export async function generateCancellationCoupon(
orderId: number,
staffUserId: number,
userId: number,
orderAmount: number,
couponCode: string
): Promise<Coupon> {
return await db.transaction(async (tx) => {
const expiryDate = new Date()
expiryDate.setDate(expiryDate.getDate() + 30)
const [coupon] = await tx.insert(coupons).values({
couponCode,
isUserBased: true,
flatDiscount: orderAmount.toString(),
minOrder: orderAmount.toString(),
maxValue: orderAmount.toString(),
validTill: expiryDate,
maxLimitForUser: 1,
createdBy: staffUserId,
isApplyForAll: false,
}).returning()
await tx.insert(couponApplicableUsers).values({
couponId: coupon.id,
userId,
})
await tx.update(orderStatus)
.set({ refundCouponId: coupon.id })
.where(eq(orderStatus.orderId, orderId))
return coupon as Coupon
})
}
export async function getOrderWithUser(orderId: number): Promise<any | null> {
return await db.query.orders.findFirst({
where: eq(orders.id, orderId),
with: {
user: true,
},
})
}
export async function createCouponForUser(
mobile: string,
couponCode: string,
staffUserId: number
): Promise<{ coupon: Coupon; user: { id: number; mobile: string; name: string | null } }> {
return await db.transaction(async (tx) => {
let user = await tx.query.users.findFirst({
where: eq(users.mobile, mobile),
})
if (!user) {
const [newUser] = await tx.insert(users).values({
name: null,
email: null,
mobile,
}).returning()
user = newUser
}
const [coupon] = await tx.insert(coupons).values({
couponCode,
isUserBased: true,
discountPercent: '20',
minOrder: '1000',
maxValue: '500',
maxLimitForUser: 1,
isApplyForAll: false,
exclusiveApply: false,
createdBy: staffUserId,
validTill: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000),
}).returning()
await tx.insert(couponApplicableUsers).values({
couponId: coupon.id,
userId: user.id,
})
return {
coupon: coupon as Coupon,
user: {
id: user.id,
mobile: user.mobile as string,
name: user.name,
},
}
})
}
export interface UserMiniInfo {
id: number
name: string
mobile: string | null
}
export async function getUsersForCoupon(
search?: string,
limit: number = 20,
offset: number = 0
): Promise<{ users: UserMiniInfo[] }> {
let whereCondition = undefined
if (search && search.trim()) {
whereCondition = or(
like(users.name, `%${search}%`),
like(users.mobile, `%${search}%`)
)
}
const userList = await db.query.users.findMany({
where: whereCondition,
columns: {
id: true,
name: true,
mobile: true,
},
limit: limit,
offset: offset,
orderBy: asc(users.name),
})
return {
users: userList.map((user: typeof users.$inferSelect) => ({
id: user.id,
name: user.name || 'Unknown',
mobile: user.mobile,
}))
}
}

View file

@ -0,0 +1,710 @@
import { db } from '../db/db_index'
import {
addresses,
complaints,
couponUsage,
orderItems,
orders,
orderStatus,
payments,
refunds,
} from '../db/schema'
import { and, desc, eq, inArray, lt, SQL } from 'drizzle-orm'
import type {
AdminOrderDetails,
AdminOrderRow,
AdminOrderStatusRecord,
AdminOrderUpdateResult,
AdminOrderItemPackagingResult,
AdminOrderMessageResult,
AdminOrderBasicResult,
AdminGetSlotOrdersResult,
AdminGetAllOrdersResultWithUserId,
AdminRebalanceSlotsResult,
AdminCancelOrderResult,
AdminRefundRecord,
RefundStatus,
PaymentStatus,
} from '@packages/shared'
import type { InferSelectModel } from 'drizzle-orm'
const isPaymentStatus = (value: string): value is PaymentStatus =>
value === 'pending' || value === 'success' || value === 'cod' || value === 'failed'
const isRefundStatus = (value: string): value is RefundStatus =>
value === 'success' || value === 'pending' || value === 'failed' || value === 'none' || value === 'na' || value === 'processed'
type OrderStatusRow = InferSelectModel<typeof orderStatus>
const mapOrderStatusRecord = (record: OrderStatusRow): AdminOrderStatusRecord => ({
id: record.id,
orderTime: record.orderTime,
userId: record.userId,
orderId: record.orderId,
isPackaged: record.isPackaged,
isDelivered: record.isDelivered,
isCancelled: record.isCancelled,
cancelReason: record.cancelReason ?? null,
isCancelledByAdmin: record.isCancelledByAdmin ?? null,
paymentStatus: isPaymentStatus(record.paymentStatus) ? record.paymentStatus : 'pending',
cancellationUserNotes: record.cancellationUserNotes ?? null,
cancellationAdminNotes: record.cancellationAdminNotes ?? null,
cancellationReviewed: record.cancellationReviewed,
cancellationReviewedAt: record.cancellationReviewedAt ?? null,
refundCouponId: record.refundCouponId ?? null,
})
export async function updateOrderNotes(orderId: number, adminNotes: string | null): Promise<AdminOrderRow | null> {
const [result] = await db
.update(orders)
.set({ adminNotes })
.where(eq(orders.id, orderId))
.returning()
return (result || null) as AdminOrderRow | null
}
export async function updateOrderPackaged(orderId: string, isPackaged: boolean): Promise<AdminOrderUpdateResult> {
const orderIdNumber = parseInt(orderId)
await db
.update(orderItems)
.set({ is_packaged: isPackaged })
.where(eq(orderItems.orderId, orderIdNumber))
if (!isPackaged) {
await db
.update(orderStatus)
.set({ isPackaged, isDelivered: false })
.where(eq(orderStatus.orderId, orderIdNumber))
} else {
await db
.update(orderStatus)
.set({ isPackaged })
.where(eq(orderStatus.orderId, orderIdNumber))
}
const order = await db.query.orders.findFirst({
where: eq(orders.id, orderIdNumber),
})
return { success: true, userId: order?.userId ?? null }
}
export async function updateOrderDelivered(orderId: string, isDelivered: boolean): Promise<AdminOrderUpdateResult> {
const orderIdNumber = parseInt(orderId)
await db
.update(orderStatus)
.set({ isDelivered })
.where(eq(orderStatus.orderId, orderIdNumber))
const order = await db.query.orders.findFirst({
where: eq(orders.id, orderIdNumber),
})
return { success: true, userId: order?.userId ?? null }
}
export async function getOrderDetails(orderId: number): Promise<AdminOrderDetails | null> {
// Single optimized query with all relations
const orderData = await db.query.orders.findFirst({
where: eq(orders.id, orderId),
with: {
user: true,
address: true,
slot: true,
orderItems: {
with: {
product: {
with: {
unit: true,
},
},
},
},
payment: true,
paymentInfo: true,
orderStatus: true,
refunds: true,
},
})
if (!orderData) {
return null
}
const couponUsageData = await db.query.couponUsage.findMany({
where: eq(couponUsage.orderId, orderData.id),
with: {
coupon: true,
},
})
let couponData = null
if (couponUsageData.length > 0) {
let totalDiscountAmount = 0
const orderTotal = parseFloat(orderData.totalAmount.toString())
for (const usage of couponUsageData) {
let discountAmount = 0
if (usage.coupon.discountPercent) {
discountAmount =
(orderTotal * parseFloat(usage.coupon.discountPercent.toString())) /
100
} else if (usage.coupon.flatDiscount) {
discountAmount = parseFloat(usage.coupon.flatDiscount.toString())
}
if (
usage.coupon.maxValue &&
discountAmount > parseFloat(usage.coupon.maxValue.toString())
) {
discountAmount = parseFloat(usage.coupon.maxValue.toString())
}
totalDiscountAmount += discountAmount
}
couponData = {
couponCode: couponUsageData.map((u: any) => u.coupon.couponCode).join(', '),
couponDescription: `${couponUsageData.length} coupons applied`,
discountAmount: totalDiscountAmount,
}
}
const statusRecord = orderData.orderStatus?.[0]
const orderStatusRecord = statusRecord ? mapOrderStatusRecord(statusRecord) : null
let status: 'pending' | 'delivered' | 'cancelled' = 'pending'
if (orderStatusRecord?.isCancelled) {
status = 'cancelled'
} else if (orderStatusRecord?.isDelivered) {
status = 'delivered'
}
const refund = orderData.refunds?.[0]
const refundStatus = refund?.refundStatus && isRefundStatus(refund.refundStatus)
? refund.refundStatus
: null
const refundRecord: AdminRefundRecord | null = refund
? {
id: refund.id,
orderId: refund.orderId,
refundAmount: refund.refundAmount,
refundStatus,
merchantRefundId: refund.merchantRefundId,
refundProcessedAt: refund.refundProcessedAt,
createdAt: refund.createdAt,
}
: null
return {
id: orderData.id,
readableId: orderData.id,
userId: orderData.user.id,
customerName: `${orderData.user.name}`,
customerEmail: orderData.user.email,
customerMobile: orderData.user.mobile,
address: {
name: orderData.address.name,
line1: orderData.address.addressLine1,
line2: orderData.address.addressLine2,
city: orderData.address.city,
state: orderData.address.state,
pincode: orderData.address.pincode,
phone: orderData.address.phone,
},
slotInfo: orderData.slot
? {
time: orderData.slot.deliveryTime.toISOString(),
sequence: orderData.slot.deliverySequence,
}
: null,
isCod: orderData.isCod,
isOnlinePayment: orderData.isOnlinePayment,
totalAmount:
parseFloat(orderData.totalAmount?.toString() || '0') -
parseFloat(orderData.deliveryCharge?.toString() || '0'),
deliveryCharge: parseFloat(orderData.deliveryCharge?.toString() || '0'),
adminNotes: orderData.adminNotes,
userNotes: orderData.userNotes,
createdAt: orderData.createdAt,
status,
isPackaged: orderStatusRecord?.isPackaged || false,
isDelivered: orderStatusRecord?.isDelivered || false,
items: orderData.orderItems.map((item: any) => ({
id: item.id,
name: item.product.name,
quantity: item.quantity,
productSize: item.product.productQuantity,
price: item.price,
unit: item.product.unit?.shortNotation,
amount: parseFloat(item.price.toString()) * parseFloat(item.quantity || '0'),
isPackaged: item.is_packaged,
isPackageVerified: item.is_package_verified,
})),
payment: orderData.payment
? {
status: orderData.payment.status,
gateway: orderData.payment.gateway,
merchantOrderId: orderData.payment.merchantOrderId,
}
: null,
paymentInfo: orderData.paymentInfo
? {
status: orderData.paymentInfo.status,
gateway: orderData.paymentInfo.gateway,
merchantOrderId: orderData.paymentInfo.merchantOrderId,
}
: null,
cancelReason: orderStatusRecord?.cancelReason || null,
cancellationReviewed: orderStatusRecord?.cancellationReviewed || false,
isRefundDone: refundStatus === 'processed' || false,
refundStatus,
refundAmount: refund?.refundAmount
? parseFloat(refund.refundAmount.toString())
: null,
couponData,
couponCode: couponData?.couponCode || null,
couponDescription: couponData?.couponDescription || null,
discountAmount: couponData?.discountAmount || null,
orderStatus: orderStatusRecord,
refundRecord,
isFlashDelivery: orderData.isFlashDelivery,
}
}
export async function updateOrderItemPackaging(
orderItemId: number,
isPackaged?: boolean,
isPackageVerified?: boolean
): Promise<AdminOrderItemPackagingResult> {
const orderItem = await db.query.orderItems.findFirst({
where: eq(orderItems.id, orderItemId),
})
if (!orderItem) {
return { success: false, updated: false }
}
const updateData: Partial<{
is_packaged: boolean
is_package_verified: boolean
}> = {}
if (isPackaged !== undefined) {
updateData.is_packaged = isPackaged
}
if (isPackageVerified !== undefined) {
updateData.is_package_verified = isPackageVerified
}
await db
.update(orderItems)
.set(updateData)
.where(eq(orderItems.id, orderItemId))
return { success: true, updated: true }
}
export async function removeDeliveryCharge(orderId: number): Promise<AdminOrderMessageResult | null> {
const order = await db.query.orders.findFirst({
where: eq(orders.id, orderId),
})
if (!order) {
return null
}
const currentDeliveryCharge = parseFloat(order.deliveryCharge?.toString() || '0')
const currentTotalAmount = parseFloat(order.totalAmount?.toString() || '0')
const newTotalAmount = currentTotalAmount - currentDeliveryCharge
await db
.update(orders)
.set({
deliveryCharge: '0',
totalAmount: newTotalAmount.toString(),
})
.where(eq(orders.id, orderId))
return { success: true, message: 'Delivery charge removed' }
}
export async function getSlotOrders(slotId: string): Promise<AdminGetSlotOrdersResult> {
const slotOrders = await db.query.orders.findMany({
where: eq(orders.slotId, parseInt(slotId)),
with: {
user: true,
address: true,
slot: true,
orderItems: {
with: {
product: {
with: {
unit: true,
},
},
},
},
orderStatus: true,
},
})
const filteredOrders = slotOrders.filter((order: any) => {
const statusRecord = order.orderStatus[0]
return order.isCod || (statusRecord && statusRecord.paymentStatus === 'success')
})
const formattedOrders = filteredOrders.map((order: any) => {
const statusRecord = order.orderStatus[0]
let status: 'pending' | 'delivered' | 'cancelled' = 'pending'
if (statusRecord?.isCancelled) {
status = 'cancelled'
} else if (statusRecord?.isDelivered) {
status = 'delivered'
}
const items = order.orderItems.map((item: any) => ({
id: item.id,
name: item.product.name,
quantity: parseFloat(item.quantity),
price: parseFloat(item.price.toString()),
amount: parseFloat(item.quantity) * parseFloat(item.price.toString()),
unit: item.product.unit?.shortNotation || '',
isPackaged: item.is_packaged,
isPackageVerified: item.is_package_verified,
}))
const paymentMode: 'COD' | 'Online' = order.isCod ? 'COD' : 'Online'
return {
id: order.id,
readableId: order.id,
customerName: order.user.name || order.user.mobile+'',
address: `${order.address.addressLine1}${
order.address.addressLine2 ? `, ${order.address.addressLine2}` : ''
}, ${order.address.city}, ${order.address.state} - ${
order.address.pincode
}, Phone: ${order.address.phone}`,
addressId: order.addressId,
latitude: order.address.adminLatitude ?? order.address.latitude,
longitude: order.address.adminLongitude ?? order.address.longitude,
totalAmount: parseFloat(order.totalAmount),
items,
deliveryTime: order.slot?.deliveryTime.toISOString() || null,
status,
isPackaged: order.orderItems.every((item: any) => item.is_packaged) || false,
isDelivered: statusRecord?.isDelivered || false,
isCod: order.isCod,
paymentMode,
paymentStatus: isPaymentStatus(statusRecord?.paymentStatus || 'pending')
? statusRecord?.paymentStatus || 'pending'
: 'pending',
slotId: order.slotId,
adminNotes: order.adminNotes,
userNotes: order.userNotes,
}
})
return { success: true, data: formattedOrders }
}
export async function updateAddressCoords(
addressId: number,
latitude: number,
longitude: number
): Promise<AdminOrderBasicResult> {
const result = await db
.update(addresses)
.set({
adminLatitude: latitude,
adminLongitude: longitude,
})
.where(eq(addresses.id, addressId))
.returning()
return { success: result.length > 0 }
}
type GetAllOrdersInput = {
cursor?: number
limit: number
slotId?: number | null
packagedFilter?: 'all' | 'packaged' | 'not_packaged'
deliveredFilter?: 'all' | 'delivered' | 'not_delivered'
cancellationFilter?: 'all' | 'cancelled' | 'not_cancelled'
flashDeliveryFilter?: 'all' | 'flash' | 'regular'
}
export async function getAllOrders(input: GetAllOrdersInput): Promise<AdminGetAllOrdersResultWithUserId> {
const {
cursor,
limit,
slotId,
packagedFilter,
deliveredFilter,
cancellationFilter,
flashDeliveryFilter,
} = input
let whereCondition: SQL<unknown> | undefined = eq(orders.id, orders.id)
if (cursor) {
whereCondition = and(whereCondition, lt(orders.id, cursor))
}
if (slotId) {
whereCondition = and(whereCondition, eq(orders.slotId, slotId))
}
if (packagedFilter === 'packaged') {
whereCondition = and(whereCondition, eq(orderStatus.isPackaged, true))
} else if (packagedFilter === 'not_packaged') {
whereCondition = and(whereCondition, eq(orderStatus.isPackaged, false))
}
if (deliveredFilter === 'delivered') {
whereCondition = and(whereCondition, eq(orderStatus.isDelivered, true))
} else if (deliveredFilter === 'not_delivered') {
whereCondition = and(whereCondition, eq(orderStatus.isDelivered, false))
}
if (cancellationFilter === 'cancelled') {
whereCondition = and(whereCondition, eq(orderStatus.isCancelled, true))
} else if (cancellationFilter === 'not_cancelled') {
whereCondition = and(whereCondition, eq(orderStatus.isCancelled, false))
}
if (flashDeliveryFilter === 'flash') {
whereCondition = and(whereCondition, eq(orders.isFlashDelivery, true))
} else if (flashDeliveryFilter === 'regular') {
whereCondition = and(whereCondition, eq(orders.isFlashDelivery, false))
}
const allOrders = await db.query.orders.findMany({
where: whereCondition,
orderBy: desc(orders.createdAt),
limit: limit + 1,
with: {
user: true,
address: true,
slot: true,
orderItems: {
with: {
product: {
with: {
unit: true,
},
},
},
},
orderStatus: true,
},
})
const hasMore = allOrders.length > limit
const ordersToReturn = hasMore ? allOrders.slice(0, limit) : allOrders
const filteredOrders = ordersToReturn.filter((order: any) => {
const statusRecord = order.orderStatus[0]
return order.isCod || (statusRecord && statusRecord.paymentStatus === 'success')
})
const formattedOrders = filteredOrders.map((order: any) => {
const statusRecord = order.orderStatus[0]
let status: 'pending' | 'delivered' | 'cancelled' = 'pending'
if (statusRecord?.isCancelled) {
status = 'cancelled'
} else if (statusRecord?.isDelivered) {
status = 'delivered'
}
const items = order.orderItems
.map((item: any) => ({
id: item.id,
name: item.product.name,
quantity: parseFloat(item.quantity),
price: parseFloat(item.price.toString()),
amount: parseFloat(item.quantity) * parseFloat(item.price.toString()),
unit: item.product.unit?.shortNotation || '',
productSize: item.product.productQuantity,
isPackaged: item.is_packaged,
isPackageVerified: item.is_package_verified,
}))
.sort((first: any, second: any) => first.id - second.id)
return {
id: order.id,
orderId: order.id.toString(),
readableId: order.id,
customerName: order.user.name || order.user.mobile + '',
customerMobile: order.user.mobile,
address: `${order.address.addressLine1}${
order.address.addressLine2 ? `, ${order.address.addressLine2}` : ''
}, ${order.address.city}, ${order.address.state} - ${
order.address.pincode
}, Phone: ${order.address.phone}`,
addressId: order.addressId,
latitude: order.address.adminLatitude ?? order.address.latitude,
longitude: order.address.adminLongitude ?? order.address.longitude,
totalAmount: parseFloat(order.totalAmount),
deliveryCharge: parseFloat(order.deliveryCharge || '0'),
items,
createdAt: order.createdAt,
deliveryTime: order.slot?.deliveryTime.toISOString() || null,
status,
isPackaged: order.orderItems.every((item: any) => item.is_packaged) || false,
isDelivered: statusRecord?.isDelivered || false,
isCod: order.isCod,
isFlashDelivery: order.isFlashDelivery,
userNotes: order.userNotes,
adminNotes: order.adminNotes,
userNegativityScore: 0,
userId: order.userId,
}
})
return {
orders: formattedOrders,
nextCursor: hasMore ? ordersToReturn[ordersToReturn.length - 1].id : undefined,
}
}
export async function rebalanceSlots(slotIds: number[]): Promise<AdminRebalanceSlotsResult> {
const ordersList = await db.query.orders.findMany({
where: inArray(orders.slotId, slotIds),
with: {
orderItems: {
with: {
product: true,
},
},
couponUsages: {
with: {
coupon: true,
},
},
},
})
const processedOrdersData = ordersList.map((order: any) => {
let newTotal = order.orderItems.reduce((acc: number, item: any) => {
const latestPrice = +item.product.price
const amount = latestPrice * Number(item.quantity)
return acc + amount
}, 0)
order.orderItems.forEach((item: any) => {
item.price = item.product.price
item.discountedPrice = item.product.price
})
const coupon = order.couponUsages[0]?.coupon
let discount = 0
if (coupon && !coupon.isInvalidated && (!coupon.validTill || new Date(coupon.validTill) > new Date())) {
const proportion = Number(order.orderGroupProportion || 1)
if (coupon.discountPercent) {
const maxDiscount = Number(coupon.maxValue || Infinity) * proportion
discount = Math.min((newTotal * parseFloat(coupon.discountPercent)) / 100, maxDiscount)
} else {
discount = Number(coupon.flatDiscount) * proportion
}
}
newTotal -= discount
const { couponUsages, orderItems: orderItemsRaw, ...rest } = order
const updatedOrderItems = orderItemsRaw.map((item: any) => {
const { product, ...rawOrderItem } = item
return rawOrderItem
})
return { order: rest, updatedOrderItems, newTotal }
})
const updatedOrderIds: number[] = []
await db.transaction(async (tx) => {
for (const { order, updatedOrderItems, newTotal } of processedOrdersData) {
await tx.update(orders).set({ totalAmount: newTotal.toString() }).where(eq(orders.id, order.id))
updatedOrderIds.push(order.id)
for (const item of updatedOrderItems) {
await tx
.update(orderItems)
.set({
price: item.price,
discountedPrice: item.discountedPrice,
})
.where(eq(orderItems.id, item.id))
}
}
})
return {
success: true,
updatedOrders: updatedOrderIds,
message: `Rebalanced ${updatedOrderIds.length} orders.`,
}
}
export async function cancelOrder(orderId: number, reason: string): Promise<AdminCancelOrderResult> {
const order = await db.query.orders.findFirst({
where: eq(orders.id, orderId),
with: {
orderStatus: true,
},
})
if (!order) {
return { success: false, message: 'Order not found', error: 'order_not_found' }
}
const status = order.orderStatus[0]
if (!status) {
return { success: false, message: 'Order status not found', error: 'status_not_found' }
}
if (status.isCancelled) {
return { success: false, message: 'Order is already cancelled', error: 'already_cancelled' }
}
if (status.isDelivered) {
return { success: false, message: 'Cannot cancel delivered order', error: 'already_delivered' }
}
const result = await db.transaction(async (tx) => {
await tx
.update(orderStatus)
.set({
isCancelled: true,
isCancelledByAdmin: true,
cancelReason: reason,
cancellationAdminNotes: reason,
cancellationReviewed: true,
cancellationReviewedAt: new Date(),
})
.where(eq(orderStatus.id, status.id))
const refundStatus = order.isCod ? 'na' : 'pending'
await tx.insert(refunds).values({
orderId: order.id,
refundStatus,
})
return { orderId: order.id, userId: order.userId }
})
return {
success: true,
message: 'Order cancelled successfully',
orderId: result.orderId,
userId: result.userId,
}
}
export async function deleteOrderById(orderId: number): Promise<void> {
await db.transaction(async (tx) => {
await tx.delete(orderItems).where(eq(orderItems.orderId, orderId))
await tx.delete(orderStatus).where(eq(orderStatus.orderId, orderId))
await tx.delete(payments).where(eq(payments.orderId, orderId))
await tx.delete(refunds).where(eq(refunds.orderId, orderId))
await tx.delete(couponUsage).where(eq(couponUsage.orderId, orderId))
await tx.delete(complaints).where(eq(complaints.orderId, orderId))
await tx.delete(orders).where(eq(orders.id, orderId))
})
}

View file

@ -0,0 +1,827 @@
import { db } from '../db/db_index'
import {
productInfo,
units,
specialDeals,
productSlots,
productTags,
productReviews,
productGroupInfo,
productGroupMembership,
productTagInfo,
users,
storeInfo,
} from '../db/schema'
import { and, desc, eq, inArray, sql } from 'drizzle-orm'
import type { InferInsertModel, InferSelectModel } from 'drizzle-orm'
import type {
AdminProduct,
AdminProductGroupInfo,
AdminProductTagInfo,
AdminProductTagWithProducts,
AdminProductReview,
AdminProductWithDetails,
AdminProductWithRelations,
AdminSpecialDeal,
AdminUnit,
AdminUpdateSlotProductsResult,
Store,
} from '@packages/shared'
type ProductRow = InferSelectModel<typeof productInfo>
type UnitRow = InferSelectModel<typeof units>
type StoreRow = InferSelectModel<typeof storeInfo>
type SpecialDealRow = InferSelectModel<typeof specialDeals>
type ProductTagInfoRow = InferSelectModel<typeof productTagInfo>
type ProductTagRow = InferSelectModel<typeof productTags>
type ProductGroupRow = InferSelectModel<typeof productGroupInfo>
type ProductGroupMembershipRow = InferSelectModel<typeof productGroupMembership>
type ProductReviewRow = InferSelectModel<typeof productReviews>
const getStringArray = (value: unknown): string[] | null => {
if (!Array.isArray(value)) return null
return value.map((item) => String(item))
}
const mapUnit = (unit: UnitRow): AdminUnit => ({
id: unit.id,
shortNotation: unit.shortNotation,
fullName: unit.fullName,
})
const mapStore = (store: StoreRow): Store => ({
id: store.id,
name: store.name,
description: store.description,
imageUrl: store.imageUrl,
owner: store.owner,
createdAt: store.createdAt,
// updatedAt: store.createdAt,
})
const mapProduct = (product: ProductRow): AdminProduct => ({
id: product.id,
name: product.name,
shortDescription: product.shortDescription ?? null,
longDescription: product.longDescription ?? null,
unitId: product.unitId,
price: String(product.price ?? '0'),
marketPrice: product.marketPrice ? String(product.marketPrice) : null,
images: getStringArray(product.images),
imageKeys: getStringArray(product.images),
isOutOfStock: product.isOutOfStock,
isSuspended: product.isSuspended,
isFlashAvailable: product.isFlashAvailable,
flashPrice: product.flashPrice ? String(product.flashPrice) : null,
createdAt: product.createdAt,
incrementStep: product.incrementStep,
productQuantity: product.productQuantity,
storeId: product.storeId,
})
const mapSpecialDeal = (deal: SpecialDealRow): AdminSpecialDeal => ({
id: deal.id,
productId: deal.productId,
quantity: String(deal.quantity ?? '0'),
price: String(deal.price ?? '0'),
validTill: deal.validTill,
})
const mapTagInfo = (tag: ProductTagInfoRow): AdminProductTagInfo => ({
id: tag.id,
tagName: tag.tagName,
tagDescription: tag.tagDescription ?? null,
imageUrl: tag.imageUrl ?? null,
isDashboardTag: tag.isDashboardTag,
relatedStores: tag.relatedStores,
createdAt: tag.createdAt,
})
export async function getAllProducts(): Promise<AdminProductWithRelations[]> {
type ProductWithRelationsRow = ProductRow & { unit: UnitRow; store: StoreRow | null }
const products = await db.query.productInfo.findMany({
orderBy: productInfo.name,
with: {
unit: true,
store: true,
},
}) as ProductWithRelationsRow[]
return products.map((product) => ({
...mapProduct(product),
unit: mapUnit(product.unit),
store: product.store ? mapStore(product.store) : null,
}))
}
export async function getProductById(id: number): Promise<AdminProductWithDetails | null> {
const product = await db.query.productInfo.findFirst({
where: eq(productInfo.id, id),
with: {
unit: true,
},
})
if (!product) {
return null
}
const deals = await db.query.specialDeals.findMany({
where: eq(specialDeals.productId, id),
orderBy: specialDeals.quantity,
})
const productTagsData = await db.query.productTags.findMany({
where: eq(productTags.productId, id),
with: {
tag: true,
},
}) as Array<ProductTagRow & { tag: ProductTagInfoRow }>
return {
...mapProduct(product),
unit: mapUnit(product.unit),
deals: deals.map(mapSpecialDeal),
tags: productTagsData.map((tag) => mapTagInfo(tag.tag)),
}
}
export async function deleteProduct(id: number): Promise<AdminProduct | null> {
const [deletedProduct] = await db
.delete(productInfo)
.where(eq(productInfo.id, id))
.returning()
if (!deletedProduct) {
return null
}
return mapProduct(deletedProduct)
}
type ProductInfoInsert = InferInsertModel<typeof productInfo>
type ProductInfoUpdate = Partial<ProductInfoInsert>
export async function createProduct(input: ProductInfoInsert): Promise<AdminProduct> {
const [product] = await db.insert(productInfo).values(input).returning()
return mapProduct(product)
}
export async function updateProduct(id: number, updates: ProductInfoUpdate): Promise<AdminProduct | null> {
const [product] = await db.update(productInfo)
.set(updates)
.where(eq(productInfo.id, id))
.returning()
if (!product) {
return null
}
return mapProduct(product)
}
export async function toggleProductOutOfStock(id: number): Promise<AdminProduct | null> {
const product = await db.query.productInfo.findFirst({
where: eq(productInfo.id, id),
})
if (!product) {
return null
}
const [updatedProduct] = await db
.update(productInfo)
.set({
isOutOfStock: !product.isOutOfStock,
})
.where(eq(productInfo.id, id))
.returning()
if (!updatedProduct) {
return null
}
return mapProduct(updatedProduct)
}
export async function updateSlotProducts(slotId: string, productIds: string[]): Promise<AdminUpdateSlotProductsResult> {
const currentAssociations = await db.query.productSlots.findMany({
where: eq(productSlots.slotId, parseInt(slotId)),
columns: {
productId: true,
},
}) as Array<{ productId: number }>
const currentProductIds = currentAssociations.map((assoc: { productId: number }) => assoc.productId)
const newProductIds = productIds.map((id: string) => parseInt(id))
const productsToAdd = newProductIds.filter((id: number) => !currentProductIds.includes(id))
const productsToRemove = currentProductIds.filter((id: number) => !newProductIds.includes(id))
if (productsToRemove.length > 0) {
await db.delete(productSlots).where(
and(
eq(productSlots.slotId, parseInt(slotId)),
inArray(productSlots.productId, productsToRemove)
)
)
}
if (productsToAdd.length > 0) {
const newAssociations = productsToAdd.map((productId) => ({
productId,
slotId: parseInt(slotId),
}))
await db.insert(productSlots).values(newAssociations)
}
return {
message: 'Slot products updated successfully',
added: productsToAdd.length,
removed: productsToRemove.length,
}
}
export async function getSlotProductIds(slotId: string): Promise<number[]> {
const associations = await db.query.productSlots.findMany({
where: eq(productSlots.slotId, parseInt(slotId)),
columns: {
productId: true,
},
})
return associations.map((assoc: { productId: number }) => assoc.productId)
}
export async function getAllUnits(): Promise<AdminUnit[]> {
const allUnits = await db.query.units.findMany({
orderBy: units.shortNotation,
})
return allUnits.map(mapUnit)
}
export async function getAllProductTags(): Promise<AdminProductTagWithProducts[]> {
const tags = await db.query.productTagInfo.findMany({
with: {
products: {
with: {
product: true,
},
},
},
}) as Array<ProductTagInfoRow & { products: Array<ProductTagRow & { product: ProductRow }> }>
return tags.map((tag: ProductTagInfoRow & { products: Array<ProductTagRow & { product: ProductRow }> }) => ({
...mapTagInfo(tag),
products: tag.products.map((assignment: ProductTagRow & { product: ProductRow }) => ({
productId: assignment.productId,
tagId: assignment.tagId,
assignedAt: assignment.assignedAt,
product: mapProduct(assignment.product),
})),
}))
}
export async function getAllProductTagInfos(): Promise<AdminProductTagInfo[]> {
const tags = await db.query.productTagInfo.findMany({
orderBy: productTagInfo.tagName,
})
return tags.map(mapTagInfo)
}
export async function getProductTagInfoById(tagId: number): Promise<AdminProductTagInfo | null> {
const tag = await db.query.productTagInfo.findFirst({
where: eq(productTagInfo.id, tagId),
})
if (!tag) {
return null
}
return mapTagInfo(tag)
}
export interface CreateProductTagInput {
tagName: string
tagDescription?: string | null
imageUrl?: string | null
isDashboardTag?: boolean
relatedStores?: number[]
}
export async function createProductTag(input: CreateProductTagInput): Promise<AdminProductTagWithProducts> {
const [tag] = await db.insert(productTagInfo).values({
tagName: input.tagName,
tagDescription: input.tagDescription || null,
imageUrl: input.imageUrl || null,
isDashboardTag: input.isDashboardTag || false,
relatedStores: input.relatedStores || [],
}).returning()
return {
...mapTagInfo(tag),
products: [],
}
}
export async function getProductTagById(tagId: number): Promise<AdminProductTagWithProducts | null> {
const tag = await db.query.productTagInfo.findFirst({
where: eq(productTagInfo.id, tagId),
with: {
products: {
with: {
product: true,
},
},
},
})
if (!tag) {
return null
}
return {
...mapTagInfo(tag),
products: tag.products.map((assignment: ProductTagRow & { product: ProductRow }) => ({
productId: assignment.productId,
tagId: assignment.tagId,
assignedAt: assignment.assignedAt,
product: mapProduct(assignment.product),
})),
}
}
export interface UpdateProductTagInput {
tagName?: string
tagDescription?: string | null
imageUrl?: string | null
isDashboardTag?: boolean
relatedStores?: number[]
}
export async function updateProductTag(tagId: number, input: UpdateProductTagInput): Promise<AdminProductTagWithProducts> {
const [tag] = await db.update(productTagInfo).set({
...(input.tagName !== undefined && { tagName: input.tagName }),
...(input.tagDescription !== undefined && { tagDescription: input.tagDescription }),
...(input.imageUrl !== undefined && { imageUrl: input.imageUrl }),
...(input.isDashboardTag !== undefined && { isDashboardTag: input.isDashboardTag }),
...(input.relatedStores !== undefined && { relatedStores: input.relatedStores }),
}).where(eq(productTagInfo.id, tagId)).returning()
const fullTag = await db.query.productTagInfo.findFirst({
where: eq(productTagInfo.id, tagId),
with: {
products: {
with: {
product: true,
},
},
},
})
return {
...mapTagInfo(tag),
products: fullTag?.products.map((assignment: ProductTagRow & { product: ProductRow }) => ({
productId: assignment.productId,
tagId: assignment.tagId,
assignedAt: assignment.assignedAt,
product: mapProduct(assignment.product),
})) || [],
}
}
export async function deleteProductTag(tagId: number): Promise<void> {
await db.delete(productTagInfo).where(eq(productTagInfo.id, tagId))
}
export async function checkProductTagExistsByName(tagName: string): Promise<boolean> {
const tag = await db.query.productTagInfo.findFirst({
where: eq(productTagInfo.tagName, tagName),
})
return !!tag
}
export async function getSlotsProductIds(slotIds: number[]): Promise<Record<number, number[]>> {
if (slotIds.length === 0) {
return {}
}
const associations = await db.query.productSlots.findMany({
where: inArray(productSlots.slotId, slotIds),
columns: {
slotId: true,
productId: true,
},
}) as Array<{ slotId: number; productId: number }>
const result: Record<number, number[]> = {}
for (const assoc of associations) {
if (!result[assoc.slotId]) {
result[assoc.slotId] = []
}
result[assoc.slotId].push(assoc.productId)
}
slotIds.forEach((slotId) => {
if (!result[slotId]) {
result[slotId] = []
}
})
return result
}
export async function getProductReviews(productId: number, limit: number, offset: number) {
const reviews = await db
.select({
id: productReviews.id,
reviewBody: productReviews.reviewBody,
ratings: productReviews.ratings,
imageUrls: productReviews.imageUrls,
reviewTime: productReviews.reviewTime,
adminResponse: productReviews.adminResponse,
adminResponseImages: productReviews.adminResponseImages,
userName: users.name,
})
.from(productReviews)
.innerJoin(users, eq(productReviews.userId, users.id))
.where(eq(productReviews.productId, productId))
.orderBy(desc(productReviews.reviewTime))
.limit(limit)
.offset(offset)
const totalCountResult = await db
.select({ count: sql`count(*)` })
.from(productReviews)
.where(eq(productReviews.productId, productId))
const totalCount = Number(totalCountResult[0].count)
const mappedReviews: AdminProductReview[] = reviews.map((review: any) => ({
id: review.id,
reviewBody: review.reviewBody,
ratings: review.ratings,
imageUrls: review.imageUrls,
reviewTime: review.reviewTime,
adminResponse: review.adminResponse ?? null,
adminResponseImages: review.adminResponseImages,
userName: review.userName ?? null,
}))
return {
reviews: mappedReviews,
totalCount,
}
}
export async function respondToReview(
reviewId: number,
adminResponse: string | undefined,
adminResponseImages: string[]
): Promise<AdminProductReview | null> {
const [updatedReview] = await db
.update(productReviews)
.set({
adminResponse,
adminResponseImages,
})
.where(eq(productReviews.id, reviewId))
.returning()
if (!updatedReview) {
return null
}
return {
id: updatedReview.id,
reviewBody: updatedReview.reviewBody,
ratings: updatedReview.ratings,
imageUrls: updatedReview.imageUrls,
reviewTime: updatedReview.reviewTime,
adminResponse: updatedReview.adminResponse ?? null,
adminResponseImages: updatedReview.adminResponseImages,
userName: null,
}
}
export async function getAllProductGroups() {
const groups = await db.query.productGroupInfo.findMany({
with: {
memberships: {
with: {
product: true,
},
},
},
orderBy: desc(productGroupInfo.createdAt),
})
return groups.map((group: any) => ({
id: group.id,
groupName: group.groupName,
description: group.description ?? null,
createdAt: group.createdAt,
products: group.memberships.map((membership: any) => mapProduct(membership.product)),
productCount: group.memberships.length,
memberships: group.memberships
}))
}
export async function createProductGroup(
groupName: string,
description: string | undefined,
productIds: number[]
): Promise<AdminProductGroupInfo> {
const [newGroup] = await db
.insert(productGroupInfo)
.values({
groupName,
description,
})
.returning()
if (productIds.length > 0) {
const memberships = productIds.map((productId) => ({
productId,
groupId: newGroup.id,
}))
await db.insert(productGroupMembership).values(memberships)
}
return {
id: newGroup.id,
groupName: newGroup.groupName,
description: newGroup.description ?? null,
createdAt: newGroup.createdAt,
}
}
export async function updateProductGroup(
id: number,
groupName: string | undefined,
description: string | undefined,
productIds: number[] | undefined
): Promise<AdminProductGroupInfo | null> {
const updateData: Partial<{
groupName: string
description: string | null
}> = {}
if (groupName !== undefined) updateData.groupName = groupName
if (description !== undefined) updateData.description = description
const [updatedGroup] = await db
.update(productGroupInfo)
.set(updateData)
.where(eq(productGroupInfo.id, id))
.returning()
if (!updatedGroup) {
return null
}
if (productIds !== undefined) {
await db.delete(productGroupMembership).where(eq(productGroupMembership.groupId, id))
if (productIds.length > 0) {
const memberships = productIds.map((productId) => ({
productId,
groupId: id,
}))
await db.insert(productGroupMembership).values(memberships)
}
}
return {
id: updatedGroup.id,
groupName: updatedGroup.groupName,
description: updatedGroup.description ?? null,
createdAt: updatedGroup.createdAt,
}
}
export async function deleteProductGroup(id: number): Promise<AdminProductGroupInfo | null> {
await db.delete(productGroupMembership).where(eq(productGroupMembership.groupId, id))
const [deletedGroup] = await db
.delete(productGroupInfo)
.where(eq(productGroupInfo.id, id))
.returning()
if (!deletedGroup) {
return null
}
return {
id: deletedGroup.id,
groupName: deletedGroup.groupName,
description: deletedGroup.description ?? null,
createdAt: deletedGroup.createdAt,
}
}
export async function addProductToGroup(groupId: number, productId: number): Promise<void> {
await db.insert(productGroupMembership).values({ groupId, productId })
}
export async function removeProductFromGroup(groupId: number, productId: number): Promise<void> {
await db.delete(productGroupMembership)
.where(and(
eq(productGroupMembership.groupId, groupId),
eq(productGroupMembership.productId, productId)
))
}
export async function updateProductPrices(updates: Array<{
productId: number
price?: number
marketPrice?: number | null
flashPrice?: number | null
isFlashAvailable?: boolean
}>) {
if (updates.length === 0) {
return { updatedCount: 0, invalidIds: [] }
}
const productIds = updates.map((update) => update.productId)
const existingProducts = await db.query.productInfo.findMany({
where: inArray(productInfo.id, productIds),
columns: { id: true },
}) as Array<{ id: number }>
const existingIds = new Set(existingProducts.map((product: { id: number }) => product.id))
const invalidIds = productIds.filter((id) => !existingIds.has(id))
if (invalidIds.length > 0) {
return { updatedCount: 0, invalidIds }
}
const updatePromises = updates.map((update) => {
const { productId, price, marketPrice, flashPrice, isFlashAvailable } = update
const updateData: Partial<Pick<ProductInfoInsert, 'price' | 'marketPrice' | 'flashPrice' | 'isFlashAvailable'>> = {}
if (price !== undefined) updateData.price = price.toString()
if (marketPrice !== undefined) updateData.marketPrice = marketPrice === null ? null : marketPrice.toString()
if (flashPrice !== undefined) updateData.flashPrice = flashPrice === null ? null : flashPrice.toString()
if (isFlashAvailable !== undefined) updateData.isFlashAvailable = isFlashAvailable
return db
.update(productInfo)
.set(updateData)
.where(eq(productInfo.id, productId))
})
await Promise.all(updatePromises)
return { updatedCount: updates.length, invalidIds: [] }
}
// ==========================================================================
// Product Helpers for Admin Controller
// ==========================================================================
export async function checkProductExistsByName(name: string): Promise<boolean> {
const product = await db.query.productInfo.findFirst({
where: eq(productInfo.name, name),
columns: { id: true },
})
return !!product
}
export async function checkUnitExists(unitId: number): Promise<boolean> {
const unit = await db.query.units.findFirst({
where: eq(units.id, unitId),
columns: { id: true },
})
return !!unit
}
export async function getProductImagesById(productId: number): Promise<string[] | null> {
const product = await db.query.productInfo.findFirst({
where: eq(productInfo.id, productId),
columns: { images: true },
})
if (!product) {
return null
}
return getStringArray(product.images) || []
}
export interface CreateSpecialDealInput {
quantity: number
price: number
validTill: string | Date
}
export async function createSpecialDealsForProduct(
productId: number,
deals: CreateSpecialDealInput[]
): Promise<AdminSpecialDeal[]> {
if (deals.length === 0) {
return []
}
const dealInserts = deals.map((deal) => ({
productId,
quantity: deal.quantity.toString(),
price: deal.price.toString(),
validTill: new Date(deal.validTill),
}))
const createdDeals = await db
.insert(specialDeals)
.values(dealInserts)
.returning()
return createdDeals.map(mapSpecialDeal)
}
export async function updateProductDeals(
productId: number,
deals: CreateSpecialDealInput[]
): Promise<void> {
if (deals.length === 0) {
await db.delete(specialDeals).where(eq(specialDeals.productId, productId))
return
}
const existingDeals = await db.query.specialDeals.findMany({
where: eq(specialDeals.productId, productId),
})
const existingDealsMap = new Map<string, SpecialDealRow>(
existingDeals.map((deal: SpecialDealRow) => [`${deal.quantity}-${deal.price}`, deal])
)
const newDealsMap = new Map<string, CreateSpecialDealInput>(
deals.map((deal) => [`${deal.quantity}-${deal.price}`, deal])
)
const dealsToAdd = deals.filter((deal) => {
const key = `${deal.quantity}-${deal.price}`
return !existingDealsMap.has(key)
})
const dealsToRemove = existingDeals.filter((deal: SpecialDealRow) => {
const key = `${deal.quantity}-${deal.price}`
return !newDealsMap.has(key)
})
const dealsToUpdate = deals.filter((deal: CreateSpecialDealInput) => {
const key = `${deal.quantity}-${deal.price}`
const existing = existingDealsMap.get(key)
const nextValidTill = deal.validTill instanceof Date
? deal.validTill.toISOString().split('T')[0]
: String(deal.validTill)
return existing && existing.validTill.toISOString().split('T')[0] !== nextValidTill
})
if (dealsToRemove.length > 0) {
await db.delete(specialDeals).where(
inArray(specialDeals.id, dealsToRemove.map((deal: SpecialDealRow) => deal.id))
)
}
if (dealsToAdd.length > 0) {
const dealInserts = dealsToAdd.map((deal) => ({
productId,
quantity: deal.quantity.toString(),
price: deal.price.toString(),
validTill: new Date(deal.validTill),
}))
await db.insert(specialDeals).values(dealInserts)
}
for (const deal of dealsToUpdate) {
const key = `${deal.quantity}-${deal.price}`
const existingDeal = existingDealsMap.get(key)
if (existingDeal) {
await db.update(specialDeals)
.set({ validTill: new Date(deal.validTill) })
.where(eq(specialDeals.id, existingDeal.id))
}
}
}
export async function replaceProductTags(productId: number, tagIds: number[]): Promise<void> {
await db.delete(productTags).where(eq(productTags.productId, productId))
if (tagIds.length === 0) {
return
}
const tagAssociations = tagIds.map((tagId) => ({
productId,
tagId,
}))
await db.insert(productTags).values(tagAssociations)
}

View file

@ -0,0 +1,351 @@
import { db } from '../db/db_index'
import {
deliverySlotInfo,
productSlots,
productInfo,
vendorSnippets,
productGroupInfo,
} from '../db/schema'
import { and, asc, desc, eq, gt, inArray } from 'drizzle-orm'
import type {
AdminDeliverySlot,
AdminSlotWithProducts,
AdminSlotWithProductsAndSnippetsBase,
AdminSlotCreateResult,
AdminSlotUpdateResult,
AdminVendorSnippet,
AdminSlotProductSummary,
AdminUpdateSlotCapacityResult,
} from '@packages/shared'
type SlotSnippetInput = {
name: string
productIds: number[]
validTill?: string
}
const getStringArray = (value: unknown): string[] | null => {
if (!Array.isArray(value)) return null
return value.map((item) => String(item))
}
const getNumberArray = (value: unknown): number[] => {
if (!Array.isArray(value)) return []
return value.map((item) => Number(item))
}
const mapDeliverySlot = (slot: typeof deliverySlotInfo.$inferSelect): AdminDeliverySlot => ({
id: slot.id,
deliveryTime: slot.deliveryTime,
freezeTime: slot.freezeTime,
isActive: slot.isActive,
isFlash: slot.isFlash,
isCapacityFull: slot.isCapacityFull,
deliverySequence: slot.deliverySequence,
groupIds: slot.groupIds,
})
const mapSlotProductSummary = (product: { id: number; name: string; images: unknown }): AdminSlotProductSummary => ({
id: product.id,
name: product.name,
images: getStringArray(product.images),
})
const mapVendorSnippet = (snippet: typeof vendorSnippets.$inferSelect): AdminVendorSnippet => ({
id: snippet.id,
snippetCode: snippet.snippetCode,
slotId: snippet.slotId ?? null,
productIds: snippet.productIds || [],
isPermanent: snippet.isPermanent,
validTill: snippet.validTill ?? null,
createdAt: snippet.createdAt,
})
export async function getActiveSlotsWithProducts(): Promise<AdminSlotWithProducts[]> {
const slots = await db.query.deliverySlotInfo
.findMany({
where: eq(deliverySlotInfo.isActive, true),
orderBy: desc(deliverySlotInfo.deliveryTime),
with: {
productSlots: {
with: {
product: {
columns: {
id: true,
name: true,
images: true,
},
},
},
},
},
})
return slots.map((slot: any) => ({
...mapDeliverySlot(slot),
deliverySequence: getNumberArray(slot.deliverySequence),
products: slot.productSlots.map((ps: any) => mapSlotProductSummary(ps.product)),
}))
}
export async function getActiveSlots(): Promise<AdminDeliverySlot[]> {
const slots = await db.query.deliverySlotInfo.findMany({
where: eq(deliverySlotInfo.isActive, true),
})
return slots.map(mapDeliverySlot)
}
export async function getSlotsAfterDate(afterDate: Date): Promise<AdminDeliverySlot[]> {
const slots = await db.query.deliverySlotInfo.findMany({
where: and(
eq(deliverySlotInfo.isActive, true),
gt(deliverySlotInfo.deliveryTime, afterDate)
),
orderBy: asc(deliverySlotInfo.deliveryTime),
})
return slots.map(mapDeliverySlot)
}
export async function getSlotByIdWithRelations(id: number): Promise<AdminSlotWithProductsAndSnippetsBase | null> {
const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, id),
with: {
productSlots: {
with: {
product: {
columns: {
id: true,
name: true,
images: true,
},
},
},
},
vendorSnippets: true,
},
})
if (!slot) {
return null
}
return {
...mapDeliverySlot(slot),
deliverySequence: getNumberArray(slot.deliverySequence),
groupIds: getNumberArray(slot.groupIds),
products: slot.productSlots.map((ps: any) => mapSlotProductSummary(ps.product)),
vendorSnippets: slot.vendorSnippets.map(mapVendorSnippet),
}
}
export async function createSlotWithRelations(input: {
deliveryTime: string
freezeTime: string
isActive?: boolean
productIds?: number[]
vendorSnippets?: SlotSnippetInput[]
groupIds?: number[]
}): Promise<AdminSlotCreateResult> {
const { deliveryTime, freezeTime, isActive, productIds, vendorSnippets: snippets, groupIds } = input
const result = await db.transaction(async (tx) => {
const [newSlot] = await tx
.insert(deliverySlotInfo)
.values({
deliveryTime: new Date(deliveryTime),
freezeTime: new Date(freezeTime),
isActive: isActive !== undefined ? isActive : true,
groupIds: groupIds !== undefined ? groupIds : [],
})
.returning()
if (productIds && productIds.length > 0) {
const associations = productIds.map((productId) => ({
productId,
slotId: newSlot.id,
}))
await tx.insert(productSlots).values(associations)
}
let createdSnippets: AdminVendorSnippet[] = []
if (snippets && snippets.length > 0) {
for (const snippet of snippets) {
const products = await tx.query.productInfo.findMany({
where: inArray(productInfo.id, snippet.productIds),
})
if (products.length !== snippet.productIds.length) {
throw new Error(`One or more invalid product IDs in snippet "${snippet.name}"`)
}
const existingSnippet = await tx.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.snippetCode, snippet.name),
})
if (existingSnippet) {
throw new Error(`Snippet name "${snippet.name}" already exists`)
}
const [createdSnippet] = await tx.insert(vendorSnippets).values({
snippetCode: snippet.name,
slotId: newSlot.id,
productIds: snippet.productIds,
validTill: snippet.validTill ? new Date(snippet.validTill) : undefined,
}).returning()
createdSnippets.push(mapVendorSnippet(createdSnippet))
}
}
return {
slot: mapDeliverySlot(newSlot),
createdSnippets,
message: 'Slot created successfully',
}
})
return result
}
export async function updateSlotWithRelations(input: {
id: number
deliveryTime: string
freezeTime: string
isActive?: boolean
productIds?: number[]
vendorSnippets?: SlotSnippetInput[]
groupIds?: number[]
}): Promise<AdminSlotUpdateResult | null> {
const { id, deliveryTime, freezeTime, isActive, productIds, vendorSnippets: snippets, groupIds } = input
let validGroupIds = groupIds
if (groupIds && groupIds.length > 0) {
const existingGroups = await db.query.productGroupInfo.findMany({
where: inArray(productGroupInfo.id, groupIds),
columns: { id: true },
})
validGroupIds = existingGroups.map((group: { id: number }) => group.id)
}
const result = await db.transaction(async (tx) => {
const [updatedSlot] = await tx
.update(deliverySlotInfo)
.set({
deliveryTime: new Date(deliveryTime),
freezeTime: new Date(freezeTime),
isActive: isActive !== undefined ? isActive : true,
groupIds: validGroupIds !== undefined ? validGroupIds : [],
})
.where(eq(deliverySlotInfo.id, id))
.returning()
if (!updatedSlot) {
return null
}
if (productIds !== undefined) {
await tx.delete(productSlots).where(eq(productSlots.slotId, id))
if (productIds.length > 0) {
const associations = productIds.map((productId) => ({
productId,
slotId: id,
}))
await tx.insert(productSlots).values(associations)
}
}
let createdSnippets: AdminVendorSnippet[] = []
if (snippets && snippets.length > 0) {
for (const snippet of snippets) {
const products = await tx.query.productInfo.findMany({
where: inArray(productInfo.id, snippet.productIds),
})
if (products.length !== snippet.productIds.length) {
throw new Error(`One or more invalid product IDs in snippet "${snippet.name}"`)
}
const existingSnippet = await tx.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.snippetCode, snippet.name),
})
if (existingSnippet) {
throw new Error(`Snippet name "${snippet.name}" already exists`)
}
const [createdSnippet] = await tx.insert(vendorSnippets).values({
snippetCode: snippet.name,
slotId: id,
productIds: snippet.productIds,
validTill: snippet.validTill ? new Date(snippet.validTill) : undefined,
}).returning()
createdSnippets.push(mapVendorSnippet(createdSnippet))
}
}
return {
slot: mapDeliverySlot(updatedSlot),
createdSnippets,
message: 'Slot updated successfully',
}
})
return result
}
export async function deleteSlotById(id: number): Promise<AdminDeliverySlot | null> {
const [deletedSlot] = await db
.update(deliverySlotInfo)
.set({ isActive: false })
.where(eq(deliverySlotInfo.id, id))
.returning()
if (!deletedSlot) {
return null
}
return mapDeliverySlot(deletedSlot)
}
export async function getSlotDeliverySequence(slotId: number): Promise<AdminDeliverySlot | null> {
const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, slotId),
})
if (!slot) {
return null
}
return mapDeliverySlot(slot)
}
export async function updateSlotDeliverySequence(slotId: number, sequence: unknown) {
const [updatedSlot] = await db
.update(deliverySlotInfo)
.set({ deliverySequence: sequence as Record<string, number> })
.where(eq(deliverySlotInfo.id, slotId))
.returning({
id: deliverySlotInfo.id,
deliverySequence: deliverySlotInfo.deliverySequence,
})
return updatedSlot || null
}
export async function updateSlotCapacity(slotId: number, isCapacityFull: boolean): Promise<AdminUpdateSlotCapacityResult | null> {
const [updatedSlot] = await db
.update(deliverySlotInfo)
.set({ isCapacityFull })
.where(eq(deliverySlotInfo.id, slotId))
.returning()
if (!updatedSlot) {
return null
}
return {
success: true,
slot: mapDeliverySlot(updatedSlot),
message: `Slot ${isCapacityFull ? 'marked as full capacity' : 'capacity reset'}`,
}
}

View file

@ -0,0 +1,154 @@
import { db } from '../db/db_index'
import { staffUsers, staffRoles, users, userDetails, orders } from '../db/schema'
import { eq, or, like, and, lt, desc } from 'drizzle-orm'
export interface StaffUser {
id: number
name: string
password: string
staffRoleId: number | null
createdAt: Date
}
export async function getStaffUserByName(name: string): Promise<StaffUser | null> {
const staff = await db.query.staffUsers.findFirst({
where: eq(staffUsers.name, name),
})
return staff || null
}
export async function getStaffUserById(staffId: number): Promise<StaffUser | null> {
const staff = await db.query.staffUsers.findFirst({
where: eq(staffUsers.id, staffId),
})
return staff || null
}
export async function getAllStaff(): Promise<any[]> {
const staff = await db.query.staffUsers.findMany({
columns: {
id: true,
name: true,
},
with: {
role: {
with: {
rolePermissions: {
with: {
permission: true,
},
},
},
},
},
})
return staff
}
export async function getAllUsers(
cursor?: number,
limit: number = 20,
search?: string
): Promise<{ users: any[]; hasMore: boolean }> {
let whereCondition = undefined
if (search) {
whereCondition = or(
like(users.name, `%${search}%`),
like(users.email, `%${search}%`),
like(users.mobile, `%${search}%`)
)
}
if (cursor) {
const cursorCondition = lt(users.id, cursor)
whereCondition = whereCondition ? and(whereCondition, cursorCondition) : cursorCondition
}
const allUsers = await db.query.users.findMany({
where: whereCondition,
with: {
userDetails: true,
},
orderBy: desc(users.id),
limit: limit + 1,
})
const hasMore = allUsers.length > limit
const usersToReturn = hasMore ? allUsers.slice(0, limit) : allUsers
return { users: usersToReturn, hasMore }
}
export async function getUserWithDetails(userId: number): Promise<any | null> {
const user = await db.query.users.findFirst({
where: eq(users.id, userId),
with: {
userDetails: true,
orders: {
orderBy: desc(orders.createdAt),
limit: 1,
},
},
})
return user || null
}
export async function updateUserSuspensionStatus(userId: number, isSuspended: boolean): Promise<void> {
await db
.insert(userDetails)
.values({ userId, isSuspended })
.onConflictDoUpdate({
target: userDetails.userId,
set: { isSuspended },
})
}
export async function checkStaffUserExists(name: string): Promise<boolean> {
const existingUser = await db.query.staffUsers.findFirst({
where: eq(staffUsers.name, name),
})
return !!existingUser
}
export async function checkStaffRoleExists(roleId: number): Promise<boolean> {
const role = await db.query.staffRoles.findFirst({
where: eq(staffRoles.id, roleId),
})
return !!role
}
export async function createStaffUser(
name: string,
password: string,
roleId: number
): Promise<StaffUser> {
const [newUser] = await db.insert(staffUsers).values({
name: name.trim(),
password,
staffRoleId: roleId,
}).returning()
return {
id: newUser.id,
name: newUser.name,
password: newUser.password,
staffRoleId: newUser.staffRoleId,
createdAt: newUser.createdAt,
}
}
export async function getAllRoles(): Promise<any[]> {
const roles = await db.query.staffRoles.findMany({
columns: {
id: true,
roleName: true,
},
})
return roles
}

View file

@ -0,0 +1,145 @@
import { db } from '../db/db_index'
import { storeInfo, productInfo } from '../db/schema'
import { eq, inArray } from 'drizzle-orm'
export interface Store {
id: number
name: string
description: string | null
imageUrl: string | null
owner: number
createdAt: Date
// updatedAt: Date
}
export async function getAllStores(): Promise<any[]> {
const stores = await db.query.storeInfo.findMany({
with: {
owner: true,
},
})
return stores
}
export async function getStoreById(id: number): Promise<any | null> {
const store = await db.query.storeInfo.findFirst({
where: eq(storeInfo.id, id),
with: {
owner: true,
},
})
return store || null
}
export interface CreateStoreInput {
name: string
description?: string
imageUrl?: string
owner: number
}
export async function createStore(
input: CreateStoreInput,
products?: number[]
): Promise<Store> {
const [newStore] = await db
.insert(storeInfo)
.values({
name: input.name,
description: input.description,
imageUrl: input.imageUrl,
owner: input.owner,
})
.returning()
if (products && products.length > 0) {
await db
.update(productInfo)
.set({ storeId: newStore.id })
.where(inArray(productInfo.id, products))
}
return {
id: newStore.id,
name: newStore.name,
description: newStore.description,
imageUrl: newStore.imageUrl,
owner: newStore.owner,
createdAt: newStore.createdAt,
// updatedAt: newStore.updatedAt,
}
}
export interface UpdateStoreInput {
name?: string
description?: string
imageUrl?: string
owner?: number
}
export async function updateStore(
id: number,
input: UpdateStoreInput,
products?: number[]
): Promise<Store> {
const [updatedStore] = await db
.update(storeInfo)
.set({
...input,
// updatedAt: new Date(),
})
.where(eq(storeInfo.id, id))
.returning()
if (!updatedStore) {
throw new Error('Store not found')
}
if (products !== undefined) {
await db
.update(productInfo)
.set({ storeId: null })
.where(eq(productInfo.storeId, id))
if (products.length > 0) {
await db
.update(productInfo)
.set({ storeId: id })
.where(inArray(productInfo.id, products))
}
}
return {
id: updatedStore.id,
name: updatedStore.name,
description: updatedStore.description,
imageUrl: updatedStore.imageUrl,
owner: updatedStore.owner,
createdAt: updatedStore.createdAt,
// updatedAt: updatedStore.updatedAt,
}
}
export async function deleteStore(id: number): Promise<{ message: string }> {
return await db.transaction(async (tx) => {
await tx
.update(productInfo)
.set({ storeId: null })
.where(eq(productInfo.storeId, id))
const [deletedStore] = await tx
.delete(storeInfo)
.where(eq(storeInfo.id, id))
.returning()
if (!deletedStore) {
throw new Error('Store not found')
}
return {
message: 'Store deleted successfully',
}
})
}

View file

@ -0,0 +1,270 @@
import { db } from '../db/db_index'
import { users, userDetails, orders, orderItems, complaints, notifCreds, unloggedUserTokens, userIncidents, orderStatus } from '../db/schema'
import { eq, sql, desc, asc, count, max, inArray } from 'drizzle-orm'
export async function createUserByMobile(mobile: string): Promise<any> {
const [newUser] = await db
.insert(users)
.values({
name: null,
email: null,
mobile,
})
.returning()
return newUser
}
export async function getUserByMobile(mobile: string): Promise<any | null> {
const [existingUser] = await db
.select()
.from(users)
.where(eq(users.mobile, mobile))
.limit(1)
return existingUser || null
}
export async function getUnresolvedComplaintsCount(): Promise<number> {
const result = await db
.select({ count: count(complaints.id) })
.from(complaints)
.where(eq(complaints.isResolved, false))
return result[0]?.count || 0
}
export async function getAllUsersWithFilters(
limit: number,
cursor?: number,
search?: string
): Promise<{ users: any[]; hasMore: boolean }> {
const whereConditions = []
if (search && search.trim()) {
whereConditions.push(sql`${users.mobile} LIKE ${`%${search.trim()}%`}`)
}
if (cursor) {
whereConditions.push(sql`${users.id} > ${cursor}`)
}
const usersList = await db
.select({
id: users.id,
name: users.name,
mobile: users.mobile,
createdAt: users.createdAt,
})
.from(users)
.where(whereConditions.length > 0 ? sql.join(whereConditions, sql` AND `) : undefined)
.orderBy(asc(users.id))
.limit(limit + 1)
const hasMore = usersList.length > limit
const usersToReturn = hasMore ? usersList.slice(0, limit) : usersList
return { users: usersToReturn, hasMore }
}
export async function getOrderCountsByUserIds(userIds: number[]): Promise<{ userId: number; totalOrders: number }[]> {
if (userIds.length === 0) return []
return await db
.select({
userId: orders.userId,
totalOrders: count(orders.id),
})
.from(orders)
.where(sql`${orders.userId} IN (${sql.join(userIds, sql`, `)})`)
.groupBy(orders.userId)
}
export async function getLastOrdersByUserIds(userIds: number[]): Promise<{ userId: number; lastOrderDate: Date | null }[]> {
if (userIds.length === 0) return []
return await db
.select({
userId: orders.userId,
lastOrderDate: max(orders.createdAt),
})
.from(orders)
.where(sql`${orders.userId} IN (${sql.join(userIds, sql`, `)})`)
.groupBy(orders.userId)
}
export async function getSuspensionStatusesByUserIds(userIds: number[]): Promise<{ userId: number; isSuspended: boolean }[]> {
if (userIds.length === 0) return []
return await db
.select({
userId: userDetails.userId,
isSuspended: userDetails.isSuspended,
})
.from(userDetails)
.where(sql`${userDetails.userId} IN (${sql.join(userIds, sql`, `)})`)
}
export async function getUserBasicInfo(userId: number): Promise<any | null> {
const user = await db
.select({
id: users.id,
name: users.name,
mobile: users.mobile,
createdAt: users.createdAt,
})
.from(users)
.where(eq(users.id, userId))
.limit(1)
return user[0] || null
}
export async function getUserSuspensionStatus(userId: number): Promise<boolean> {
const userDetail = await db
.select({
isSuspended: userDetails.isSuspended,
})
.from(userDetails)
.where(eq(userDetails.userId, userId))
.limit(1)
return userDetail[0]?.isSuspended ?? false
}
export async function getUserOrders(userId: number): Promise<any[]> {
return await db
.select({
id: orders.id,
readableId: orders.readableId,
totalAmount: orders.totalAmount,
createdAt: orders.createdAt,
isFlashDelivery: orders.isFlashDelivery,
})
.from(orders)
.where(eq(orders.userId, userId))
.orderBy(desc(orders.createdAt))
}
export async function getOrderStatusesByOrderIds(orderIds: number[]): Promise<{ orderId: number; isDelivered: boolean; isCancelled: boolean }[]> {
if (orderIds.length === 0) return []
return await db
.select({
orderId: orderStatus.orderId,
isDelivered: orderStatus.isDelivered,
isCancelled: orderStatus.isCancelled,
})
.from(orderStatus)
.where(sql`${orderStatus.orderId} IN (${sql.join(orderIds, sql`, `)})`)
}
export async function getItemCountsByOrderIds(orderIds: number[]): Promise<{ orderId: number; itemCount: number }[]> {
if (orderIds.length === 0) return []
return await db
.select({
orderId: orderItems.orderId,
itemCount: count(orderItems.id),
})
.from(orderItems)
.where(sql`${orderItems.orderId} IN (${sql.join(orderIds, sql`, `)})`)
.groupBy(orderItems.orderId)
}
export async function upsertUserSuspension(userId: number, isSuspended: boolean): Promise<void> {
const existingDetail = await db
.select({ id: userDetails.id })
.from(userDetails)
.where(eq(userDetails.userId, userId))
.limit(1)
if (existingDetail.length > 0) {
await db
.update(userDetails)
.set({ isSuspended })
.where(eq(userDetails.userId, userId))
} else {
await db
.insert(userDetails)
.values({
userId,
isSuspended,
})
}
}
export async function searchUsers(search?: string): Promise<any[]> {
if (search && search.trim()) {
return await db
.select({
id: users.id,
name: users.name,
mobile: users.mobile,
})
.from(users)
.where(sql`${users.mobile} LIKE ${`%${search.trim()}%`} OR ${users.name} LIKE ${`%${search.trim()}%`}`)
} else {
return await db
.select({
id: users.id,
name: users.name,
mobile: users.mobile,
})
.from(users)
}
}
export async function getAllNotifCreds(): Promise<{ userId: number, token: string }[]> {
return await db
.select({ userId: notifCreds.userId, token: notifCreds.token })
.from(notifCreds)
}
export async function getAllUnloggedTokens(): Promise<{ token: string }[]> {
return await db
.select({ token: unloggedUserTokens.token })
.from(unloggedUserTokens)
}
export async function getNotifTokensByUserIds(userIds: number[]): Promise<{ token: string }[]> {
return await db
.select({ token: notifCreds.token })
.from(notifCreds)
.where(inArray(notifCreds.userId, userIds))
}
export async function getUserIncidentsWithRelations(userId: number): Promise<any[]> {
return await db.query.userIncidents.findMany({
where: eq(userIncidents.userId, userId),
with: {
order: {
with: {
orderStatus: true,
},
},
addedBy: true,
},
orderBy: desc(userIncidents.dateAdded),
})
}
export async function createUserIncident(
userId: number,
orderId: number | undefined,
adminComment: string | undefined,
adminUserId: number,
negativityScore: number | undefined
): Promise<any> {
const [incident] = await db.insert(userIncidents)
.values({
userId,
orderId,
adminComment,
addedBy: adminUserId,
negativityScore,
})
.returning()
return incident
}

View file

@ -0,0 +1,250 @@
import { db } from '../db/db_index'
import { vendorSnippets, deliverySlotInfo, productInfo, orders, orderItems, orderStatus } from '../db/schema'
import { desc, eq, inArray } from 'drizzle-orm'
import type { InferSelectModel } from 'drizzle-orm'
import type {
AdminDeliverySlot,
AdminVendorSnippet,
AdminVendorSnippetWithSlot,
AdminVendorSnippetProduct,
AdminVendorUpdatePackagingResult,
} from '@packages/shared'
type VendorSnippetRow = InferSelectModel<typeof vendorSnippets>
type DeliverySlotRow = InferSelectModel<typeof deliverySlotInfo>
type ProductRow = InferSelectModel<typeof productInfo>
const mapVendorSnippet = (snippet: VendorSnippetRow): AdminVendorSnippet => ({
id: snippet.id,
snippetCode: snippet.snippetCode,
slotId: snippet.slotId ?? null,
productIds: snippet.productIds || [],
isPermanent: snippet.isPermanent,
validTill: snippet.validTill ?? null,
createdAt: snippet.createdAt,
})
const mapDeliverySlot = (slot: DeliverySlotRow): AdminDeliverySlot => ({
id: slot.id,
deliveryTime: slot.deliveryTime,
freezeTime: slot.freezeTime,
isActive: slot.isActive,
isFlash: slot.isFlash,
isCapacityFull: slot.isCapacityFull,
deliverySequence: slot.deliverySequence,
groupIds: slot.groupIds,
})
const mapProductSummary = (product: { id: number; name: string }): AdminVendorSnippetProduct => ({
id: product.id,
name: product.name,
})
export async function checkVendorSnippetExists(snippetCode: string): Promise<boolean> {
const existingSnippet = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.snippetCode, snippetCode),
})
return !!existingSnippet
}
export async function getVendorSnippetById(id: number): Promise<AdminVendorSnippetWithSlot | null> {
const snippet = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.id, id),
with: {
slot: true,
},
})
if (!snippet) {
return null
}
return {
...mapVendorSnippet(snippet),
slot: snippet.slot ? mapDeliverySlot(snippet.slot) : null,
}
}
export async function getVendorSnippetByCode(snippetCode: string): Promise<AdminVendorSnippet | null> {
const snippet = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.snippetCode, snippetCode),
})
return snippet ? mapVendorSnippet(snippet) : null
}
export async function getAllVendorSnippets(): Promise<AdminVendorSnippetWithSlot[]> {
const snippets = await db.query.vendorSnippets.findMany({
with: {
slot: true,
},
orderBy: desc(vendorSnippets.createdAt),
})
return snippets.map((snippet: VendorSnippetRow & { slot: DeliverySlotRow | null }) => ({
...mapVendorSnippet(snippet),
slot: snippet.slot ? mapDeliverySlot(snippet.slot) : null,
}))
}
export async function createVendorSnippet(input: {
snippetCode: string
slotId?: number
productIds: number[]
isPermanent: boolean
validTill?: Date
}): Promise<AdminVendorSnippet> {
const [result] = await db.insert(vendorSnippets).values({
snippetCode: input.snippetCode,
slotId: input.slotId,
productIds: input.productIds,
isPermanent: input.isPermanent,
validTill: input.validTill,
}).returning()
return mapVendorSnippet(result)
}
export async function updateVendorSnippet(id: number, updates: {
snippetCode?: string
slotId?: number | null
productIds?: number[]
isPermanent?: boolean
validTill?: Date | null
}): Promise<AdminVendorSnippet | null> {
const [result] = await db.update(vendorSnippets)
.set(updates)
.where(eq(vendorSnippets.id, id))
.returning()
return result ? mapVendorSnippet(result) : null
}
export async function deleteVendorSnippet(id: number): Promise<AdminVendorSnippet | null> {
const [result] = await db.delete(vendorSnippets)
.where(eq(vendorSnippets.id, id))
.returning()
return result ? mapVendorSnippet(result) : null
}
export async function getProductsByIds(productIds: number[]): Promise<AdminVendorSnippetProduct[]> {
const products = await db.query.productInfo.findMany({
where: inArray(productInfo.id, productIds),
columns: { id: true, name: true },
})
const prods = products.map(mapProductSummary)
return prods
}
export async function getVendorSlotById(slotId: number): Promise<AdminDeliverySlot | null> {
const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, slotId),
})
return slot ? mapDeliverySlot(slot) : null
}
export async function getVendorOrdersBySlotId(slotId: number) {
return await db.query.orders.findMany({
where: eq(orders.slotId, slotId),
with: {
orderItems: {
with: {
product: {
with: {
unit: true,
},
},
},
},
orderStatus: true,
user: true,
slot: true,
},
orderBy: desc(orders.createdAt),
})
}
export async function getVendorOrders() {
return await db.query.orders.findMany({
with: {
user: true,
orderItems: {
with: {
product: {
with: {
unit: true,
},
},
},
},
},
orderBy: desc(orders.createdAt),
})
}
export async function getOrderItemsByOrderIds(orderIds: number[]) {
return await db.query.orderItems.findMany({
where: inArray(orderItems.orderId, orderIds),
with: {
product: {
with: {
unit: true,
},
},
},
})
}
export async function getOrderStatusByOrderIds(orderIds: number[]) {
return await db.query.orderStatus.findMany({
where: inArray(orderStatus.orderId, orderIds),
})
}
export async function updateVendorOrderItemPackaging(
orderItemId: number,
isPackaged: boolean
): Promise<AdminVendorUpdatePackagingResult> {
const orderItem = await db.query.orderItems.findFirst({
where: eq(orderItems.id, orderItemId),
with: {
order: {
with: {
slot: true,
},
},
},
})
if (!orderItem) {
return { success: false, message: 'Order item not found' }
}
if (!orderItem.order.slotId) {
return { success: false, message: 'Order item not associated with a vendor slot' }
}
const snippetExists = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.slotId, orderItem.order.slotId),
})
if (!snippetExists) {
return { success: false, message: "No vendor snippet found for this order's slot" }
}
const [updatedItem] = await db.update(orderItems)
.set({
is_packaged: isPackaged,
})
.where(eq(orderItems.id, orderItemId))
.returning({ id: orderItems.id })
if (!updatedItem) {
return { success: false, message: 'Failed to update packaging status' }
}
return { success: true, orderItemId, is_packaged: isPackaged }
}

View file

@ -0,0 +1,19 @@
// Common utility functions that can be used by both admin and user APIs
export function formatDate(date: Date): string {
return date.toISOString()
}
export function generateCode(prefix: string, length: number = 6): string {
const timestamp = Date.now().toString().slice(-length)
const random = Math.random().toString(36).substring(2, 8).toUpperCase()
return `${prefix}${timestamp}${random}`
}
export function calculateDiscount(amount: number, percent: number, maxDiscount?: number): number {
let discount = (amount * percent) / 100
if (maxDiscount && discount > maxDiscount) {
discount = maxDiscount
}
return discount
}

View file

@ -0,0 +1,26 @@
import type { D1Database } from '@cloudflare/workers-types'
import { drizzle, type DrizzleD1Database } from 'drizzle-orm/d1'
import * as schema from './schema'
type DbClient = DrizzleD1Database<typeof schema>
let dbInstance: DbClient | null = null
export function initDb(database: D1Database): void {
const base = drizzle(database, { schema }) as DbClient
dbInstance = Object.assign(base, {
transaction: async <T>(handler: (tx: DbClient) => Promise<T>): Promise<T> => {
return handler(base)
},
})
}
export const db = new Proxy({} as DbClient, {
get(_target, prop: keyof DbClient) {
if (!dbInstance) {
throw new Error('D1 database not initialized. Call initDb(env.DB) before using db helpers.')
}
return dbInstance[prop]
},
})

View file

@ -0,0 +1,125 @@
/*
* This was a one time script to change the composition of the signed urls
*/
import { db } from '@/src/db/db_index'
import {
userDetails,
productInfo,
productTagInfo,
complaints,
} from '@/src/db/schema'
import { eq, not, isNull } from 'drizzle-orm'
const S3_DOMAIN = 'https://s3.sgp.io.cloud.ovh.net'
const cleanImageUrl = (url: string): string => {
if (url.startsWith(S3_DOMAIN)) {
return url.replace(S3_DOMAIN + '/', '')
}
return url
}
const cleanImageUrls = (urls: string[]): string[] => {
return urls.map(cleanImageUrl)
}
async function migrateUserDetails() {
console.log('Migrating userDetails...')
const users = await db.select().from(userDetails).where(not(isNull(userDetails.profileImage)))
console.log(`Found ${users.length} user records with profile images`)
for (const user of users) {
if (user.profileImage) {
const cleanedUrl = cleanImageUrl(user.profileImage)
await db.update(userDetails)
.set({ profileImage: cleanedUrl })
.where(eq(userDetails.id, user.id))
}
}
console.log('userDetails migration completed')
}
async function migrateProductInfo() {
console.log('Migrating productInfo...')
const products = await db.select().from(productInfo).where(not(isNull(productInfo.images)))
console.log(`Found ${products.length} product records with images`)
for (const product of products) {
if (product.images && Array.isArray(product.images)) {
const cleanedUrls = cleanImageUrls(product.images)
await db.update(productInfo)
.set({ images: cleanedUrls })
.where(eq(productInfo.id, product.id))
}
}
console.log('productInfo migration completed')
}
async function migrateProductTagInfo() {
console.log('Migrating productTagInfo...')
const tags = await db.select().from(productTagInfo).where(not(isNull(productTagInfo.imageUrl)))
console.log(`Found ${tags.length} tag records with images`)
for (const tag of tags) {
if (tag.imageUrl) {
const cleanedUrl = cleanImageUrl(tag.imageUrl)
await db.update(productTagInfo)
.set({ imageUrl: cleanedUrl })
.where(eq(productTagInfo.id, tag.id))
}
}
console.log('productTagInfo migration completed')
}
async function migrateComplaints() {
console.log('Migrating complaints...')
const complaintRecords = await db.select().from(complaints).where(not(isNull(complaints.images)))
console.log(`Found ${complaintRecords.length} complaint records with images`)
for (const complaint of complaintRecords) {
if (complaint.images && Array.isArray(complaint.images)) {
const cleanedUrls = cleanImageUrls(complaint.images)
await db.update(complaints)
.set({ images: cleanedUrls })
.where(eq(complaints.id, complaint.id))
}
}
console.log('complaints migration completed')
}
async function runMigration() {
console.log('Starting image URL migration...')
console.log(`Removing S3 domain: ${S3_DOMAIN}`)
try {
await migrateUserDetails()
await migrateProductInfo()
await migrateProductTagInfo()
await migrateComplaints()
console.log('Migration completed successfully!')
} catch (error) {
console.error('Migration failed:', error)
throw error
}
}
// Run the migration
runMigration()
.then(() => {
console.log('Process completed successfully')
process.exit(0)
})
.catch((error) => {
console.error('Process failed:', error)
process.exit(1)
})

View file

@ -0,0 +1,728 @@
import {
sqliteTable,
integer,
text,
real,
uniqueIndex,
primaryKey,
check,
customType,
} from 'drizzle-orm/sqlite-core'
import { relations, sql } from 'drizzle-orm'
const jsonText = <T>(name: string) =>
customType<{ data: T | null; driverData: string | null }>({
dataType() {
return 'text'
},
toDriver(value) {
if (value === undefined || value === null) return null
return JSON.stringify(value)
},
fromDriver(value) {
if (value === null || value === undefined) return null
try {
return JSON.parse(String(value)) as T
} catch {
return null
}
},
})(name)
const numericText = (name: string) =>
customType<{ data: string | null; driverData: string | null }>({
dataType() {
return 'text'
},
toDriver(value) {
if (value === undefined || value === null) return null
return String(value)
},
fromDriver(value) {
if (value === null || value === undefined) return null
return String(value)
},
})(name)
const staffRoleValues = ['super_admin', 'admin', 'marketer', 'delivery_staff'] as const
const staffPermissionValues = ['crud_product', 'make_coupon', 'crud_staff_users'] as const
const uploadStatusValues = ['pending', 'claimed'] as const
const paymentStatusValues = ['pending', 'success', 'cod', 'failed'] as const
export const staffRoleEnum = (name: string) => text(name, { enum: staffRoleValues })
export const staffPermissionEnum = (name: string) => text(name, { enum: staffPermissionValues })
export const uploadStatusEnum = (name: string) => text(name, { enum: uploadStatusValues })
export const paymentStatusEnum = (name: string) => text(name, { enum: paymentStatusValues })
export const users = sqliteTable('users', {
id: integer().primaryKey({ autoIncrement: true }),
name: text(),
email: text(),
mobile: text(),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(),
}, (t) => ({
unq_email: uniqueIndex('unique_email').on(t.email),
}))
export const userDetails = sqliteTable('user_details', {
id: integer().primaryKey({ autoIncrement: true }),
userId: integer('user_id').notNull().references(() => users.id).unique(),
bio: text('bio'),
dateOfBirth: integer('date_of_birth', { mode: 'timestamp' }),
gender: text('gender'),
occupation: text('occupation'),
profileImage: text('profile_image'),
isSuspended: integer('is_suspended', { mode: 'boolean' }).notNull().default(false),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().defaultNow(),
})
export const userCreds = sqliteTable('user_creds', {
id: integer().primaryKey({ autoIncrement: true }),
userId: integer('user_id').notNull().references(() => users.id),
userPassword: text('user_password').notNull(),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(),
})
export const addressZones = sqliteTable('address_zones', {
id: integer().primaryKey({ autoIncrement: true }),
zoneName: text('zone_name').notNull(),
addedAt: integer('added_at', { mode: 'timestamp' }).notNull().defaultNow(),
})
export const addressAreas = sqliteTable('address_areas', {
id: integer().primaryKey({ autoIncrement: true }),
placeName: text('place_name').notNull(),
zoneId: integer('zone_id').references(() => addressZones.id),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(),
})
export const addresses = sqliteTable('addresses', {
id: integer().primaryKey({ autoIncrement: true }),
userId: integer('user_id').notNull().references(() => users.id),
name: text('name').notNull(),
phone: text('phone').notNull(),
addressLine1: text('address_line1').notNull(),
addressLine2: text('address_line2'),
city: text('city').notNull(),
state: text('state').notNull(),
pincode: text('pincode').notNull(),
isDefault: integer('is_default', { mode: 'boolean' }).notNull().default(false),
latitude: real('latitude'),
longitude: real('longitude'),
googleMapsUrl: text('google_maps_url'),
adminLatitude: real('admin_latitude'),
adminLongitude: real('admin_longitude'),
zoneId: integer('zone_id').references(() => addressZones.id),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(),
})
export const staffRoles = sqliteTable('staff_roles', {
id: integer().primaryKey({ autoIncrement: true }),
roleName: staffRoleEnum('role_name').notNull(),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(),
}, (t) => ({
unq_role_name: uniqueIndex('unique_role_name').on(t.roleName),
}))
export const staffPermissions = sqliteTable('staff_permissions', {
id: integer().primaryKey({ autoIncrement: true }),
permissionName: staffPermissionEnum('permission_name').notNull(),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(),
}, (t) => ({
unq_permission_name: uniqueIndex('unique_permission_name').on(t.permissionName),
}))
export const staffRolePermissions = sqliteTable('staff_role_permissions', {
id: integer().primaryKey({ autoIncrement: true }),
staffRoleId: integer('staff_role_id').notNull().references(() => staffRoles.id),
staffPermissionId: integer('staff_permission_id').notNull().references(() => staffPermissions.id),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(),
}, (t) => ({
unq_role_permission: uniqueIndex('unique_role_permission').on(t.staffRoleId, t.staffPermissionId),
}))
export const staffUsers = sqliteTable('staff_users', {
id: integer().primaryKey({ autoIncrement: true }),
name: text().notNull(),
password: text().notNull(),
staffRoleId: integer('staff_role_id').references(() => staffRoles.id),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(),
})
export const storeInfo = sqliteTable('store_info', {
id: integer().primaryKey({ autoIncrement: true }),
name: text().notNull(),
description: text(),
imageUrl: text('image_url'),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(),
owner: integer('owner').notNull().references(() => staffUsers.id),
})
export const units = sqliteTable('units', {
id: integer().primaryKey({ autoIncrement: true }),
shortNotation: text('short_notation').notNull(),
fullName: text('full_name').notNull(),
}, (t) => ({
unq_short_notation: uniqueIndex('unique_short_notation').on(t.shortNotation),
}))
export const productInfo = sqliteTable('product_info', {
id: integer().primaryKey({ autoIncrement: true }),
name: text().notNull(),
shortDescription: text('short_description'),
longDescription: text('long_description'),
unitId: integer('unit_id').notNull().references(() => units.id),
price: numericText('price').notNull(),
marketPrice: numericText('market_price'),
images: jsonText<string[] | null>('images'),
isOutOfStock: integer('is_out_of_stock', { mode: 'boolean' }).notNull().default(false),
isSuspended: integer('is_suspended', { mode: 'boolean' }).notNull().default(false),
isFlashAvailable: integer('is_flash_available', { mode: 'boolean' }).notNull().default(false),
flashPrice: numericText('flash_price'),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(),
incrementStep: real('increment_step').notNull().default(1),
productQuantity: real('product_quantity').notNull().default(1),
storeId: integer('store_id').references(() => storeInfo.id),
})
export const productGroupInfo = sqliteTable('product_group_info', {
id: integer().primaryKey({ autoIncrement: true }),
groupName: text('group_name').notNull(),
description: text(),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(),
})
export const productGroupMembership = sqliteTable('product_group_membership', {
productId: integer('product_id').notNull().references(() => productInfo.id),
groupId: integer('group_id').notNull().references(() => productGroupInfo.id),
addedAt: integer('added_at', { mode: 'timestamp' }).notNull().defaultNow(),
}, (t) => ({
pk: primaryKey({ columns: [t.productId, t.groupId], name: 'product_group_membership_pk' }),
}))
export const homeBanners = sqliteTable('home_banners', {
id: integer().primaryKey({ autoIncrement: true }),
name: text('name').notNull(),
imageUrl: text('image_url').notNull(),
description: text('description'),
productIds: jsonText<number[] | null>('product_ids'),
redirectUrl: text('redirect_url'),
serialNum: integer('serial_num'),
isActive: integer('is_active', { mode: 'boolean' }).notNull().default(false),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(),
lastUpdated: integer('last_updated', { mode: 'timestamp' }).notNull().defaultNow(),
})
export const productReviews = sqliteTable('product_reviews', {
id: integer().primaryKey({ autoIncrement: true }),
userId: integer('user_id').notNull().references(() => users.id),
productId: integer('product_id').notNull().references(() => productInfo.id),
reviewBody: text('review_body').notNull(),
imageUrls: jsonText<string[]>('image_urls').$defaultFn(() => []),
reviewTime: integer('review_time', { mode: 'timestamp' }).notNull().defaultNow(),
ratings: real('ratings').notNull(),
adminResponse: text('admin_response'),
adminResponseImages: jsonText<string[]>('admin_response_images').$defaultFn(() => []),
}, (t) => ({
ratingCheck: check('rating_check', sql`${t.ratings} >= 1 AND ${t.ratings} <= 5`),
}))
export const uploadUrlStatus = sqliteTable('upload_url_status', {
id: integer().primaryKey({ autoIncrement: true }),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(),
key: text('key').notNull(),
status: uploadStatusEnum('status').notNull().default('pending'),
})
export const productTagInfo = sqliteTable('product_tag_info', {
id: integer().primaryKey({ autoIncrement: true }),
tagName: text('tag_name').notNull().unique(),
tagDescription: text('tag_description'),
imageUrl: text('image_url'),
isDashboardTag: integer('is_dashboard_tag', { mode: 'boolean' }).notNull().default(false),
relatedStores: jsonText<number[]>('related_stores').$defaultFn(() => []),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(),
})
export const productTags = sqliteTable('product_tags', {
id: integer().primaryKey({ autoIncrement: true }),
productId: integer('product_id').notNull().references(() => productInfo.id),
tagId: integer('tag_id').notNull().references(() => productTagInfo.id),
assignedAt: integer('assigned_at', { mode: 'timestamp' }).notNull().defaultNow(),
}, (t) => ({
unq_product_tag: uniqueIndex('unique_product_tag').on(t.productId, t.tagId),
}))
export const deliverySlotInfo = sqliteTable('delivery_slot_info', {
id: integer().primaryKey({ autoIncrement: true }),
deliveryTime: integer('delivery_time', { mode: 'timestamp' }).notNull(),
freezeTime: integer('freeze_time', { mode: 'timestamp' }).notNull(),
isActive: integer('is_active', { mode: 'boolean' }).notNull().default(true),
isFlash: integer('is_flash', { mode: 'boolean' }).notNull().default(false),
isCapacityFull: integer('is_capacity_full', { mode: 'boolean' }).notNull().default(false),
deliverySequence: jsonText<Record<string, number>>('delivery_sequence').$defaultFn(() => ({})),
groupIds: jsonText<number[]>('group_ids').$defaultFn(() => []),
})
export const vendorSnippets = sqliteTable('vendor_snippets', {
id: integer().primaryKey({ autoIncrement: true }),
snippetCode: text('snippet_code').notNull().unique(),
slotId: integer('slot_id').references(() => deliverySlotInfo.id),
isPermanent: integer('is_permanent', { mode: 'boolean' }).notNull().default(false),
productIds: jsonText<number[]>('product_ids').notNull(),
validTill: integer('valid_till', { mode: 'timestamp' }),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(),
})
export const productSlots = sqliteTable('product_slots', {
productId: integer('product_id').notNull().references(() => productInfo.id),
slotId: integer('slot_id').notNull().references(() => deliverySlotInfo.id),
}, (t) => ({
pk: primaryKey({ columns: [t.productId, t.slotId], name: 'product_slot_pk' }),
}))
export const specialDeals = sqliteTable('special_deals', {
id: integer().primaryKey({ autoIncrement: true }),
productId: integer('product_id').notNull().references(() => productInfo.id),
quantity: numericText('quantity').notNull(),
price: numericText('price').notNull(),
validTill: integer('valid_till', { mode: 'timestamp' }).notNull(),
})
export const paymentInfoTable = sqliteTable('payment_info', {
id: integer().primaryKey({ autoIncrement: true }),
status: text().notNull(),
gateway: text().notNull(),
orderId: text('order_id'),
token: text('token'),
merchantOrderId: text('merchant_order_id').notNull().unique(),
payload: jsonText<unknown>('payload'),
})
export const orders = sqliteTable('orders', {
id: integer().primaryKey({ autoIncrement: true }),
userId: integer('user_id').notNull().references(() => users.id),
addressId: integer('address_id').notNull().references(() => addresses.id),
slotId: integer('slot_id').references(() => deliverySlotInfo.id),
isCod: integer('is_cod', { mode: 'boolean' }).notNull().default(false),
isOnlinePayment: integer('is_online_payment', { mode: 'boolean' }).notNull().default(false),
paymentInfoId: integer('payment_info_id').references(() => paymentInfoTable.id),
totalAmount: numericText('total_amount').notNull(),
deliveryCharge: numericText('delivery_charge').notNull().default('0'),
readableId: integer('readable_id').notNull(),
adminNotes: text('admin_notes'),
userNotes: text('user_notes'),
orderGroupId: text('order_group_id'),
orderGroupProportion: numericText('order_group_proportion'),
isFlashDelivery: integer('is_flash_delivery', { mode: 'boolean' }).notNull().default(false),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(),
})
export const orderItems = sqliteTable('order_items', {
id: integer().primaryKey({ autoIncrement: true }),
orderId: integer('order_id').notNull().references(() => orders.id),
productId: integer('product_id').notNull().references(() => productInfo.id),
quantity: text('quantity').notNull(),
price: numericText('price').notNull(),
discountedPrice: numericText('discounted_price'),
is_packaged: integer('is_packaged', { mode: 'boolean' }).notNull().default(false),
is_package_verified: integer('is_package_verified', { mode: 'boolean' }).notNull().default(false),
})
export const orderStatus = sqliteTable('order_status', {
id: integer().primaryKey({ autoIncrement: true }),
orderTime: integer('order_time', { mode: 'timestamp' }).notNull().defaultNow(),
userId: integer('user_id').notNull().references(() => users.id),
orderId: integer('order_id').notNull().references(() => orders.id),
isPackaged: integer('is_packaged', { mode: 'boolean' }).notNull().default(false),
isDelivered: integer('is_delivered', { mode: 'boolean' }).notNull().default(false),
isCancelled: integer('is_cancelled', { mode: 'boolean' }).notNull().default(false),
cancelReason: text('cancel_reason'),
isCancelledByAdmin: integer('is_cancelled_by_admin', { mode: 'boolean' }),
paymentStatus: paymentStatusEnum('payment_state').notNull().default('pending'),
cancellationUserNotes: text('cancellation_user_notes'),
cancellationAdminNotes: text('cancellation_admin_notes'),
cancellationReviewed: integer('cancellation_reviewed', { mode: 'boolean' }).notNull().default(false),
cancellationReviewedAt: integer('cancellation_reviewed_at', { mode: 'timestamp' }),
refundCouponId: integer('refund_coupon_id').references(() => coupons.id),
})
export const payments = sqliteTable('payments', {
id: integer().primaryKey({ autoIncrement: true }),
status: text().notNull(),
gateway: text().notNull(),
orderId: integer('order_id').notNull().references(() => orders.id),
token: text('token'),
merchantOrderId: text('merchant_order_id').notNull().unique(),
payload: jsonText<unknown>('payload'),
})
export const refunds = sqliteTable('refunds', {
id: integer().primaryKey({ autoIncrement: true }),
orderId: integer('order_id').notNull().references(() => orders.id),
refundAmount: numericText('refund_amount'),
refundStatus: text('refund_status').default('none'),
merchantRefundId: text('merchant_refund_id'),
refundProcessedAt: integer('refund_processed_at', { mode: 'timestamp' }),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(),
})
export const keyValStore = sqliteTable('key_val_store', {
key: text('key').primaryKey(),
value: jsonText<unknown>('value'),
})
export const notifications = sqliteTable('notifications', {
id: integer().primaryKey({ autoIncrement: true }),
userId: integer('user_id').notNull().references(() => users.id),
title: text().notNull(),
body: text().notNull(),
type: text(),
isRead: integer('is_read', { mode: 'boolean' }).notNull().default(false),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(),
})
export const productCategories = sqliteTable('product_categories', {
id: integer().primaryKey({ autoIncrement: true }),
name: text().notNull(),
description: text(),
})
export const cartItems = sqliteTable('cart_items', {
id: integer().primaryKey({ autoIncrement: true }),
userId: integer('user_id').notNull().references(() => users.id),
productId: integer('product_id').notNull().references(() => productInfo.id),
quantity: numericText('quantity').notNull(),
addedAt: integer('added_at', { mode: 'timestamp' }).notNull().defaultNow(),
}, (t) => ({
unq_user_product: uniqueIndex('unique_user_product').on(t.userId, t.productId),
}))
export const complaints = sqliteTable('complaints', {
id: integer().primaryKey({ autoIncrement: true }),
userId: integer('user_id').notNull().references(() => users.id),
orderId: integer('order_id').references(() => orders.id),
complaintBody: text('complaint_body').notNull(),
images: jsonText<string[] | null>('images'),
response: text('response'),
isResolved: integer('is_resolved', { mode: 'boolean' }).notNull().default(false),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(),
})
export const coupons = sqliteTable('coupons', {
id: integer().primaryKey({ autoIncrement: true }),
couponCode: text('coupon_code').notNull().unique(),
isUserBased: integer('is_user_based', { mode: 'boolean' }).notNull().default(false),
discountPercent: numericText('discount_percent'),
flatDiscount: numericText('flat_discount'),
minOrder: numericText('min_order'),
productIds: jsonText<number[] | null>('product_ids'),
createdBy: integer('created_by').notNull().references(() => staffUsers.id),
maxValue: numericText('max_value'),
isApplyForAll: integer('is_apply_for_all', { mode: 'boolean' }).notNull().default(false),
validTill: integer('valid_till', { mode: 'timestamp' }),
maxLimitForUser: integer('max_limit_for_user'),
isInvalidated: integer('is_invalidated', { mode: 'boolean' }).notNull().default(false),
exclusiveApply: integer('exclusive_apply', { mode: 'boolean' }).notNull().default(false),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(),
})
export const couponUsage = sqliteTable('coupon_usage', {
id: integer().primaryKey({ autoIncrement: true }),
userId: integer('user_id').notNull().references(() => users.id),
couponId: integer('coupon_id').notNull().references(() => coupons.id),
orderId: integer('order_id').references(() => orders.id),
orderItemId: integer('order_item_id').references(() => orderItems.id),
usedAt: integer('used_at', { mode: 'timestamp' }).notNull().defaultNow(),
})
export const couponApplicableUsers = sqliteTable('coupon_applicable_users', {
id: integer().primaryKey({ autoIncrement: true }),
couponId: integer('coupon_id').notNull().references(() => coupons.id),
userId: integer('user_id').notNull().references(() => users.id),
}, (t) => ({
unq_coupon_user: uniqueIndex('unique_coupon_user').on(t.couponId, t.userId),
}))
export const couponApplicableProducts = sqliteTable('coupon_applicable_products', {
id: integer().primaryKey({ autoIncrement: true }),
couponId: integer('coupon_id').notNull().references(() => coupons.id),
productId: integer('product_id').notNull().references(() => productInfo.id),
}, (t) => ({
unq_coupon_product: uniqueIndex('unique_coupon_product').on(t.couponId, t.productId),
}))
export const userIncidents = sqliteTable('user_incidents', {
id: integer().primaryKey({ autoIncrement: true }),
userId: integer('user_id').notNull().references(() => users.id),
orderId: integer('order_id').references(() => orders.id),
dateAdded: integer('date_added', { mode: 'timestamp' }).notNull().defaultNow(),
adminComment: text('admin_comment'),
addedBy: integer('added_by').references(() => staffUsers.id),
negativityScore: integer('negativity_score'),
})
export const reservedCoupons = sqliteTable('reserved_coupons', {
id: integer().primaryKey({ autoIncrement: true }),
secretCode: text('secret_code').notNull().unique(),
couponCode: text('coupon_code').notNull(),
discountPercent: numericText('discount_percent'),
flatDiscount: numericText('flat_discount'),
minOrder: numericText('min_order'),
productIds: jsonText<number[] | null>('product_ids'),
maxValue: numericText('max_value'),
validTill: integer('valid_till', { mode: 'timestamp' }),
maxLimitForUser: integer('max_limit_for_user'),
exclusiveApply: integer('exclusive_apply', { mode: 'boolean' }).notNull().default(false),
isRedeemed: integer('is_redeemed', { mode: 'boolean' }).notNull().default(false),
redeemedBy: integer('redeemed_by').references(() => users.id),
redeemedAt: integer('redeemed_at', { mode: 'timestamp' }),
createdBy: integer('created_by').notNull().references(() => staffUsers.id),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(),
})
export const notifCreds = sqliteTable('notif_creds', {
id: integer().primaryKey({ autoIncrement: true }),
token: text().notNull().unique(),
addedAt: integer('added_at', { mode: 'timestamp' }).notNull().defaultNow(),
userId: integer('user_id').notNull().references(() => users.id),
lastVerified: integer('last_verified', { mode: 'timestamp' }),
})
export const unloggedUserTokens = sqliteTable('unlogged_user_tokens', {
id: integer().primaryKey({ autoIncrement: true }),
token: text().notNull().unique(),
addedAt: integer('added_at', { mode: 'timestamp' }).notNull().defaultNow(),
lastVerified: integer('last_verified', { mode: 'timestamp' }),
})
export const userNotifications = sqliteTable('user_notifications', {
id: integer().primaryKey({ autoIncrement: true }),
title: text('title').notNull(),
imageUrl: text('image_url'),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(),
body: text('body').notNull(),
applicableUsers: jsonText<number[] | null>('applicable_users'),
})
// Relations
export const usersRelations = relations(users, ({ many, one }) => ({
addresses: many(addresses),
orders: many(orders),
notifications: many(notifications),
cartItems: many(cartItems),
userCreds: one(userCreds),
coupons: many(coupons),
couponUsages: many(couponUsage),
applicableCoupons: many(couponApplicableUsers),
userDetails: one(userDetails),
notifCreds: many(notifCreds),
userIncidents: many(userIncidents),
}))
export const userCredsRelations = relations(userCreds, ({ one }) => ({
user: one(users, { fields: [userCreds.userId], references: [users.id] }),
}))
export const staffUsersRelations = relations(staffUsers, ({ one, many }) => ({
role: one(staffRoles, { fields: [staffUsers.staffRoleId], references: [staffRoles.id] }),
coupons: many(coupons),
stores: many(storeInfo),
}))
export const addressesRelations = relations(addresses, ({ one, many }) => ({
user: one(users, { fields: [addresses.userId], references: [users.id] }),
orders: many(orders),
zone: one(addressZones, { fields: [addresses.zoneId], references: [addressZones.id] }),
}))
export const unitsRelations = relations(units, ({ many }) => ({
products: many(productInfo),
}))
export const productInfoRelations = relations(productInfo, ({ one, many }) => ({
unit: one(units, { fields: [productInfo.unitId], references: [units.id] }),
store: one(storeInfo, { fields: [productInfo.storeId], references: [storeInfo.id] }),
productSlots: many(productSlots),
specialDeals: many(specialDeals),
orderItems: many(orderItems),
cartItems: many(cartItems),
tags: many(productTags),
applicableCoupons: many(couponApplicableProducts),
reviews: many(productReviews),
groups: many(productGroupMembership),
}))
export const productTagInfoRelations = relations(productTagInfo, ({ many }) => ({
products: many(productTags),
}))
export const productTagsRelations = relations(productTags, ({ one }) => ({
product: one(productInfo, { fields: [productTags.productId], references: [productInfo.id] }),
tag: one(productTagInfo, { fields: [productTags.tagId], references: [productTagInfo.id] }),
}))
export const deliverySlotInfoRelations = relations(deliverySlotInfo, ({ many }) => ({
productSlots: many(productSlots),
orders: many(orders),
vendorSnippets: many(vendorSnippets),
}))
export const productSlotsRelations = relations(productSlots, ({ one }) => ({
product: one(productInfo, { fields: [productSlots.productId], references: [productInfo.id] }),
slot: one(deliverySlotInfo, { fields: [productSlots.slotId], references: [deliverySlotInfo.id] }),
}))
export const specialDealsRelations = relations(specialDeals, ({ one }) => ({
product: one(productInfo, { fields: [specialDeals.productId], references: [productInfo.id] }),
}))
export const ordersRelations = relations(orders, ({ one, many }) => ({
user: one(users, { fields: [orders.userId], references: [users.id] }),
address: one(addresses, { fields: [orders.addressId], references: [addresses.id] }),
slot: one(deliverySlotInfo, { fields: [orders.slotId], references: [deliverySlotInfo.id] }),
orderItems: many(orderItems),
payment: one(payments),
paymentInfo: one(paymentInfoTable, { fields: [orders.paymentInfoId], references: [paymentInfoTable.id] }),
orderStatus: many(orderStatus),
refunds: many(refunds),
couponUsages: many(couponUsage),
userIncidents: many(userIncidents),
}))
export const orderItemsRelations = relations(orderItems, ({ one }) => ({
order: one(orders, { fields: [orderItems.orderId], references: [orders.id] }),
product: one(productInfo, { fields: [orderItems.productId], references: [productInfo.id] }),
}))
export const orderStatusRelations = relations(orderStatus, ({ one }) => ({
order: one(orders, { fields: [orderStatus.orderId], references: [orders.id] }),
user: one(users, { fields: [orderStatus.userId], references: [users.id] }),
refundCoupon: one(coupons, { fields: [orderStatus.refundCouponId], references: [coupons.id] }),
}))
export const paymentInfoRelations = relations(paymentInfoTable, ({ one }) => ({
order: one(orders, { fields: [paymentInfoTable.id], references: [orders.paymentInfoId] }),
}))
export const paymentsRelations = relations(payments, ({ one }) => ({
order: one(orders, { fields: [payments.orderId], references: [orders.id] }),
}))
export const refundsRelations = relations(refunds, ({ one }) => ({
order: one(orders, { fields: [refunds.orderId], references: [orders.id] }),
}))
export const notificationsRelations = relations(notifications, ({ one }) => ({
user: one(users, { fields: [notifications.userId], references: [users.id] }),
}))
export const productCategoriesRelations = relations(productCategories, ({}) => ({}))
export const cartItemsRelations = relations(cartItems, ({ one }) => ({
user: one(users, { fields: [cartItems.userId], references: [users.id] }),
product: one(productInfo, { fields: [cartItems.productId], references: [productInfo.id] }),
}))
export const complaintsRelations = relations(complaints, ({ one }) => ({
user: one(users, { fields: [complaints.userId], references: [users.id] }),
order: one(orders, { fields: [complaints.orderId], references: [orders.id] }),
}))
export const couponsRelations = relations(coupons, ({ one, many }) => ({
creator: one(staffUsers, { fields: [coupons.createdBy], references: [staffUsers.id] }),
usages: many(couponUsage),
applicableUsers: many(couponApplicableUsers),
applicableProducts: many(couponApplicableProducts),
}))
export const couponUsageRelations = relations(couponUsage, ({ one }) => ({
user: one(users, { fields: [couponUsage.userId], references: [users.id] }),
coupon: one(coupons, { fields: [couponUsage.couponId], references: [coupons.id] }),
order: one(orders, { fields: [couponUsage.orderId], references: [orders.id] }),
orderItem: one(orderItems, { fields: [couponUsage.orderItemId], references: [orderItems.id] }),
}))
export const userDetailsRelations = relations(userDetails, ({ one }) => ({
user: one(users, { fields: [userDetails.userId], references: [users.id] }),
}))
export const notifCredsRelations = relations(notifCreds, ({ one }) => ({
user: one(users, { fields: [notifCreds.userId], references: [users.id] }),
}))
export const userNotificationsRelations = relations(userNotifications, ({}) => ({
// No relations needed for now
}))
export const storeInfoRelations = relations(storeInfo, ({ one, many }) => ({
owner: one(staffUsers, { fields: [storeInfo.owner], references: [staffUsers.id] }),
products: many(productInfo),
}))
export const couponApplicableUsersRelations = relations(couponApplicableUsers, ({ one }) => ({
coupon: one(coupons, { fields: [couponApplicableUsers.couponId], references: [coupons.id] }),
user: one(users, { fields: [couponApplicableUsers.userId], references: [users.id] }),
}))
export const couponApplicableProductsRelations = relations(couponApplicableProducts, ({ one }) => ({
coupon: one(coupons, { fields: [couponApplicableProducts.couponId], references: [coupons.id] }),
product: one(productInfo, { fields: [couponApplicableProducts.productId], references: [productInfo.id] }),
}))
export const reservedCouponsRelations = relations(reservedCoupons, ({ one }) => ({
redeemedUser: one(users, { fields: [reservedCoupons.redeemedBy], references: [users.id] }),
creator: one(staffUsers, { fields: [reservedCoupons.createdBy], references: [staffUsers.id] }),
}))
export const productReviewsRelations = relations(productReviews, ({ one }) => ({
user: one(users, { fields: [productReviews.userId], references: [users.id] }),
product: one(productInfo, { fields: [productReviews.productId], references: [productInfo.id] }),
}))
export const addressZonesRelations = relations(addressZones, ({ many }) => ({
addresses: many(addresses),
areas: many(addressAreas),
}))
export const addressAreasRelations = relations(addressAreas, ({ one }) => ({
zone: one(addressZones, { fields: [addressAreas.zoneId], references: [addressZones.id] }),
}))
export const productGroupInfoRelations = relations(productGroupInfo, ({ many }) => ({
memberships: many(productGroupMembership),
}))
export const productGroupMembershipRelations = relations(productGroupMembership, ({ one }) => ({
product: one(productInfo, { fields: [productGroupMembership.productId], references: [productInfo.id] }),
group: one(productGroupInfo, { fields: [productGroupMembership.groupId], references: [productGroupInfo.id] }),
}))
export const homeBannersRelations = relations(homeBanners, ({}) => ({
// Relations for productIds array would be more complex, skipping for now
}))
export const staffRolesRelations = relations(staffRoles, ({ many }) => ({
staffUsers: many(staffUsers),
rolePermissions: many(staffRolePermissions),
}))
export const staffPermissionsRelations = relations(staffPermissions, ({ many }) => ({
rolePermissions: many(staffRolePermissions),
}))
export const staffRolePermissionsRelations = relations(staffRolePermissions, ({ one }) => ({
role: one(staffRoles, { fields: [staffRolePermissions.staffRoleId], references: [staffRoles.id] }),
permission: one(staffPermissions, { fields: [staffRolePermissions.staffPermissionId], references: [staffPermissions.id] }),
}))
export const userIncidentsRelations = relations(userIncidents, ({ one }) => ({
user: one(users, { fields: [userIncidents.userId], references: [users.id] }),
order: one(orders, { fields: [userIncidents.orderId], references: [orders.id] }),
addedBy: one(staffUsers, { fields: [userIncidents.addedBy], references: [staffUsers.id] }),
}))
export const vendorSnippetsRelations = relations(vendorSnippets, ({ one }) => ({
slot: one(deliverySlotInfo, { fields: [vendorSnippets.slotId], references: [deliverySlotInfo.id] }),
}))

View file

@ -0,0 +1,147 @@
import { db } from '@/src/db/db_index'
import {
units,
productInfo,
deliverySlotInfo,
productSlots,
keyValStore,
staffRoles,
staffPermissions,
staffRolePermissions,
} from '@/src/db/schema'
import { eq } from 'drizzle-orm'
import { minOrderValue, deliveryCharge } from '@/src/lib/env-exporter'
import { CONST_KEYS } from '@/src/lib/const-keys'
export async function seed() {
console.log('Seeding database...')
// Seed units individually
const unitsToSeed = [
{ shortNotation: 'Kg', fullName: 'Kilogram' },
{ shortNotation: 'L', fullName: 'Litre' },
{ shortNotation: 'Dz', fullName: 'Dozen' },
{ shortNotation: 'Pc', fullName: 'Unit Piece' },
]
for (const unit of unitsToSeed) {
const existingUnit = await db.query.units.findFirst({
where: eq(units.shortNotation, unit.shortNotation),
})
if (!existingUnit) {
await db.insert(units).values(unit)
}
}
// Seed staff roles individually
const rolesToSeed = ['super_admin', 'admin', 'marketer', 'delivery_staff'] as const
for (const roleName of rolesToSeed) {
const existingRole = await db.query.staffRoles.findFirst({
where: eq(staffRoles.roleName, roleName),
})
if (!existingRole) {
await db.insert(staffRoles).values({ roleName })
}
}
// Seed staff permissions individually
const permissionsToSeed = ['crud_product', 'make_coupon', 'crud_staff_users'] as const
for (const permissionName of permissionsToSeed) {
const existingPermission = await db.query.staffPermissions.findFirst({
where: eq(staffPermissions.permissionName, permissionName),
})
if (!existingPermission) {
await db.insert(staffPermissions).values({ permissionName })
}
}
// Seed role-permission assignments
await db.transaction(async (tx) => {
// Get role IDs
const superAdminRole = await tx.query.staffRoles.findFirst({ where: eq(staffRoles.roleName, 'super_admin') })
const adminRole = await tx.query.staffRoles.findFirst({ where: eq(staffRoles.roleName, 'admin') })
const marketerRole = await tx.query.staffRoles.findFirst({ where: eq(staffRoles.roleName, 'marketer') })
// Get permission IDs
const crudProductPerm = await tx.query.staffPermissions.findFirst({ where: eq(staffPermissions.permissionName, 'crud_product') })
const makeCouponPerm = await tx.query.staffPermissions.findFirst({ where: eq(staffPermissions.permissionName, 'make_coupon') })
const crudStaffUsersPerm = await tx.query.staffPermissions.findFirst({ where: eq(staffPermissions.permissionName, 'crud_staff_users') })
// Assign all permissions to super_admin
;[crudProductPerm, makeCouponPerm, crudStaffUsersPerm].forEach(async (perm) => {
if (superAdminRole && perm) {
const existingSuperAdminPerm = await tx.query.staffRolePermissions.findFirst({
where: eq(staffRolePermissions.staffRoleId, superAdminRole.id) && eq(staffRolePermissions.staffPermissionId, perm.id),
})
if (!existingSuperAdminPerm) {
await tx.insert(staffRolePermissions).values({
staffRoleId: superAdminRole.id,
staffPermissionId: perm.id,
})
}
}
})
// Assign all permissions to admin
;[crudProductPerm, makeCouponPerm].forEach(async (perm) => {
if (adminRole && perm) {
const existingAdminPerm = await tx.query.staffRolePermissions.findFirst({
where: eq(staffRolePermissions.staffRoleId, adminRole.id) && eq(staffRolePermissions.staffPermissionId, perm.id),
})
if (!existingAdminPerm) {
await tx.insert(staffRolePermissions).values({
staffRoleId: adminRole.id,
staffPermissionId: perm.id,
})
}
}
})
// Assign make_coupon to marketer
if (marketerRole && makeCouponPerm) {
const existingMarketerCoupon = await tx.query.staffRolePermissions.findFirst({
where: eq(staffRolePermissions.staffRoleId, marketerRole.id) && eq(staffRolePermissions.staffPermissionId, makeCouponPerm.id),
})
if (!existingMarketerCoupon) {
await tx.insert(staffRolePermissions).values({
staffRoleId: marketerRole.id,
staffPermissionId: makeCouponPerm.id,
})
}
}
})
// Seed key-val store constants using CONST_KEYS
const constantsToSeed = [
{ key: CONST_KEYS.readableOrderId, value: 0 },
{ key: CONST_KEYS.minRegularOrderValue, value: minOrderValue },
{ key: CONST_KEYS.freeDeliveryThreshold, value: minOrderValue },
{ key: CONST_KEYS.deliveryCharge, value: deliveryCharge },
{ key: CONST_KEYS.flashFreeDeliveryThreshold, value: 500 },
{ key: CONST_KEYS.flashDeliveryCharge, value: 69 },
{ key: CONST_KEYS.popularItems, value: [] },
{ key: CONST_KEYS.allItemsOrder, value: [] },
{ key: CONST_KEYS.versionNum, value: '1.1.0' },
{ key: CONST_KEYS.playStoreUrl, value: 'https://play.google.com/store/apps/details?id=in.freshyo.app' },
{ key: CONST_KEYS.appStoreUrl, value: 'https://apps.apple.com/in/app/freshyo/id6756889077' },
{ key: CONST_KEYS.isFlashDeliveryEnabled, value: false },
{ key: CONST_KEYS.supportMobile, value: '8688182552' },
{ key: CONST_KEYS.supportEmail, value: 'qushammohd@gmail.com' },
]
for (const constant of constantsToSeed) {
const existing = await db.query.keyValStore.findFirst({
where: eq(keyValStore.key, constant.key),
})
if (!existing) {
await db.insert(keyValStore).values({
key: constant.key,
value: constant.value,
})
}
}
console.log('Seeding completed.')
}

View file

@ -0,0 +1,47 @@
import type { InferSelectModel } from 'drizzle-orm'
import type {
users,
addresses,
units,
productInfo,
deliverySlotInfo,
productSlots,
specialDeals,
orders,
orderItems,
payments,
notifications,
productCategories,
cartItems,
coupons,
} from '@/src/db/schema'
export type User = InferSelectModel<typeof users>
export type Address = InferSelectModel<typeof addresses>
export type Unit = InferSelectModel<typeof units>
export type ProductInfo = InferSelectModel<typeof productInfo>
export type DeliverySlotInfo = InferSelectModel<typeof deliverySlotInfo>
export type ProductSlot = InferSelectModel<typeof productSlots>
export type SpecialDeal = InferSelectModel<typeof specialDeals>
export type Order = InferSelectModel<typeof orders>
export type OrderItem = InferSelectModel<typeof orderItems>
export type Payment = InferSelectModel<typeof payments>
export type Notification = InferSelectModel<typeof notifications>
export type ProductCategory = InferSelectModel<typeof productCategories>
export type CartItem = InferSelectModel<typeof cartItems>
export type Coupon = InferSelectModel<typeof coupons>
// Combined types
export type ProductWithUnit = ProductInfo & {
unit: Unit
}
export type OrderWithItems = Order & {
items: (OrderItem & { product: ProductInfo })[]
address: Address
slot: DeliverySlotInfo
}
export type CartItemWithProduct = CartItem & {
product: ProductInfo
}

View file

@ -0,0 +1,114 @@
import { db } from '../db/db_index'
import { homeBanners } from '../db/schema'
import { desc, eq } from 'drizzle-orm'
export interface Banner {
id: number
name: string
imageUrl: string
description: string | null
productIds: number[] | null
redirectUrl: string | null
serialNum: number | null
isActive: boolean
createdAt: Date
lastUpdated: Date
}
type BannerRow = typeof homeBanners.$inferSelect
export async function getBanners(): Promise<Banner[]> {
const banners = await db.query.homeBanners.findMany({
orderBy: desc(homeBanners.createdAt),
}) as BannerRow[]
return banners.map((banner) => ({
id: banner.id,
name: banner.name,
imageUrl: banner.imageUrl,
description: banner.description,
productIds: banner.productIds || [],
redirectUrl: banner.redirectUrl,
serialNum: banner.serialNum,
isActive: banner.isActive,
createdAt: banner.createdAt,
lastUpdated: banner.lastUpdated,
}))
}
export async function getBannerById(id: number): Promise<Banner | null> {
const banner = await db.query.homeBanners.findFirst({
where: eq(homeBanners.id, id),
})
if (!banner) return null
return {
id: banner.id,
name: banner.name,
imageUrl: banner.imageUrl,
description: banner.description,
productIds: banner.productIds || [],
redirectUrl: banner.redirectUrl,
serialNum: banner.serialNum,
isActive: banner.isActive,
createdAt: banner.createdAt,
lastUpdated: banner.lastUpdated,
}
}
export type CreateBannerInput = Omit<Banner, 'id' | 'createdAt' | 'lastUpdated'>
export async function createBanner(input: CreateBannerInput): Promise<Banner> {
const [banner] = await db.insert(homeBanners).values({
name: input.name,
imageUrl: input.imageUrl,
description: input.description,
productIds: input.productIds,
redirectUrl: input.redirectUrl,
serialNum: input.serialNum,
isActive: input.isActive,
}).returning()
return {
id: banner.id,
name: banner.name,
imageUrl: banner.imageUrl,
description: banner.description,
productIds: banner.productIds || [],
redirectUrl: banner.redirectUrl,
serialNum: banner.serialNum,
isActive: banner.isActive,
createdAt: banner.createdAt,
lastUpdated: banner.lastUpdated,
}
}
export type UpdateBannerInput = Partial<Omit<Banner, 'id' | 'createdAt'>>
export async function updateBanner(id: number, input: UpdateBannerInput): Promise<Banner> {
const [banner] = await db.update(homeBanners)
.set({
...input,
lastUpdated: new Date(),
})
.where(eq(homeBanners.id, id))
.returning()
return {
id: banner.id,
name: banner.name,
imageUrl: banner.imageUrl,
description: banner.description,
productIds: banner.productIds || [],
redirectUrl: banner.redirectUrl,
serialNum: banner.serialNum,
isActive: banner.isActive,
createdAt: banner.createdAt,
lastUpdated: banner.lastUpdated,
}
}
export async function deleteBanner(id: number): Promise<void> {
await db.delete(homeBanners).where(eq(homeBanners.id, id))
}

View file

@ -0,0 +1,74 @@
import { db } from '../db/db_index'
import { complaints, users } from '../db/schema'
import { eq, desc, lt } from 'drizzle-orm'
export interface Complaint {
id: number
complaintBody: string
userId: number
orderId: number | null
isResolved: boolean
response: string | null
createdAt: Date
images: string[] | null
}
export interface ComplaintWithUser extends Complaint {
userName: string | null
userMobile: string | null
}
export async function getComplaints(
cursor?: number,
limit: number = 20
): Promise<{ complaints: ComplaintWithUser[]; hasMore: boolean }> {
const whereCondition = cursor ? lt(complaints.id, cursor) : undefined
const complaintsData = await db
.select({
id: complaints.id,
complaintBody: complaints.complaintBody,
userId: complaints.userId,
orderId: complaints.orderId,
isResolved: complaints.isResolved,
response: complaints.response,
createdAt: complaints.createdAt,
images: complaints.images,
userName: users.name,
userMobile: users.mobile,
})
.from(complaints)
.leftJoin(users, eq(complaints.userId, users.id))
.where(whereCondition)
.orderBy(desc(complaints.id))
.limit(limit + 1)
const hasMore = complaintsData.length > limit
const complaintsToReturn = hasMore ? complaintsData.slice(0, limit) : complaintsData
return {
complaints: complaintsToReturn.map((c) => ({
id: c.id,
complaintBody: c.complaintBody,
userId: c.userId,
orderId: c.orderId,
isResolved: c.isResolved,
response: c.response,
createdAt: c.createdAt,
images: c.images,
userName: c.userName,
userMobile: c.userMobile,
})),
hasMore,
}
}
export async function resolveComplaint(
id: number,
response?: string
): Promise<void> {
await db
.update(complaints)
.set({ isResolved: true, response })
.where(eq(complaints.id, id))
}

View file

@ -0,0 +1,29 @@
import { db } from '../db/db_index'
import { keyValStore } from '../db/schema'
export interface Constant {
key: string
value: any
}
export async function getAllConstants(): Promise<Constant[]> {
const constants = await db.select().from(keyValStore)
return constants.map(c => ({
key: c.key,
value: c.value,
}))
}
export async function upsertConstants(constants: Constant[]): Promise<void> {
await db.transaction(async (tx) => {
for (const { key, value } of constants) {
await tx.insert(keyValStore)
.values({ key, value })
.onConflictDoUpdate({
target: keyValStore.key,
set: { value },
})
}
})
}

View file

@ -0,0 +1,632 @@
import { db } from '../db/db_index';
import { coupons, reservedCoupons, users } from '../db/schema';
import { eq, and, like, or, inArray, lt, desc, asc } from 'drizzle-orm';
export interface Coupon {
id: number;
couponCode: string;
isUserBased: boolean;
discountPercent: string | null;
flatDiscount: string | null;
minOrder: string | null;
productIds: number[] | null;
maxValue: string | null;
isApplyForAll: boolean;
validTill: Date | null;
maxLimitForUser: number | null;
exclusiveApply: boolean;
isInvalidated: boolean;
createdAt: Date;
createdBy: number;
}
export async function getAllCoupons(
cursor?: number,
limit: number = 50,
search?: string
): Promise<{ coupons: any[]; hasMore: boolean }> {
let whereCondition = undefined;
const conditions = [];
if (cursor) {
conditions.push(lt(coupons.id, cursor));
}
if (search && search.trim()) {
conditions.push(like(coupons.couponCode, `%${search}%`));
}
if (conditions.length > 0) {
whereCondition = and(...conditions);
}
const result = await db.query.coupons.findMany({
where: whereCondition,
with: {
creator: true,
applicableUsers: {
with: {
user: true,
},
},
applicableProducts: {
with: {
product: true,
},
},
},
orderBy: (couponsTable: typeof coupons) => [desc(couponsTable.createdAt)],
limit: limit + 1,
});
const hasMore = result.length > limit;
const couponsList = hasMore ? result.slice(0, limit) : result;
return { coupons: couponsList, hasMore };
}
export async function getCouponById(id: number): Promise<any | null> {
const result = await db.query.coupons.findFirst({
where: eq(coupons.id, id),
with: {
creator: true,
applicableUsers: {
with: {
user: true,
},
},
applicableProducts: {
with: {
product: true,
},
},
},
});
return result || null;
}
export async function invalidateCoupon(id: number): Promise<Coupon> {
const result = await db.update(coupons)
.set({ isInvalidated: true })
.where(eq(coupons.id, id))
.returning();
return result[0];
}
export interface CouponValidationResult {
valid: boolean;
message?: string;
discountAmount?: number;
coupon?: Partial<Coupon>;
}
export async function validateCoupon(
code: string,
userId: number,
orderAmount: number
): Promise<CouponValidationResult> {
const coupon = await db.query.coupons.findFirst({
where: and(
eq(coupons.couponCode, code.toUpperCase()),
eq(coupons.isInvalidated, false)
),
});
if (!coupon) {
return { valid: false, message: "Coupon not found or invalidated" };
}
// Check expiry date
if (coupon.validTill && new Date(coupon.validTill) < new Date()) {
return { valid: false, message: "Coupon has expired" };
}
// Check if coupon applies to all users or specific user
if (!coupon.isApplyForAll && !coupon.isUserBased) {
return { valid: false, message: "Coupon is not available for use" };
}
// Check minimum order amount
const minOrderValue = coupon.minOrder ? parseFloat(coupon.minOrder) : 0;
if (minOrderValue > 0 && orderAmount < minOrderValue) {
return { valid: false, message: `Minimum order amount is ${minOrderValue}` };
}
// Calculate discount
let discountAmount = 0;
if (coupon.discountPercent) {
const percent = parseFloat(coupon.discountPercent);
discountAmount = (orderAmount * percent) / 100;
} else if (coupon.flatDiscount) {
discountAmount = parseFloat(coupon.flatDiscount);
}
// Apply max value limit
const maxValueLimit = coupon.maxValue ? parseFloat(coupon.maxValue) : 0;
if (maxValueLimit > 0 && discountAmount > maxValueLimit) {
discountAmount = maxValueLimit;
}
return {
valid: true,
discountAmount,
coupon: {
id: coupon.id,
discountPercent: coupon.discountPercent,
flatDiscount: coupon.flatDiscount,
maxValue: coupon.maxValue,
}
};
}
export async function getReservedCoupons(
cursor?: number,
limit: number = 50,
search?: string
): Promise<{ coupons: any[]; hasMore: boolean }> {
let whereCondition = undefined;
const conditions = [];
if (cursor) {
conditions.push(lt(reservedCoupons.id, cursor));
}
if (search && search.trim()) {
conditions.push(or(
like(reservedCoupons.secretCode, `%${search}%`),
like(reservedCoupons.couponCode, `%${search}%`)
));
}
if (conditions.length > 0) {
whereCondition = and(...conditions);
}
const result = await db.query.reservedCoupons.findMany({
where: whereCondition,
with: {
redeemedUser: true,
creator: true,
},
orderBy: (reservedCouponsTable: typeof reservedCoupons) => [desc(reservedCouponsTable.createdAt)],
limit: limit + 1,
});
const hasMore = result.length > limit;
const couponsList = hasMore ? result.slice(0, limit) : result;
return { coupons: couponsList, hasMore };
}
export interface UserMiniInfo {
id: number;
name: string;
mobile: string | null;
}
export async function getUsersForCoupon(
search?: string,
limit: number = 20,
offset: number = 0
): Promise<{ users: UserMiniInfo[] }> {
let whereCondition = undefined;
if (search && search.trim()) {
whereCondition = or(
like(users.name, `%${search}%`),
like(users.mobile, `%${search}%`)
);
}
const userList = await db.query.users.findMany({
where: whereCondition,
columns: {
id: true,
name: true,
mobile: true,
},
limit: limit,
offset: offset,
orderBy: (usersTable: typeof users) => [asc(usersTable.name)],
});
return {
users: userList.map((user: typeof users.$inferSelect) => ({
id: user.id,
name: user.name || 'Unknown',
mobile: user.mobile,
}))
};
}
// ============================================================================
// BATCH 2: Transaction Methods
// ============================================================================
import { couponApplicableUsers, couponApplicableProducts, orders, orderStatus } from '../db/schema';
export interface CreateCouponInput {
couponCode: string;
isUserBased: boolean;
discountPercent?: string;
flatDiscount?: string;
minOrder?: string;
productIds?: number[] | null;
maxValue?: string;
isApplyForAll: boolean;
validTill?: Date;
maxLimitForUser?: number;
exclusiveApply: boolean;
createdBy: number;
}
export async function createCouponWithRelations(
input: CreateCouponInput,
applicableUsers?: number[],
applicableProducts?: number[]
): Promise<Coupon> {
return await db.transaction(async (tx) => {
// Create the coupon
const [coupon] = await tx.insert(coupons).values({
couponCode: input.couponCode,
isUserBased: input.isUserBased,
discountPercent: input.discountPercent,
flatDiscount: input.flatDiscount,
minOrder: input.minOrder,
productIds: input.productIds,
createdBy: input.createdBy,
maxValue: input.maxValue,
isApplyForAll: input.isApplyForAll,
validTill: input.validTill,
maxLimitForUser: input.maxLimitForUser,
exclusiveApply: input.exclusiveApply,
}).returning();
// Insert applicable users
if (applicableUsers && applicableUsers.length > 0) {
await tx.insert(couponApplicableUsers).values(
applicableUsers.map(userId => ({
couponId: coupon.id,
userId,
}))
);
}
// Insert applicable products
if (applicableProducts && applicableProducts.length > 0) {
await tx.insert(couponApplicableProducts).values(
applicableProducts.map(productId => ({
couponId: coupon.id,
productId,
}))
);
}
return {
id: coupon.id,
couponCode: coupon.couponCode,
isUserBased: coupon.isUserBased,
discountPercent: coupon.discountPercent,
flatDiscount: coupon.flatDiscount,
minOrder: coupon.minOrder,
productIds: coupon.productIds,
maxValue: coupon.maxValue,
isApplyForAll: coupon.isApplyForAll,
validTill: coupon.validTill,
maxLimitForUser: coupon.maxLimitForUser,
exclusiveApply: coupon.exclusiveApply,
isInvalidated: coupon.isInvalidated,
createdAt: coupon.createdAt,
createdBy: coupon.createdBy,
};
});
}
export interface UpdateCouponInput {
couponCode?: string;
isUserBased?: boolean;
discountPercent?: string;
flatDiscount?: string;
minOrder?: string;
productIds?: number[] | null;
maxValue?: string;
isApplyForAll?: boolean;
validTill?: Date | null;
maxLimitForUser?: number;
exclusiveApply?: boolean;
isInvalidated?: boolean;
}
export async function updateCouponWithRelations(
id: number,
input: UpdateCouponInput,
applicableUsers?: number[],
applicableProducts?: number[]
): Promise<Coupon> {
return await db.transaction(async (tx) => {
// Update the coupon
const [coupon] = await tx.update(coupons)
.set({
...input,
})
.where(eq(coupons.id, id))
.returning();
// Update applicable users: delete existing and insert new
if (applicableUsers !== undefined) {
await tx.delete(couponApplicableUsers).where(eq(couponApplicableUsers.couponId, id));
if (applicableUsers.length > 0) {
await tx.insert(couponApplicableUsers).values(
applicableUsers.map(userId => ({
couponId: id,
userId,
}))
);
}
}
// Update applicable products: delete existing and insert new
if (applicableProducts !== undefined) {
await tx.delete(couponApplicableProducts).where(eq(couponApplicableProducts.couponId, id));
if (applicableProducts.length > 0) {
await tx.insert(couponApplicableProducts).values(
applicableProducts.map(productId => ({
couponId: id,
productId,
}))
);
}
}
return {
id: coupon.id,
couponCode: coupon.couponCode,
isUserBased: coupon.isUserBased,
discountPercent: coupon.discountPercent,
flatDiscount: coupon.flatDiscount,
minOrder: coupon.minOrder,
productIds: coupon.productIds,
maxValue: coupon.maxValue,
isApplyForAll: coupon.isApplyForAll,
validTill: coupon.validTill,
maxLimitForUser: coupon.maxLimitForUser,
exclusiveApply: coupon.exclusiveApply,
isInvalidated: coupon.isInvalidated,
createdAt: coupon.createdAt,
createdBy: coupon.createdBy,
};
});
}
export async function generateCancellationCoupon(
orderId: number,
staffUserId: number,
userId: number,
orderAmount: number,
couponCode: string
): Promise<Coupon> {
return await db.transaction(async (tx) => {
// Calculate expiry date (30 days from now)
const expiryDate = new Date();
expiryDate.setDate(expiryDate.getDate() + 30);
// Create the coupon
const [coupon] = await tx.insert(coupons).values({
couponCode,
isUserBased: true,
flatDiscount: orderAmount.toString(),
minOrder: orderAmount.toString(),
maxValue: orderAmount.toString(),
validTill: expiryDate,
maxLimitForUser: 1,
createdBy: staffUserId,
isApplyForAll: false,
}).returning();
// Insert applicable users
await tx.insert(couponApplicableUsers).values({
couponId: coupon.id,
userId,
});
// Update order_status with refund coupon ID
await tx.update(orderStatus)
.set({ refundCouponId: coupon.id })
.where(eq(orderStatus.orderId, orderId));
return {
id: coupon.id,
couponCode: coupon.couponCode,
isUserBased: coupon.isUserBased,
discountPercent: coupon.discountPercent,
flatDiscount: coupon.flatDiscount,
minOrder: coupon.minOrder,
productIds: coupon.productIds,
maxValue: coupon.maxValue,
isApplyForAll: coupon.isApplyForAll,
validTill: coupon.validTill,
maxLimitForUser: coupon.maxLimitForUser,
exclusiveApply: coupon.exclusiveApply,
isInvalidated: coupon.isInvalidated,
createdAt: coupon.createdAt,
createdBy: coupon.createdBy,
};
});
}
export interface CreateReservedCouponInput {
secretCode: string;
couponCode: string;
discountPercent?: string;
flatDiscount?: string;
minOrder?: string;
productIds?: number[] | null;
maxValue?: string;
validTill?: Date;
maxLimitForUser?: number;
exclusiveApply: boolean;
createdBy: number;
}
export async function createReservedCouponWithProducts(
input: CreateReservedCouponInput,
applicableProducts?: number[]
): Promise<any> {
return await db.transaction(async (tx) => {
const [coupon] = await tx.insert(reservedCoupons).values({
secretCode: input.secretCode,
couponCode: input.couponCode,
discountPercent: input.discountPercent,
flatDiscount: input.flatDiscount,
minOrder: input.minOrder,
productIds: input.productIds,
maxValue: input.maxValue,
validTill: input.validTill,
maxLimitForUser: input.maxLimitForUser,
exclusiveApply: input.exclusiveApply,
createdBy: input.createdBy,
}).returning();
// Insert applicable products if provided
if (applicableProducts && applicableProducts.length > 0) {
await tx.insert(couponApplicableProducts).values(
applicableProducts.map(productId => ({
couponId: coupon.id,
productId,
}))
);
}
return coupon;
});
}
export async function getOrCreateUserByMobile(
mobile: string
): Promise<{ id: number; mobile: string; name: string | null }> {
return await db.transaction(async (tx) => {
// Check if user exists
let user = await tx.query.users.findFirst({
where: eq(users.mobile, mobile),
});
if (!user) {
// Create new user
const [newUser] = await tx.insert(users).values({
name: null,
email: null,
mobile,
}).returning();
user = newUser;
}
return {
id: user.id,
mobile: user.mobile,
name: user.name,
};
});
}
export async function createCouponForUser(
mobile: string,
couponCode: string,
staffUserId: number
): Promise<{ coupon: Coupon; user: { id: number; mobile: string; name: string | null } }> {
return await db.transaction(async (tx) => {
// Get or create user
let user = await tx.query.users.findFirst({
where: eq(users.mobile, mobile),
});
if (!user) {
const [newUser] = await tx.insert(users).values({
name: null,
email: null,
mobile,
}).returning();
user = newUser;
}
// Create the coupon
const [coupon] = await tx.insert(coupons).values({
couponCode,
isUserBased: true,
discountPercent: "20",
minOrder: "1000",
maxValue: "500",
maxLimitForUser: 1,
isApplyForAll: false,
exclusiveApply: false,
createdBy: staffUserId,
validTill: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000), // 90 days from now
}).returning();
// Associate coupon with user
await tx.insert(couponApplicableUsers).values({
couponId: coupon.id,
userId: user.id,
});
return {
coupon: {
id: coupon.id,
couponCode: coupon.couponCode,
isUserBased: coupon.isUserBased,
discountPercent: coupon.discountPercent,
flatDiscount: coupon.flatDiscount,
minOrder: coupon.minOrder,
productIds: coupon.productIds,
maxValue: coupon.maxValue,
isApplyForAll: coupon.isApplyForAll,
validTill: coupon.validTill,
maxLimitForUser: coupon.maxLimitForUser,
exclusiveApply: coupon.exclusiveApply,
isInvalidated: coupon.isInvalidated,
createdAt: coupon.createdAt,
createdBy: coupon.createdBy,
},
user: {
id: user.id,
mobile: user.mobile,
name: user.name,
},
};
});
}
// ============================================================================
// Utility Functions
// ============================================================================
export async function checkUsersExist(userIds: number[]): Promise<boolean> {
const existingUsers = await db.query.users.findMany({
where: inArray(users.id, userIds),
columns: { id: true },
});
return existingUsers.length === userIds.length;
}
export async function checkCouponExists(couponCode: string): Promise<boolean> {
const existing = await db.query.coupons.findFirst({
where: eq(coupons.couponCode, couponCode),
});
return !!existing;
}
export async function checkReservedCouponExists(secretCode: string): Promise<boolean> {
const existing = await db.query.reservedCoupons.findFirst({
where: eq(reservedCoupons.secretCode, secretCode),
});
return !!existing;
}
export async function getOrderWithUser(orderId: number): Promise<any | null> {
return await db.query.orders.findFirst({
where: eq(orders.id, orderId),
with: {
user: true,
},
});
}

View file

@ -0,0 +1,269 @@
import { db } from '../db/db_index';
import { orders, orderItems, orderStatus, users, addresses, refunds, complaints, payments } from '../db/schema';
import { eq, and, gte, lt, desc, inArray, sql } from 'drizzle-orm';
export async function updateOrderNotes(orderId: number, adminNotes: string | null): Promise<any> {
const [result] = await db
.update(orders)
.set({ adminNotes })
.where(eq(orders.id, orderId))
.returning();
return result;
}
export async function getOrderWithDetails(orderId: number): Promise<any | null> {
return await db.query.orders.findFirst({
where: eq(orders.id, orderId),
with: {
orderItems: {
with: {
product: {
with: {
unit: true,
},
},
},
},
user: true,
address: true,
orderStatus: true,
slot: true,
payments: true,
refunds: true,
},
});
}
export async function getFullOrder(orderId: number): Promise<any | null> {
return await db.query.orders.findFirst({
where: eq(orders.id, orderId),
with: {
orderItems: {
with: {
product: {
with: {
unit: true,
},
},
},
},
user: {
with: {
userDetails: true,
},
},
address: true,
orderStatus: true,
slot: true,
payments: true,
refunds: true,
complaints: true,
},
});
}
export async function getOrderDetails(orderId: number): Promise<any | null> {
return await db.query.orders.findFirst({
where: eq(orders.id, orderId),
with: {
orderItems: {
with: {
product: {
with: {
unit: true,
},
},
},
},
user: true,
address: true,
orderStatus: true,
slot: true,
payments: true,
refunds: true,
complaints: true,
},
});
}
export async function getAllOrders(
limit: number,
cursor?: number,
slotId?: number | null,
filters?: any
): Promise<{ orders: any[]; hasMore: boolean }> {
let whereConditions = [];
if (cursor) {
whereConditions.push(lt(orders.id, cursor));
}
if (slotId) {
whereConditions.push(eq(orders.slotId, slotId));
}
// Add filter conditions
if (filters) {
if (filters.packagedFilter === 'packaged') {
whereConditions.push(
sql`${orders.id} IN (SELECT ${orderStatus.orderId} FROM ${orderStatus} WHERE ${orderStatus.isPackaged} = 1)`
);
} else if (filters.packagedFilter === 'not_packaged') {
whereConditions.push(
sql`${orders.id} IN (SELECT ${orderStatus.orderId} FROM ${orderStatus} WHERE ${orderStatus.isPackaged} = 0)`
);
}
if (filters.deliveredFilter === 'delivered') {
whereConditions.push(
sql`${orders.id} IN (SELECT ${orderStatus.orderId} FROM ${orderStatus} WHERE ${orderStatus.isDelivered} = 1)`
);
} else if (filters.deliveredFilter === 'not_delivered') {
whereConditions.push(
sql`${orders.id} IN (SELECT ${orderStatus.orderId} FROM ${orderStatus} WHERE ${orderStatus.isDelivered} = 0)`
);
}
if (filters.flashDeliveryFilter === 'flash') {
whereConditions.push(eq(orders.isFlashDelivery, true));
} else if (filters.flashDeliveryFilter === 'regular') {
whereConditions.push(eq(orders.isFlashDelivery, false));
}
}
const ordersList = await db.query.orders.findMany({
where: whereConditions.length > 0 ? and(...whereConditions) : undefined,
with: {
orderItems: {
with: {
product: true,
},
},
user: true,
orderStatus: true,
slot: true,
},
orderBy: desc(orders.id),
limit: limit + 1,
});
const hasMore = ordersList.length > limit;
return { orders: hasMore ? ordersList.slice(0, limit) : ordersList, hasMore };
}
export async function getOrdersBySlotId(slotId: number): Promise<any[]> {
return await db.query.orders.findMany({
where: eq(orders.slotId, slotId),
with: {
orderItems: {
with: {
product: true,
},
},
user: true,
orderStatus: true,
address: true,
},
orderBy: desc(orders.createdAt),
});
}
export async function updateOrderPackaged(orderId: number, isPackaged: boolean): Promise<any> {
const [result] = await db
.update(orderStatus)
.set({ isPackaged })
.where(eq(orderStatus.orderId, orderId))
.returning();
return result;
}
export async function updateOrderDelivered(orderId: number, isDelivered: boolean): Promise<any> {
const [result] = await db
.update(orderStatus)
.set({ isDelivered })
.where(eq(orderStatus.orderId, orderId))
.returning();
return result;
}
export async function updateOrderItemPackaging(
orderItemId: number,
isPackaged: boolean,
isPackageVerified: boolean
): Promise<void> {
await db.update(orderItems)
.set({ is_packaged: isPackaged, is_package_verified: isPackageVerified })
.where(eq(orderItems.id, orderItemId));
}
export async function updateAddressCoords(addressId: number, lat: number, lng: number): Promise<void> {
await db.update(addresses)
.set({ adminLatitude: lat, adminLongitude: lng })
.where(eq(addresses.id, addressId));
}
export async function getOrderStatus(orderId: number): Promise<any | null> {
return await db.query.orderStatus.findFirst({
where: eq(orderStatus.orderId, orderId),
});
}
export async function cancelOrder(orderId: number, reason: string): Promise<any> {
return await db.transaction(async (tx) => {
const order = await tx.query.orders.findFirst({
where: eq(orders.id, orderId),
})
if (!order) {
return null
}
await tx.update(orderStatus)
.set({
isCancelled: true,
cancelReason: reason,
})
.where(eq(orderStatus.orderId, orderId))
return order
});
}
export async function getTodaysOrders(slotId?: number): Promise<any[]> {
const today = new Date();
today.setHours(0, 0, 0, 0);
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
let whereConditions = [
gte(orders.createdAt, today),
lt(orders.createdAt, tomorrow),
];
if (slotId) {
whereConditions.push(eq(orders.slotId, slotId));
}
return await db.query.orders.findMany({
where: and(...whereConditions),
with: {
orderItems: {
with: {
product: true,
},
},
user: true,
orderStatus: true,
},
orderBy: desc(orders.createdAt),
});
}
export async function removeDeliveryCharge(orderId: number): Promise<any> {
const [result] = await db
.update(orders)
.set({ deliveryCharge: '0' })
.where(eq(orders.id, orderId))
.returning();
return result;
}

View file

@ -0,0 +1,130 @@
import { db } from '../db/db_index';
import { productInfo, units, specialDeals, productSlots, productTags, productReviews, productGroupInfo, productGroupMembership } from '../db/schema';
import { eq, and, inArray, desc, sql, asc } from 'drizzle-orm';
export async function getAllProducts(): Promise<any[]> {
return await db.query.productInfo.findMany({
orderBy: productInfo.name,
with: {
unit: true,
store: true,
},
});
}
export async function getProductById(id: number): Promise<any | null> {
return await db.query.productInfo.findFirst({
where: eq(productInfo.id, id),
with: {
unit: true,
store: true,
productSlots: {
with: {
slot: true,
},
},
specialDeals: true,
productTags: {
with: {
tag: true,
},
},
},
});
}
export async function createProduct(input: any): Promise<any> {
const [product] = await db.insert(productInfo).values(input).returning();
return product;
}
export async function updateProduct(id: number, updates: any): Promise<any> {
const [product] = await db.update(productInfo)
.set(updates)
.where(eq(productInfo.id, id))
.returning();
return product;
}
export async function toggleProductOutOfStock(id: number, isOutOfStock: boolean): Promise<any> {
const [product] = await db.update(productInfo)
.set({ isOutOfStock })
.where(eq(productInfo.id, id))
.returning();
return product;
}
export async function getAllUnits(): Promise<any[]> {
return await db.query.units.findMany({
orderBy: units.shortNotation,
});
}
export async function getAllProductTags(): Promise<any[]> {
return await db.query.productTags.findMany({
with: {
products: {
with: {
product: true,
},
},
},
});
}
export async function getProductReviews(productId: number): Promise<any[]> {
return await db.query.productReviews.findMany({
where: eq(productReviews.productId, productId),
with: {
user: true,
},
orderBy: desc(productReviews.reviewTime),
});
}
export async function respondToReview(reviewId: number, adminResponse: string): Promise<void> {
await db.update(productReviews)
.set({ adminResponse })
.where(eq(productReviews.id, reviewId));
}
export async function getAllProductGroups(): Promise<any[]> {
return await db.query.productGroupInfo.findMany({
with: {
products: {
with: {
product: true,
},
},
},
});
}
export async function createProductGroup(name: string): Promise<any> {
const [group] = await db.insert(productGroupInfo).values({ groupName: name }).returning();
return group;
}
export async function updateProductGroup(id: number, name: string): Promise<any> {
const [group] = await db.update(productGroupInfo)
.set({ groupName: name })
.where(eq(productGroupInfo.id, id))
.returning();
return group;
}
export async function deleteProductGroup(id: number): Promise<void> {
await db.delete(productGroupInfo).where(eq(productGroupInfo.id, id));
}
export async function addProductToGroup(groupId: number, productId: number): Promise<void> {
await db.insert(productGroupMembership).values({ groupId, productId });
}
export async function removeProductFromGroup(groupId: number, productId: number): Promise<void> {
await db.delete(productGroupMembership)
.where(and(
eq(productGroupMembership.groupId, groupId),
eq(productGroupMembership.productId, productId)
));
}

View file

@ -0,0 +1,101 @@
import { db } from '../db/db_index';
import { deliverySlotInfo, productSlots, productInfo, vendorSnippets } from '../db/schema';
import { eq, and, inArray, desc } from 'drizzle-orm';
export async function getAllSlots(): Promise<any[]> {
return await db.query.deliverySlotInfo.findMany({
orderBy: desc(deliverySlotInfo.deliveryTime),
with: {
productSlots: {
with: {
product: true,
},
},
vendorSnippets: true,
},
});
}
export async function getSlotById(id: number): Promise<any | null> {
return await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, id),
with: {
productSlots: {
with: {
product: true,
},
},
vendorSnippets: {
with: {
slot: true,
},
},
},
});
}
export async function createSlot(input: any): Promise<any> {
const [slot] = await db.insert(deliverySlotInfo).values(input).returning();
return slot;
}
export async function updateSlot(id: number, updates: any): Promise<any> {
const [slot] = await db.update(deliverySlotInfo)
.set(updates)
.where(eq(deliverySlotInfo.id, id))
.returning();
return slot;
}
export async function deleteSlot(id: number): Promise<void> {
await db.delete(deliverySlotInfo).where(eq(deliverySlotInfo.id, id));
}
export async function getSlotProducts(slotId: number): Promise<any[]> {
return await db.query.productSlots.findMany({
where: eq(productSlots.slotId, slotId),
with: {
product: true,
},
});
}
export async function addProductToSlot(slotId: number, productId: number): Promise<void> {
await db.insert(productSlots).values({ slotId, productId });
}
export async function removeProductFromSlot(slotId: number, productId: number): Promise<void> {
await db.delete(productSlots)
.where(and(
eq(productSlots.slotId, slotId),
eq(productSlots.productId, productId)
));
}
export async function clearSlotProducts(slotId: number): Promise<void> {
await db.delete(productSlots).where(eq(productSlots.slotId, slotId));
}
export async function updateSlotCapacity(slotId: number, maxCapacity: number): Promise<any> {
const [slot] = await db.update(deliverySlotInfo)
.set({ isCapacityFull: Boolean(maxCapacity) })
.where(eq(deliverySlotInfo.id, slotId))
.returning();
return slot;
}
export async function getSlotDeliverySequence(slotId: number): Promise<any | null> {
const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, slotId),
columns: {
deliverySequence: true,
},
});
return slot?.deliverySequence || null;
}
export async function updateSlotDeliverySequence(slotId: number, sequence: any): Promise<void> {
await db.update(deliverySlotInfo)
.set({ deliverySequence: sequence })
.where(eq(deliverySlotInfo.id, slotId));
}

View file

@ -0,0 +1,153 @@
import { db } from '../db/db_index';
import { staffUsers, staffRoles, users, userDetails, orders } from '../db/schema';
import { eq, or, and, lt, desc, like } from 'drizzle-orm';
export interface StaffUser {
id: number;
name: string;
password: string;
staffRoleId: number;
createdAt: Date;
}
export async function getStaffUserByName(name: string): Promise<StaffUser | null> {
const staff = await db.query.staffUsers.findFirst({
where: eq(staffUsers.name, name),
});
return staff || null;
}
export async function getAllStaff(): Promise<any[]> {
const staff = await db.query.staffUsers.findMany({
columns: {
id: true,
name: true,
},
with: {
role: {
with: {
rolePermissions: {
with: {
permission: true,
},
},
},
},
},
});
return staff;
}
export async function getStaffByName(name: string): Promise<StaffUser | null> {
const staff = await db.query.staffUsers.findFirst({
where: eq(staffUsers.name, name),
});
return staff || null;
}
export async function getAllUsers(
cursor?: number,
limit: number = 20,
search?: string
): Promise<{ users: any[]; hasMore: boolean }> {
let whereCondition = undefined;
if (search) {
whereCondition = or(
like(users.name, `%${search}%`),
like(users.email, `%${search}%`),
like(users.mobile, `%${search}%`)
);
}
if (cursor) {
const cursorCondition = lt(users.id, cursor);
whereCondition = whereCondition ? and(whereCondition, cursorCondition) : cursorCondition;
}
const allUsers = await db.query.users.findMany({
where: whereCondition,
with: {
userDetails: true,
},
orderBy: desc(users.id),
limit: limit + 1,
});
const hasMore = allUsers.length > limit;
const usersToReturn = hasMore ? allUsers.slice(0, limit) : allUsers;
return { users: usersToReturn, hasMore };
}
export async function getUserWithDetails(userId: number): Promise<any | null> {
const user = await db.query.users.findFirst({
where: eq(users.id, userId),
with: {
userDetails: true,
orders: {
orderBy: desc(orders.createdAt),
limit: 1,
},
},
});
return user || null;
}
export async function updateUserSuspension(userId: number, isSuspended: boolean): Promise<void> {
await db
.insert(userDetails)
.values({ userId, isSuspended })
.onConflictDoUpdate({
target: userDetails.userId,
set: { isSuspended },
});
}
export async function checkStaffUserExists(name: string): Promise<boolean> {
const existingUser = await db.query.staffUsers.findFirst({
where: eq(staffUsers.name, name),
});
return !!existingUser;
}
export async function checkStaffRoleExists(roleId: number): Promise<boolean> {
const role = await db.query.staffRoles.findFirst({
where: eq(staffRoles.id, roleId),
});
return !!role;
}
export async function createStaffUser(
name: string,
password: string,
roleId: number
): Promise<StaffUser> {
const [newUser] = await db.insert(staffUsers).values({
name: name.trim(),
password,
staffRoleId: roleId,
}).returning();
return {
id: newUser.id,
name: newUser.name,
password: newUser.password,
staffRoleId: newUser.staffRoleId ?? roleId,
createdAt: newUser.createdAt,
};
}
export async function getAllRoles(): Promise<any[]> {
const roles = await db.query.staffRoles.findMany({
columns: {
id: true,
roleName: true,
},
});
return roles;
}

View file

@ -0,0 +1,150 @@
import { db } from '../db/db_index';
import { storeInfo, productInfo } from '../db/schema';
import { eq, inArray } from 'drizzle-orm';
export interface Store {
id: number;
name: string;
description: string | null;
imageUrl: string | null;
owner: number;
createdAt: Date;
updatedAt: Date;
}
export async function getAllStores(): Promise<any[]> {
const stores = await db.query.storeInfo.findMany({
with: {
owner: true,
},
});
return stores;
}
export async function getStoreById(id: number): Promise<any | null> {
const store = await db.query.storeInfo.findFirst({
where: eq(storeInfo.id, id),
with: {
owner: true,
},
});
return store || null;
}
export interface CreateStoreInput {
name: string;
description?: string;
imageUrl?: string;
owner: number;
}
export async function createStore(
input: CreateStoreInput,
products?: number[]
): Promise<Store> {
const [newStore] = await db
.insert(storeInfo)
.values({
name: input.name,
description: input.description,
imageUrl: input.imageUrl,
owner: input.owner,
})
.returning();
// Assign selected products to this store
if (products && products.length > 0) {
await db
.update(productInfo)
.set({ storeId: newStore.id })
.where(inArray(productInfo.id, products));
}
return {
id: newStore.id,
name: newStore.name,
description: newStore.description,
imageUrl: newStore.imageUrl,
owner: newStore.owner,
createdAt: newStore.createdAt,
updatedAt: newStore.createdAt,
};
}
export interface UpdateStoreInput {
name?: string;
description?: string;
imageUrl?: string;
owner?: number;
}
export async function updateStore(
id: number,
input: UpdateStoreInput,
products?: number[]
): Promise<Store> {
const [updatedStore] = await db
.update(storeInfo)
.set({
...input,
})
.where(eq(storeInfo.id, id))
.returning();
if (!updatedStore) {
throw new Error("Store not found");
}
// Update products if provided
if (products !== undefined) {
// First, set storeId to null for products not in the list but currently assigned to this store
await db
.update(productInfo)
.set({ storeId: null })
.where(eq(productInfo.storeId, id));
// Then, assign the selected products to this store
if (products.length > 0) {
await db
.update(productInfo)
.set({ storeId: id })
.where(inArray(productInfo.id, products));
}
}
return {
id: updatedStore.id,
name: updatedStore.name,
description: updatedStore.description,
imageUrl: updatedStore.imageUrl,
owner: updatedStore.owner,
createdAt: updatedStore.createdAt,
updatedAt: updatedStore.createdAt,
};
}
export async function deleteStore(id: number): Promise<{ message: string }> {
return await db.transaction(async (tx) => {
// First, update all products of this store to set storeId to null
await tx
.update(productInfo)
.set({ storeId: null })
.where(eq(productInfo.storeId, id));
// Then delete the store
const [deletedStore] = await tx
.delete(storeInfo)
.where(eq(storeInfo.id, id))
.returning();
if (!deletedStore) {
throw new Error("Store not found");
}
return {
message: "Store deleted successfully",
};
});
}

View file

@ -0,0 +1,20 @@
import { and, eq } from 'drizzle-orm'
import { db } from '../db/db_index'
import { uploadUrlStatus } from '../db/schema'
export async function createUploadUrlStatus(key: string): Promise<void> {
await db.insert(uploadUrlStatus).values({
key,
status: 'pending',
})
}
export async function claimUploadUrlStatus(key: string): Promise<boolean> {
const result = await db
.update(uploadUrlStatus)
.set({ status: 'claimed' })
.where(and(eq(uploadUrlStatus.key, key), eq(uploadUrlStatus.status, 'pending')))
.returning()
return result.length > 0
}

View file

@ -0,0 +1,270 @@
import { db } from '../db/db_index';
import { users, userDetails, orders, orderItems, complaints, notifCreds, unloggedUserTokens, userIncidents, orderStatus } from '../db/schema';
import { eq, sql, desc, asc, count, max, inArray, like } from 'drizzle-orm';
export async function createUserByMobile(mobile: string): Promise<any> {
const [newUser] = await db
.insert(users)
.values({
name: null,
email: null,
mobile,
})
.returning();
return newUser;
}
export async function getUserByMobile(mobile: string): Promise<any | null> {
const [existingUser] = await db
.select()
.from(users)
.where(eq(users.mobile, mobile))
.limit(1);
return existingUser || null;
}
export async function getUnresolvedComplaintsCount(): Promise<number> {
const result = await db
.select({ count: count(complaints.id) })
.from(complaints)
.where(eq(complaints.isResolved, false));
return result[0]?.count || 0;
}
export async function getAllUsersWithFilters(
limit: number,
cursor?: number,
search?: string
): Promise<{ users: any[]; hasMore: boolean }> {
const whereConditions = [];
if (search && search.trim()) {
whereConditions.push(sql`${users.mobile} LIKE ${`%${search.trim()}%`}`);
}
if (cursor) {
whereConditions.push(sql`${users.id} > ${cursor}`);
}
const usersList = await db
.select({
id: users.id,
name: users.name,
mobile: users.mobile,
createdAt: users.createdAt,
})
.from(users)
.where(whereConditions.length > 0 ? sql.join(whereConditions, sql` AND `) : undefined)
.orderBy(asc(users.id))
.limit(limit + 1);
const hasMore = usersList.length > limit;
const usersToReturn = hasMore ? usersList.slice(0, limit) : usersList;
return { users: usersToReturn, hasMore };
}
export async function getOrderCountsByUserIds(userIds: number[]): Promise<{ userId: number; totalOrders: number }[]> {
if (userIds.length === 0) return [];
return await db
.select({
userId: orders.userId,
totalOrders: count(orders.id),
})
.from(orders)
.where(sql`${orders.userId} IN (${sql.join(userIds, sql`, `)})`)
.groupBy(orders.userId);
}
export async function getLastOrdersByUserIds(userIds: number[]): Promise<{ userId: number; lastOrderDate: Date | null }[]> {
if (userIds.length === 0) return [];
return await db
.select({
userId: orders.userId,
lastOrderDate: max(orders.createdAt),
})
.from(orders)
.where(sql`${orders.userId} IN (${sql.join(userIds, sql`, `)})`)
.groupBy(orders.userId);
}
export async function getSuspensionStatusesByUserIds(userIds: number[]): Promise<{ userId: number; isSuspended: boolean }[]> {
if (userIds.length === 0) return [];
return await db
.select({
userId: userDetails.userId,
isSuspended: userDetails.isSuspended,
})
.from(userDetails)
.where(sql`${userDetails.userId} IN (${sql.join(userIds, sql`, `)})`);
}
export async function getUserBasicInfo(userId: number): Promise<any | null> {
const user = await db
.select({
id: users.id,
name: users.name,
mobile: users.mobile,
createdAt: users.createdAt,
})
.from(users)
.where(eq(users.id, userId))
.limit(1);
return user[0] || null;
}
export async function getUserSuspensionStatus(userId: number): Promise<boolean> {
const userDetail = await db
.select({
isSuspended: userDetails.isSuspended,
})
.from(userDetails)
.where(eq(userDetails.userId, userId))
.limit(1);
return userDetail[0]?.isSuspended ?? false;
}
export async function getUserOrders(userId: number): Promise<any[]> {
return await db
.select({
id: orders.id,
readableId: orders.readableId,
totalAmount: orders.totalAmount,
createdAt: orders.createdAt,
isFlashDelivery: orders.isFlashDelivery,
})
.from(orders)
.where(eq(orders.userId, userId))
.orderBy(desc(orders.createdAt));
}
export async function getOrderStatusesByOrderIds(orderIds: number[]): Promise<{ orderId: number; isDelivered: boolean; isCancelled: boolean }[]> {
if (orderIds.length === 0) return [];
return await db
.select({
orderId: orderStatus.orderId,
isDelivered: orderStatus.isDelivered,
isCancelled: orderStatus.isCancelled,
})
.from(orderStatus)
.where(sql`${orderStatus.orderId} IN (${sql.join(orderIds, sql`, `)})`);
}
export async function getItemCountsByOrderIds(orderIds: number[]): Promise<{ orderId: number; itemCount: number }[]> {
if (orderIds.length === 0) return [];
return await db
.select({
orderId: orderItems.orderId,
itemCount: count(orderItems.id),
})
.from(orderItems)
.where(sql`${orderItems.orderId} IN (${sql.join(orderIds, sql`, `)})`)
.groupBy(orderItems.orderId);
}
export async function upsertUserSuspension(userId: number, isSuspended: boolean): Promise<void> {
const existingDetail = await db
.select({ id: userDetails.id })
.from(userDetails)
.where(eq(userDetails.userId, userId))
.limit(1);
if (existingDetail.length > 0) {
await db
.update(userDetails)
.set({ isSuspended })
.where(eq(userDetails.userId, userId));
} else {
await db
.insert(userDetails)
.values({
userId,
isSuspended,
});
}
}
export async function searchUsers(search?: string): Promise<any[]> {
if (search && search.trim()) {
return await db
.select({
id: users.id,
name: users.name,
mobile: users.mobile,
})
.from(users)
.where(sql`${users.mobile} LIKE ${`%${search.trim()}%`} OR ${users.name} LIKE ${`%${search.trim()}%`}`);
} else {
return await db
.select({
id: users.id,
name: users.name,
mobile: users.mobile,
})
.from(users);
}
}
export async function getAllNotifCreds(): Promise<{ userId: number }[]> {
return await db
.select({ userId: notifCreds.userId })
.from(notifCreds);
}
export async function getAllUnloggedTokens(): Promise<{ token: string }[]> {
return await db
.select({ token: unloggedUserTokens.token })
.from(unloggedUserTokens);
}
export async function getNotifTokensByUserIds(userIds: number[]): Promise<{ token: string }[]> {
return await db
.select({ token: notifCreds.token })
.from(notifCreds)
.where(inArray(notifCreds.userId, userIds));
}
export async function getUserIncidentsWithRelations(userId: number): Promise<any[]> {
return await db.query.userIncidents.findMany({
where: eq(userIncidents.userId, userId),
with: {
order: {
with: {
orderStatus: true,
},
},
addedBy: true,
},
orderBy: desc(userIncidents.dateAdded),
});
}
export async function createUserIncident(
userId: number,
orderId: number | undefined,
adminComment: string | undefined,
adminUserId: number,
negativityScore: number | undefined
): Promise<any> {
const [incident] = await db.insert(userIncidents)
.values({
userId,
orderId,
adminComment,
addedBy: adminUserId,
negativityScore,
})
.returning();
return incident;
}

View file

@ -0,0 +1,130 @@
import { db } from '../db/db_index';
import { vendorSnippets, deliverySlotInfo, productInfo, orders, orderItems, orderStatus } from '../db/schema';
import { eq, and, inArray, gt, sql, asc, desc } from 'drizzle-orm';
export async function checkVendorSnippetExists(snippetCode: string): Promise<boolean> {
const existingSnippet = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.snippetCode, snippetCode),
});
return !!existingSnippet;
}
export async function getVendorSnippetById(id: number): Promise<any | null> {
return await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.id, id),
with: {
slot: true,
},
});
}
export async function getVendorSnippetByCode(snippetCode: string): Promise<any | null> {
return await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.snippetCode, snippetCode),
});
}
export async function getAllVendorSnippets(): Promise<any[]> {
return await db.query.vendorSnippets.findMany({
with: {
slot: true,
},
orderBy: desc(vendorSnippets.createdAt),
});
}
export interface CreateVendorSnippetInput {
snippetCode: string;
slotId?: number;
productIds: number[];
isPermanent: boolean;
validTill?: Date;
}
export async function createVendorSnippet(input: CreateVendorSnippetInput): Promise<any> {
const [result] = await db.insert(vendorSnippets).values({
snippetCode: input.snippetCode,
slotId: input.slotId,
productIds: input.productIds,
isPermanent: input.isPermanent,
validTill: input.validTill,
}).returning();
return result;
}
export async function updateVendorSnippet(id: number, updates: any): Promise<any> {
const [result] = await db.update(vendorSnippets)
.set(updates)
.where(eq(vendorSnippets.id, id))
.returning();
return result;
}
export async function deleteVendorSnippet(id: number): Promise<void> {
await db.delete(vendorSnippets)
.where(eq(vendorSnippets.id, id));
}
export async function getProductsByIds(productIds: number[]): Promise<any[]> {
return await db.query.productInfo.findMany({
where: inArray(productInfo.id, productIds),
columns: { id: true, name: true },
});
}
export async function getVendorSlotById(slotId: number): Promise<any | null> {
return await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, slotId),
});
}
export async function getVendorOrdersBySlotId(slotId: number): Promise<any[]> {
return await db.query.orders.findMany({
where: eq(orders.slotId, slotId),
with: {
orderItems: {
with: {
product: {
with: {
unit: true,
},
},
},
},
orderStatus: true,
user: true,
slot: true,
},
orderBy: desc(orders.createdAt),
});
}
export async function getOrderItemsByOrderIds(orderIds: number[]): Promise<any[]> {
return await db.query.orderItems.findMany({
where: inArray(orderItems.orderId, orderIds),
with: {
product: {
with: {
unit: true,
},
},
},
});
}
export async function getOrderStatusByOrderIds(orderIds: number[]): Promise<any[]> {
return await db.query.orderStatus.findMany({
where: inArray(orderStatus.orderId, orderIds),
});
}
export async function updateVendorOrderItemPackaging(orderItemId: number, isPackaged: boolean, isPackageVerified: boolean): Promise<void> {
await db.update(orderItems)
.set({
is_packaged: isPackaged,
is_package_verified: isPackageVerified,
})
.where(eq(orderItems.id, orderItemId));
}

View file

@ -0,0 +1,41 @@
import { db } from '../db/db_index'
import { productInfo, keyValStore } from '../db/schema'
import { inArray, eq } from 'drizzle-orm'
/**
* Toggle flash delivery availability for specific products
* @param isAvailable - Whether flash delivery should be available
* @param productIds - Array of product IDs to update
*/
export async function toggleFlashDeliveryForItems(
isAvailable: boolean,
productIds: number[]
): Promise<void> {
await db
.update(productInfo)
.set({ isFlashAvailable: isAvailable })
.where(inArray(productInfo.id, productIds))
}
/**
* Update key-value store
* @param key - The key to update
* @param value - The boolean value to set
*/
export async function toggleKeyVal(
key: string,
value: boolean
): Promise<void> {
await db
.update(keyValStore)
.set({ value })
.where(eq(keyValStore.key, key))
}
/**
* Get all key-value store constants
* @returns Array of all key-value pairs
*/
export async function getAllKeyValStore(): Promise<Array<{ key: string; value: any }>> {
return db.select().from(keyValStore)
}

View file

@ -0,0 +1,49 @@
export const CONST_KEYS = {
minRegularOrderValue: 'minRegularOrderValue',
freeDeliveryThreshold: 'freeDeliveryThreshold',
deliveryCharge: 'deliveryCharge',
flashFreeDeliveryThreshold: 'flashFreeDeliveryThreshold',
flashDeliveryCharge: 'flashDeliveryCharge',
platformFeePercent: 'platformFeePercent',
taxRate: 'taxRate',
tester: 'tester',
minOrderAmountForCoupon: 'minOrderAmountForCoupon',
maxCouponDiscount: 'maxCouponDiscount',
flashDeliverySlotId: 'flashDeliverySlotId',
readableOrderId: 'readableOrderId',
versionNum: 'versionNum',
playStoreUrl: 'playStoreUrl',
appStoreUrl: 'appStoreUrl',
popularItems: 'popularItems',
allItemsOrder: 'allItemsOrder',
isFlashDeliveryEnabled: 'isFlashDeliveryEnabled',
supportMobile: 'supportMobile',
supportEmail: 'supportEmail',
} as const
export const CONST_LABELS: Record<ConstKey, string> = {
minRegularOrderValue: 'Minimum Regular Order Value',
freeDeliveryThreshold: 'Free Delivery Threshold',
deliveryCharge: 'Delivery Charge',
flashFreeDeliveryThreshold: 'Flash Free Delivery Threshold',
flashDeliveryCharge: 'Flash Delivery Charge',
platformFeePercent: 'Platform Fee Percent',
taxRate: 'Tax Rate',
tester: 'Tester',
minOrderAmountForCoupon: 'Minimum Order Amount for Coupon',
maxCouponDiscount: 'Maximum Coupon Discount',
flashDeliverySlotId: 'Flash Delivery Slot ID',
readableOrderId: 'Readable Order ID',
versionNum: 'Version Number',
playStoreUrl: 'Play Store URL',
appStoreUrl: 'App Store URL',
popularItems: 'Popular Items',
allItemsOrder: 'All Items Order',
isFlashDeliveryEnabled: 'Enable Flash Delivery',
supportMobile: 'Support Mobile',
supportEmail: 'Support Email',
}
export type ConstKey = (typeof CONST_KEYS)[keyof typeof CONST_KEYS]
export const CONST_KEYS_ARRAY = Object.values(CONST_KEYS) as ConstKey[]

View file

@ -0,0 +1,38 @@
import { db } from '../db/db_index'
import { orders, orderItems, orderStatus, payments, refunds, couponUsage, complaints } from '../db/schema'
import { inArray } from 'drizzle-orm'
/**
* Delete orders and all their related records
* @param orderIds Array of order IDs to delete
* @returns Promise<void>
* @throws Error if deletion fails
*/
export async function deleteOrdersWithRelations(orderIds: number[]): Promise<void> {
if (orderIds.length === 0) {
return
}
// Delete child records first (in correct order to avoid FK constraint errors)
// 1. Delete coupon usage records
await db.delete(couponUsage).where(inArray(couponUsage.orderId, orderIds))
// 2. Delete complaints related to these orders
await db.delete(complaints).where(inArray(complaints.orderId, orderIds))
// 3. Delete refunds
await db.delete(refunds).where(inArray(refunds.orderId, orderIds))
// 4. Delete payments
await db.delete(payments).where(inArray(payments.orderId, orderIds))
// 5. Delete order status records
await db.delete(orderStatus).where(inArray(orderStatus.orderId, orderIds))
// 6. Delete order items
await db.delete(orderItems).where(inArray(orderItems.orderId, orderIds))
// 7. Finally delete the orders themselves
await db.delete(orders).where(inArray(orders.id, orderIds))
}

View file

@ -0,0 +1,55 @@
export const appUrl = process.env.APP_URL as string
export const jwtSecret: string = process.env.JWT_SECRET as string
export const defaultRoleName = 'gen_user'
export const encodedJwtSecret = new TextEncoder().encode(jwtSecret)
export const s3AccessKeyId = process.env.S3_ACCESS_KEY_ID as string
export const s3SecretAccessKey = process.env.S3_SECRET_ACCESS_KEY as string
export const s3BucketName = process.env.S3_BUCKET_NAME as string
export const s3Region = process.env.S3_REGION as string
export const assetsDomain = process.env.ASSETS_DOMAIN as string
export const apiCacheKey = process.env.API_CACHE_KEY as string
export const cloudflareApiToken = process.env.CLOUDFLARE_API_TOKEN as string
export const cloudflareZoneId = process.env.CLOUDFLARE_ZONE_ID as string
export const s3Url = process.env.S3_URL as string
export const redisUrl = process.env.REDIS_URL as string
export const expoAccessToken = process.env.EXPO_ACCESS_TOKEN as string
export const phonePeBaseUrl = process.env.PHONE_PE_BASE_URL as string
export const phonePeClientId = process.env.PHONE_PE_CLIENT_ID as string
export const phonePeClientVersion = Number(process.env.PHONE_PE_CLIENT_VERSION as string)
export const phonePeClientSecret = process.env.PHONE_PE_CLIENT_SECRET as string
export const phonePeMerchantId = process.env.PHONE_PE_MERCHANT_ID as string
export const razorpayId = process.env.RAZORPAY_KEY as string
export const razorpaySecret = process.env.RAZORPAY_SECRET as string
export const otpSenderAuthToken = process.env.OTP_SENDER_AUTH_TOKEN as string
export const minOrderValue = Number(process.env.MIN_ORDER_VALUE as string)
export const deliveryCharge = Number(process.env.DELIVERY_CHARGE as string)
export const telegramBotToken = process.env.TELEGRAM_BOT_TOKEN as string
export const telegramChatIds = (process.env.TELEGRAM_CHAT_IDS as string)?.split(',').map(id => id.trim()) || []
export const isDevMode = (process.env.ENV_MODE as string) === 'dev'

View file

@ -0,0 +1,18 @@
import { db } from '../db/db_index'
import { keyValStore, productInfo } from '../db/schema'
/**
* Health check - test database connectivity
* Tries to select from keyValStore first, falls back to productInfo
*/
export async function healthCheck(): Promise<{ status: string }> {
try {
// Try keyValStore first (smaller table)
await db.select({ key: keyValStore.key }).from(keyValStore).limit(1)
return { status: 'ok' }
} catch {
// Fallback to productInfo
await db.select({ name: productInfo.name }).from(productInfo).limit(1)
return { status: 'ok' }
}
}

View file

@ -0,0 +1,127 @@
import { db } from '../db/db_index'
import { eq, and } from 'drizzle-orm'
// ============================================================================
// Unit Seed Helper
// ============================================================================
export interface UnitSeedData {
shortNotation: string
fullName: string
}
export async function seedUnits(unitsToSeed: UnitSeedData[]): Promise<void> {
for (const unit of unitsToSeed) {
const { units: unitsTable } = await import('../db/schema')
const existingUnit = await db.query.units.findFirst({
where: eq(unitsTable.shortNotation, unit.shortNotation),
})
if (!existingUnit) {
await db.insert(unitsTable).values(unit)
}
}
}
// ============================================================================
// Staff Role Seed Helper
// ============================================================================
// Type for staff role names based on the enum values in schema
export type StaffRoleName = 'super_admin' | 'admin' | 'marketer' | 'delivery_staff'
export async function seedStaffRoles(rolesToSeed: StaffRoleName[]): Promise<void> {
for (const roleName of rolesToSeed) {
const { staffRoles } = await import('../db/schema')
const existingRole = await db.query.staffRoles.findFirst({
where: eq(staffRoles.roleName, roleName),
})
if (!existingRole) {
await db.insert(staffRoles).values({ roleName })
}
}
}
// ============================================================================
// Staff Permission Seed Helper
// ============================================================================
// Type for staff permission names based on the enum values in schema
export type StaffPermissionName = 'crud_product' | 'make_coupon' | 'crud_staff_users'
export async function seedStaffPermissions(permissionsToSeed: StaffPermissionName[]): Promise<void> {
for (const permissionName of permissionsToSeed) {
const { staffPermissions } = await import('../db/schema')
const existingPermission = await db.query.staffPermissions.findFirst({
where: eq(staffPermissions.permissionName, permissionName),
})
if (!existingPermission) {
await db.insert(staffPermissions).values({ permissionName })
}
}
}
// ============================================================================
// Role-Permission Assignment Helper
// ============================================================================
export interface RolePermissionAssignment {
roleName: StaffRoleName
permissionName: StaffPermissionName
}
export async function seedRolePermissions(assignments: RolePermissionAssignment[]): Promise<void> {
await db.transaction(async (tx) => {
const { staffRoles, staffPermissions, staffRolePermissions } = await import('../db/schema')
for (const assignment of assignments) {
// Get role ID
const role = await tx.query.staffRoles.findFirst({
where: eq(staffRoles.roleName, assignment.roleName),
})
// Get permission ID
const permission = await tx.query.staffPermissions.findFirst({
where: eq(staffPermissions.permissionName, assignment.permissionName),
})
if (role && permission) {
const existing = await tx.query.staffRolePermissions.findFirst({
where: and(
eq(staffRolePermissions.staffRoleId, role.id),
eq(staffRolePermissions.staffPermissionId, permission.id)
),
})
if (!existing) {
await tx.insert(staffRolePermissions).values({
staffRoleId: role.id,
staffPermissionId: permission.id,
})
}
}
}
})
}
// ============================================================================
// Key-Value Store Seed Helper
// ============================================================================
export interface KeyValSeedData {
key: string
value: any
}
export async function seedKeyValStore(constantsToSeed: KeyValSeedData[]): Promise<void> {
for (const constant of constantsToSeed) {
const { keyValStore } = await import('../db/schema')
const existing = await db.query.keyValStore.findFirst({
where: eq(keyValStore.key, constant.key),
})
if (!existing) {
await db.insert(keyValStore).values({
key: constant.key,
value: constant.value,
})
}
}
}

View file

@ -0,0 +1,294 @@
// Store Helpers - Database operations for cache initialization
// These are used by stores in apps/backend/src/stores/
import { db } from '../db/db_index'
import {
homeBanners,
productInfo,
units,
productSlots,
deliverySlotInfo,
specialDeals,
storeInfo,
productTags,
productTagInfo,
userIncidents,
} from '../db/schema'
import { eq, and, gt, sql, isNotNull, asc } from 'drizzle-orm'
// ============================================================================
// BANNER STORE HELPERS
// ============================================================================
export interface BannerData {
id: number
name: string
imageUrl: string | null
serialNum: number | null
productIds: number[] | null
createdAt: Date
}
export async function getAllBannersForCache(): Promise<BannerData[]> {
return db.query.homeBanners.findMany({
where: isNotNull(homeBanners.serialNum),
orderBy: asc(homeBanners.serialNum),
})
}
// ============================================================================
// PRODUCT STORE HELPERS
// ============================================================================
export interface ProductBasicData {
id: number
name: string
shortDescription: string | null
longDescription: string | null
price: string
marketPrice: string | null
images: unknown
isOutOfStock: boolean
storeId: number | null
unitShortNotation: string
incrementStep: number
productQuantity: number
isFlashAvailable: boolean
flashPrice: string | null
}
export interface StoreBasicData {
id: number
name: string
description: string | null
}
export interface DeliverySlotData {
productId: number
id: number
deliveryTime: Date
freezeTime: Date
isCapacityFull: boolean
}
export interface SpecialDealData {
productId: number
quantity: string
price: string
validTill: Date
}
export interface ProductTagData {
productId: number
tagName: string
}
export async function getAllProductsForCache(): Promise<ProductBasicData[]> {
const results = await db
.select({
id: productInfo.id,
name: productInfo.name,
shortDescription: productInfo.shortDescription,
longDescription: productInfo.longDescription,
price: productInfo.price,
marketPrice: productInfo.marketPrice,
images: productInfo.images,
isOutOfStock: productInfo.isOutOfStock,
storeId: productInfo.storeId,
unitShortNotation: units.shortNotation,
incrementStep: productInfo.incrementStep,
productQuantity: productInfo.productQuantity,
isFlashAvailable: productInfo.isFlashAvailable,
flashPrice: productInfo.flashPrice,
})
.from(productInfo)
.innerJoin(units, eq(productInfo.unitId, units.id))
return results.map((product) => ({
...product,
price: String(product.price ?? '0'),
marketPrice: product.marketPrice ? String(product.marketPrice) : null,
flashPrice: product.flashPrice ? String(product.flashPrice) : null,
}))
}
export async function getAllStoresForCache(): Promise<StoreBasicData[]> {
return db.query.storeInfo.findMany({
columns: { id: true, name: true, description: true },
})
}
export async function getAllDeliverySlotsForCache(): Promise<DeliverySlotData[]> {
return db
.select({
productId: productSlots.productId,
id: deliverySlotInfo.id,
deliveryTime: deliverySlotInfo.deliveryTime,
freezeTime: deliverySlotInfo.freezeTime,
isCapacityFull: deliverySlotInfo.isCapacityFull,
})
.from(productSlots)
.innerJoin(deliverySlotInfo, eq(productSlots.slotId, deliverySlotInfo.id))
.where(
and(
eq(deliverySlotInfo.isActive, true),
eq(deliverySlotInfo.isCapacityFull, false),
gt(deliverySlotInfo.deliveryTime, sql`CURRENT_TIMESTAMP`)
)
)
}
export async function getAllSpecialDealsForCache(): Promise<SpecialDealData[]> {
const results = await db
.select({
productId: specialDeals.productId,
quantity: specialDeals.quantity,
price: specialDeals.price,
validTill: specialDeals.validTill,
})
.from(specialDeals)
.where(gt(specialDeals.validTill, sql`CURRENT_TIMESTAMP`))
return results.map((deal) => ({
...deal,
quantity: String(deal.quantity ?? '0'),
price: String(deal.price ?? '0'),
}))
}
export async function getAllProductTagsForCache(): Promise<ProductTagData[]> {
return db
.select({
productId: productTags.productId,
tagName: productTagInfo.tagName,
})
.from(productTags)
.innerJoin(productTagInfo, eq(productTags.tagId, productTagInfo.id))
}
// ============================================================================
// PRODUCT TAG STORE HELPERS
// ============================================================================
export interface TagBasicData {
id: number
tagName: string
tagDescription: string | null
imageUrl: string | null
isDashboardTag: boolean
relatedStores: unknown
}
export interface TagProductMapping {
tagId: number
productId: number
}
export async function getAllTagsForCache(): Promise<TagBasicData[]> {
return db
.select({
id: productTagInfo.id,
tagName: productTagInfo.tagName,
tagDescription: productTagInfo.tagDescription,
imageUrl: productTagInfo.imageUrl,
isDashboardTag: productTagInfo.isDashboardTag,
relatedStores: productTagInfo.relatedStores,
})
.from(productTagInfo)
}
export async function getAllTagProductMappings(): Promise<TagProductMapping[]> {
return db
.select({
tagId: productTags.tagId,
productId: productTags.productId,
})
.from(productTags)
}
// ============================================================================
// SLOT STORE HELPERS
// ============================================================================
export interface SlotWithProductsData {
id: number
deliveryTime: Date
freezeTime: Date
isActive: boolean
isCapacityFull: boolean
productSlots: Array<{
product: {
id: number
name: string
productQuantity: number
shortDescription: string | null
price: string
marketPrice: string | null
unit: { shortNotation: string } | null
store: { id: number; name: string; description: string | null } | null
images: unknown
isOutOfStock: boolean
storeId: number | null
}
}>
}
export async function getAllSlotsWithProductsForCache(): Promise<SlotWithProductsData[]> {
const now = new Date()
return db.query.deliverySlotInfo.findMany({
where: and(
eq(deliverySlotInfo.isActive, true),
gt(deliverySlotInfo.deliveryTime, now)
),
with: {
productSlots: {
with: {
product: {
with: {
unit: true,
store: true,
},
},
},
},
},
orderBy: asc(deliverySlotInfo.deliveryTime),
}) as Promise<SlotWithProductsData[]>
}
// ============================================================================
// USER NEGATIVITY STORE HELPERS
// ============================================================================
export interface UserNegativityData {
userId: number
totalNegativityScore: number
}
export async function getAllUserNegativityScores(): Promise<UserNegativityData[]> {
const results = await db
.select({
userId: userIncidents.userId,
totalNegativityScore: sql`sum(${userIncidents.negativityScore})`,
})
.from(userIncidents)
.groupBy(userIncidents.userId)
return results.map((result) => ({
userId: result.userId,
totalNegativityScore: Number(result.totalNegativityScore ?? 0),
}))
}
export async function getUserNegativityScore(userId: number): Promise<number> {
const [result] = await db
.select({
totalNegativityScore: sql`sum(${userIncidents.negativityScore})`,
})
.from(userIncidents)
.where(eq(userIncidents.userId, userId))
.limit(1)
return Number(result?.totalNegativityScore ?? 0)
}

View file

@ -0,0 +1,148 @@
import { db } from '../db/db_index'
import { addresses, deliverySlotInfo, orders, orderStatus } from '../db/schema'
import { and, eq, gte } from 'drizzle-orm'
import type { InferSelectModel } from 'drizzle-orm'
import type { UserAddress } from '@packages/shared'
type AddressRow = InferSelectModel<typeof addresses>
const mapUserAddress = (address: AddressRow): UserAddress => ({
id: address.id,
userId: address.userId,
name: address.name,
phone: address.phone,
addressLine1: address.addressLine1,
addressLine2: address.addressLine2 ?? null,
city: address.city,
state: address.state,
pincode: address.pincode,
isDefault: address.isDefault,
latitude: address.latitude ?? null,
longitude: address.longitude ?? null,
googleMapsUrl: address.googleMapsUrl ?? null,
adminLatitude: address.adminLatitude ?? null,
adminLongitude: address.adminLongitude ?? null,
zoneId: address.zoneId ?? null,
createdAt: address.createdAt,
})
export async function getDefaultAddress(userId: number): Promise<UserAddress | null> {
const [defaultAddress] = await db
.select()
.from(addresses)
.where(and(eq(addresses.userId, userId), eq(addresses.isDefault, true)))
.limit(1)
return defaultAddress ? mapUserAddress(defaultAddress) : null
}
export async function getUserAddresses(userId: number): Promise<UserAddress[]> {
const userAddresses = await db.select().from(addresses).where(eq(addresses.userId, userId))
return userAddresses.map(mapUserAddress)
}
export async function getUserAddressById(userId: number, addressId: number): Promise<UserAddress | null> {
const [address] = await db
.select()
.from(addresses)
.where(and(eq(addresses.id, addressId), eq(addresses.userId, userId)))
.limit(1)
return address ? mapUserAddress(address) : null
}
export async function clearDefaultAddress(userId: number): Promise<void> {
await db.update(addresses).set({ isDefault: false }).where(eq(addresses.userId, userId))
}
export async function createUserAddress(input: {
userId: number
name: string
phone: string
addressLine1: string
addressLine2?: string
city: string
state: string
pincode: string
isDefault: boolean
latitude?: number
longitude?: number
googleMapsUrl?: string
}): Promise<UserAddress> {
const [newAddress] = await db.insert(addresses).values({
userId: input.userId,
name: input.name,
phone: input.phone,
addressLine1: input.addressLine1,
addressLine2: input.addressLine2,
city: input.city,
state: input.state,
pincode: input.pincode,
isDefault: input.isDefault,
latitude: input.latitude,
longitude: input.longitude,
googleMapsUrl: input.googleMapsUrl,
}).returning()
return mapUserAddress(newAddress)
}
export async function updateUserAddress(input: {
userId: number
addressId: number
name: string
phone: string
addressLine1: string
addressLine2?: string
city: string
state: string
pincode: string
isDefault: boolean
latitude?: number
longitude?: number
googleMapsUrl?: string
}): Promise<UserAddress | null> {
const [updatedAddress] = await db.update(addresses)
.set({
name: input.name,
phone: input.phone,
addressLine1: input.addressLine1,
addressLine2: input.addressLine2,
city: input.city,
state: input.state,
pincode: input.pincode,
isDefault: input.isDefault,
googleMapsUrl: input.googleMapsUrl,
latitude: input.latitude,
longitude: input.longitude,
})
.where(and(eq(addresses.id, input.addressId), eq(addresses.userId, input.userId)))
.returning()
return updatedAddress ? mapUserAddress(updatedAddress) : null
}
export async function deleteUserAddress(userId: number, addressId: number): Promise<boolean> {
const [deleted] = await db.delete(addresses)
.where(and(eq(addresses.id, addressId), eq(addresses.userId, userId)))
.returning({ id: addresses.id })
return !!deleted
}
export async function hasOngoingOrdersForAddress(addressId: number): Promise<boolean> {
const ongoingOrders = await db.select({
orderId: orders.id,
})
.from(orders)
.innerJoin(orderStatus, eq(orders.id, orderStatus.orderId))
.innerJoin(deliverySlotInfo, eq(orders.slotId, deliverySlotInfo.id))
.where(and(
eq(orders.addressId, addressId),
eq(orderStatus.isCancelled, false),
gte(deliverySlotInfo.deliveryTime, new Date())
))
.limit(1)
return ongoingOrders.length > 0
}

View file

@ -0,0 +1,229 @@
import { db } from '../db/db_index'
import {
users,
userCreds,
userDetails,
addresses,
cartItems,
complaints,
couponApplicableUsers,
couponUsage,
notifCreds,
notifications,
orderItems,
orderStatus,
orders,
payments,
refunds,
productReviews,
reservedCoupons,
} from '../db/schema'
import { eq } from 'drizzle-orm'
export async function getUserByEmail(email: string) {
const [user] = await db.select().from(users).where(eq(users.email, email)).limit(1)
return user || null
}
export async function getUserByMobile(mobile: string) {
const [user] = await db.select().from(users).where(eq(users.mobile, mobile)).limit(1)
return user || null
}
export async function getUserById(userId: number) {
const [user] = await db.select().from(users).where(eq(users.id, userId)).limit(1)
return user || null
}
export async function getUserCreds(userId: number) {
const [creds] = await db.select().from(userCreds).where(eq(userCreds.userId, userId)).limit(1)
return creds || null
}
export async function getUserDetails(userId: number) {
const [details] = await db.select().from(userDetails).where(eq(userDetails.userId, userId)).limit(1)
return details || null
}
export async function isUserSuspended(userId: number): Promise<boolean> {
const details = await getUserDetails(userId)
return details?.isSuspended ?? false
}
export async function createUserWithProfile(input: {
name: string
email: string
mobile: string
hashedPassword: string
profileImage?: string | null
}) {
return db.transaction(async (tx) => {
// Create user
const [user] = await tx.insert(users).values({
name: input.name,
email: input.email,
mobile: input.mobile,
}).returning()
// Create user credentials
await tx.insert(userCreds).values({
userId: user.id,
userPassword: input.hashedPassword,
})
// Create user details with profile image
await tx.insert(userDetails).values({
userId: user.id,
profileImage: input.profileImage || null,
})
return user
})
}
export async function getUserDetailsByUserId(userId: number) {
const [details] = await db.select().from(userDetails).where(eq(userDetails.userId, userId)).limit(1)
return details || null
}
export async function updateUserProfile(userId: number, data: {
name?: string
email?: string
mobile?: string
hashedPassword?: string
profileImage?: string
bio?: string
dateOfBirth?: Date | null
gender?: string
occupation?: string
}) {
return db.transaction(async (tx) => {
// Update user table
const userUpdate: any = {}
if (data.name !== undefined) userUpdate.name = data.name
if (data.email !== undefined) userUpdate.email = data.email
if (data.mobile !== undefined) userUpdate.mobile = data.mobile
if (Object.keys(userUpdate).length > 0) {
await tx.update(users).set(userUpdate).where(eq(users.id, userId))
}
// Update password if provided
if (data.hashedPassword) {
await tx.update(userCreds).set({
userPassword: data.hashedPassword,
}).where(eq(userCreds.userId, userId))
}
// Update or insert user details
const detailsUpdate: any = {}
if (data.bio !== undefined) detailsUpdate.bio = data.bio
if (data.dateOfBirth !== undefined) detailsUpdate.dateOfBirth = data.dateOfBirth
if (data.gender !== undefined) detailsUpdate.gender = data.gender
if (data.occupation !== undefined) detailsUpdate.occupation = data.occupation
if (data.profileImage !== undefined) detailsUpdate.profileImage = data.profileImage
detailsUpdate.updatedAt = new Date()
const [existingDetails] = await tx.select().from(userDetails).where(eq(userDetails.userId, userId)).limit(1)
if (existingDetails) {
await tx.update(userDetails).set(detailsUpdate).where(eq(userDetails.userId, userId))
} else {
await tx.insert(userDetails).values({
userId,
...detailsUpdate,
createdAt: new Date(),
})
}
// Return updated user
const [user] = await tx.select().from(users).where(eq(users.id, userId)).limit(1)
return user
})
}
export async function createUserWithCreds(input: {
name: string
email: string
mobile: string
hashedPassword: string
}) {
return db.transaction(async (tx) => {
const [user] = await tx.insert(users).values({
name: input.name,
email: input.email,
mobile: input.mobile,
}).returning()
await tx.insert(userCreds).values({
userId: user.id,
userPassword: input.hashedPassword,
})
return user
})
}
export async function createUserWithMobile(mobile: string) {
const [user] = await db.insert(users).values({
name: null,
email: null,
mobile,
}).returning()
return user
}
export async function upsertUserPassword(userId: number, hashedPassword: string) {
try {
await db.insert(userCreds).values({
userId,
userPassword: hashedPassword,
})
return
} catch (error: any) {
if (error.code === '23505') {
await db.update(userCreds).set({
userPassword: hashedPassword,
}).where(eq(userCreds.userId, userId))
return
}
throw error
}
}
export async function deleteUserAccount(userId: number) {
await db.transaction(async (tx) => {
await tx.delete(notifCreds).where(eq(notifCreds.userId, userId))
await tx.delete(couponApplicableUsers).where(eq(couponApplicableUsers.userId, userId))
await tx.delete(couponUsage).where(eq(couponUsage.userId, userId))
await tx.delete(complaints).where(eq(complaints.userId, userId))
await tx.delete(cartItems).where(eq(cartItems.userId, userId))
await tx.delete(notifications).where(eq(notifications.userId, userId))
await tx.delete(productReviews).where(eq(productReviews.userId, userId))
await tx.update(reservedCoupons)
.set({ redeemedBy: null })
.where(eq(reservedCoupons.redeemedBy, userId))
const userOrders = await tx
.select({ id: orders.id })
.from(orders)
.where(eq(orders.userId, userId))
for (const order of userOrders) {
await tx.delete(orderItems).where(eq(orderItems.orderId, order.id))
await tx.delete(orderStatus).where(eq(orderStatus.orderId, order.id))
await tx.delete(payments).where(eq(payments.orderId, order.id))
await tx.delete(refunds).where(eq(refunds.orderId, order.id))
await tx.delete(couponUsage).where(eq(couponUsage.orderId, order.id))
await tx.delete(complaints).where(eq(complaints.orderId, order.id))
}
await tx.delete(orders).where(eq(orders.userId, userId))
await tx.delete(addresses).where(eq(addresses.userId, userId))
await tx.delete(userDetails).where(eq(userDetails.userId, userId))
await tx.delete(userCreds).where(eq(userCreds.userId, userId))
await tx.delete(users).where(eq(users.id, userId))
})
}

View file

@ -0,0 +1,29 @@
import { db } from '../db/db_index'
import { homeBanners } from '../db/schema'
import { asc, isNotNull } from 'drizzle-orm'
import type { InferSelectModel } from 'drizzle-orm'
import type { UserBanner } from '@packages/shared'
type BannerRow = InferSelectModel<typeof homeBanners>
const mapBanner = (banner: BannerRow): UserBanner => ({
id: banner.id,
name: banner.name,
imageUrl: banner.imageUrl,
description: banner.description ?? null,
productIds: banner.productIds ?? null,
redirectUrl: banner.redirectUrl ?? null,
serialNum: banner.serialNum ?? null,
isActive: banner.isActive,
createdAt: banner.createdAt,
lastUpdated: banner.lastUpdated,
})
export async function getActiveBanners(): Promise<UserBanner[]> {
const banners = await db.query.homeBanners.findMany({
where: isNotNull(homeBanners.serialNum),
orderBy: asc(homeBanners.serialNum),
})
return banners.map(mapBanner)
}

View file

@ -0,0 +1,99 @@
import { db } from '../db/db_index'
import { cartItems, productInfo, units } from '../db/schema'
import { and, eq, sql } from 'drizzle-orm'
import type { UserCartItem } from '@packages/shared'
const getStringArray = (value: unknown): string[] => {
if (!Array.isArray(value)) return []
return value.map((item) => String(item))
}
export async function getCartItemsWithProducts(userId: number): Promise<UserCartItem[]> {
const cartItemsWithProducts = await db
.select({
cartId: cartItems.id,
productId: productInfo.id,
productName: productInfo.name,
productPrice: productInfo.price,
productImages: productInfo.images,
productQuantity: productInfo.productQuantity,
isOutOfStock: productInfo.isOutOfStock,
unitShortNotation: units.shortNotation,
quantity: cartItems.quantity,
addedAt: cartItems.addedAt,
})
.from(cartItems)
.innerJoin(productInfo, eq(cartItems.productId, productInfo.id))
.innerJoin(units, eq(productInfo.unitId, units.id))
.where(eq(cartItems.userId, userId))
return cartItemsWithProducts.map((item) => {
const priceValue = item.productPrice ?? '0'
const quantityValue = item.quantity ?? '0'
return {
id: item.cartId,
productId: item.productId,
quantity: parseFloat(quantityValue),
addedAt: item.addedAt,
product: {
id: item.productId,
name: item.productName,
price: priceValue.toString(),
productQuantity: item.productQuantity,
unit: item.unitShortNotation,
isOutOfStock: item.isOutOfStock,
images: getStringArray(item.productImages),
},
subtotal: parseFloat(priceValue.toString()) * parseFloat(quantityValue),
}
})
}
export async function getProductById(productId: number) {
return db.query.productInfo.findFirst({
where: eq(productInfo.id, productId),
})
}
export async function getCartItemByUserProduct(userId: number, productId: number) {
return db.query.cartItems.findFirst({
where: and(eq(cartItems.userId, userId), eq(cartItems.productId, productId)),
})
}
export async function incrementCartItemQuantity(itemId: number, quantity: number): Promise<void> {
await db.update(cartItems)
.set({
quantity: sql`${cartItems.quantity} + ${quantity}`,
})
.where(eq(cartItems.id, itemId))
}
export async function insertCartItem(userId: number, productId: number, quantity: number): Promise<void> {
await db.insert(cartItems).values({
userId,
productId,
quantity: quantity.toString(),
})
}
export async function updateCartItemQuantity(userId: number, itemId: number, quantity: number) {
const [updatedItem] = await db.update(cartItems)
.set({ quantity: quantity.toString() })
.where(and(eq(cartItems.id, itemId), eq(cartItems.userId, userId)))
.returning({ id: cartItems.id })
return !!updatedItem
}
export async function deleteCartItem(userId: number, itemId: number): Promise<boolean> {
const [deletedItem] = await db.delete(cartItems)
.where(and(eq(cartItems.id, itemId), eq(cartItems.userId, userId)))
.returning({ id: cartItems.id })
return !!deletedItem
}
export async function clearUserCart(userId: number): Promise<void> {
await db.delete(cartItems).where(eq(cartItems.userId, userId))
}

View file

@ -0,0 +1,45 @@
import { db } from '../db/db_index'
import { complaints } from '../db/schema'
import { asc, eq } from 'drizzle-orm'
import type { InferSelectModel } from 'drizzle-orm'
import type { UserComplaint } from '@packages/shared'
type ComplaintRow = InferSelectModel<typeof complaints>
export async function getUserComplaints(userId: number): Promise<UserComplaint[]> {
const userComplaints = await db
.select({
id: complaints.id,
complaintBody: complaints.complaintBody,
response: complaints.response,
isResolved: complaints.isResolved,
createdAt: complaints.createdAt,
orderId: complaints.orderId,
})
.from(complaints)
.where(eq(complaints.userId, userId))
.orderBy(asc(complaints.createdAt))
return userComplaints.map((complaint) => ({
id: complaint.id,
complaintBody: complaint.complaintBody,
response: complaint.response ?? null,
isResolved: complaint.isResolved,
createdAt: complaint.createdAt,
orderId: complaint.orderId ?? null,
}))
}
export async function createComplaint(
userId: number,
orderId: number | null,
complaintBody: string,
images?: string[] | null
): Promise<void> {
await db.insert(complaints).values({
userId,
orderId,
complaintBody,
images: images || null,
})
}

View file

@ -0,0 +1,146 @@
import { db } from '../db/db_index'
import {
couponApplicableProducts,
couponApplicableUsers,
couponUsage,
coupons,
reservedCoupons,
} from '../db/schema'
import { and, eq, gt, isNull, or } from 'drizzle-orm'
import type { InferSelectModel } from 'drizzle-orm'
import type { UserCoupon, UserCouponApplicableProduct, UserCouponApplicableUser, UserCouponUsage, UserCouponWithRelations } from '@packages/shared'
type CouponRow = InferSelectModel<typeof coupons>
type CouponUsageRow = InferSelectModel<typeof couponUsage>
type CouponApplicableUserRow = InferSelectModel<typeof couponApplicableUsers>
type CouponApplicableProductRow = InferSelectModel<typeof couponApplicableProducts>
type ReservedCouponRow = InferSelectModel<typeof reservedCoupons>
const mapCoupon = (coupon: CouponRow): UserCoupon => ({
id: coupon.id,
couponCode: coupon.couponCode,
isUserBased: coupon.isUserBased,
discountPercent: coupon.discountPercent ? coupon.discountPercent.toString() : null,
flatDiscount: coupon.flatDiscount ? coupon.flatDiscount.toString() : null,
minOrder: coupon.minOrder ? coupon.minOrder.toString() : null,
productIds: coupon.productIds,
maxValue: coupon.maxValue ? coupon.maxValue.toString() : null,
isApplyForAll: coupon.isApplyForAll,
validTill: coupon.validTill ?? null,
maxLimitForUser: coupon.maxLimitForUser ?? null,
isInvalidated: coupon.isInvalidated,
exclusiveApply: coupon.exclusiveApply,
createdAt: coupon.createdAt,
})
const mapUsage = (usage: CouponUsageRow): UserCouponUsage => ({
id: usage.id,
userId: usage.userId,
couponId: usage.couponId,
orderId: usage.orderId ?? null,
orderItemId: usage.orderItemId ?? null,
usedAt: usage.usedAt,
})
const mapApplicableUser = (applicable: CouponApplicableUserRow): UserCouponApplicableUser => ({
id: applicable.id,
couponId: applicable.couponId,
userId: applicable.userId,
})
const mapApplicableProduct = (applicable: CouponApplicableProductRow): UserCouponApplicableProduct => ({
id: applicable.id,
couponId: applicable.couponId,
productId: applicable.productId,
})
const mapCouponWithRelations = (coupon: CouponRow & {
usages: CouponUsageRow[]
applicableUsers: CouponApplicableUserRow[]
applicableProducts: CouponApplicableProductRow[]
}): UserCouponWithRelations => ({
...mapCoupon(coupon),
usages: coupon.usages.map(mapUsage),
applicableUsers: coupon.applicableUsers.map(mapApplicableUser),
applicableProducts: coupon.applicableProducts.map(mapApplicableProduct),
})
export async function getActiveCouponsWithRelations(userId: number): Promise<UserCouponWithRelations[]> {
const allCoupons = await db.query.coupons.findMany({
where: and(
eq(coupons.isInvalidated, false),
or(
isNull(coupons.validTill),
gt(coupons.validTill, new Date())
)
),
with: {
usages: {
where: eq(couponUsage.userId, userId),
},
applicableUsers: true,
applicableProducts: true,
},
})
return allCoupons.map(mapCouponWithRelations)
}
export async function getAllCouponsWithRelations(userId: number): Promise<UserCouponWithRelations[]> {
const allCoupons = await db.query.coupons.findMany({
with: {
usages: {
where: eq(couponUsage.userId, userId),
},
applicableUsers: true,
applicableProducts: true,
},
})
return allCoupons.map(mapCouponWithRelations)
}
export async function getReservedCouponByCode(secretCode: string): Promise<ReservedCouponRow | null> {
const reserved = await db.query.reservedCoupons.findFirst({
where: and(
eq(reservedCoupons.secretCode, secretCode.toUpperCase()),
eq(reservedCoupons.isRedeemed, false)
),
})
return reserved || null
}
export async function redeemReservedCoupon(userId: number, reservedCoupon: ReservedCouponRow): Promise<UserCoupon> {
const couponResult = await db.transaction(async (tx) => {
const [coupon] = await tx.insert(coupons).values({
couponCode: reservedCoupon.couponCode,
isUserBased: true,
discountPercent: reservedCoupon.discountPercent,
flatDiscount: reservedCoupon.flatDiscount,
minOrder: reservedCoupon.minOrder,
productIds: reservedCoupon.productIds,
maxValue: reservedCoupon.maxValue,
isApplyForAll: false,
validTill: reservedCoupon.validTill,
maxLimitForUser: reservedCoupon.maxLimitForUser,
exclusiveApply: reservedCoupon.exclusiveApply,
createdBy: reservedCoupon.createdBy,
}).returning()
await tx.insert(couponApplicableUsers).values({
couponId: coupon.id,
userId,
})
await tx.update(reservedCoupons).set({
isRedeemed: true,
redeemedBy: userId,
redeemedAt: new Date(),
}).where(eq(reservedCoupons.id, reservedCoupon.id))
return coupon
})
return mapCoupon(couponResult)
}

View file

@ -0,0 +1,738 @@
import { db } from '../db/db_index'
import {
orders,
orderItems,
orderStatus,
addresses,
productInfo,
paymentInfoTable,
coupons,
couponUsage,
cartItems,
refunds,
units,
userDetails,
deliverySlotInfo,
} from '../db/schema'
import { and, eq, inArray, desc, gte, sql } from 'drizzle-orm'
import type {
UserOrderSummary,
UserOrderDetail,
UserRecentProduct,
} from '@packages/shared'
export interface OrderItemInput {
productId: number
quantity: number
slotId: number | null
}
export interface PlaceOrderInput {
userId: number
selectedItems: OrderItemInput[]
addressId: number
paymentMethod: 'online' | 'cod'
couponId?: number
userNotes?: string
isFlash?: boolean
}
export interface OrderGroupData {
slotId: number | null
items: Array<{
productId: number
quantity: number
slotId: number | null
product: typeof productInfo.$inferSelect
}>
}
export interface PlacedOrder {
id: number
userId: number
addressId: number
slotId: number | null
totalAmount: string
deliveryCharge: string
isCod: boolean
isOnlinePayment: boolean
paymentInfoId: number | null
readableId: number
userNotes: string | null
orderGroupId: string
orderGroupProportion: string
isFlashDelivery: boolean
createdAt: Date
}
export interface OrderWithRelations {
id: number
userId: number
addressId: number
slotId: number | null
totalAmount: string
deliveryCharge: string
isCod: boolean
isOnlinePayment: boolean
isFlashDelivery: boolean
userNotes: string | null
createdAt: Date
orderItems: Array<{
id: number
productId: number
quantity: string
price: string
discountedPrice: string | null
is_packaged: boolean
product: {
id: number
name: string
images: unknown
}
}>
slot: {
deliveryTime: Date
} | null
paymentInfo: {
id: number
status: string
} | null
orderStatus: Array<{
id: number
isCancelled: boolean
isDelivered: boolean
paymentStatus: string
cancelReason: string | null
}>
refunds: Array<{
refundStatus: string
refundAmount: string | null
}>
}
export interface OrderDetailWithRelations {
id: number
userId: number
addressId: number
slotId: number | null
totalAmount: string
deliveryCharge: string
isCod: boolean
isOnlinePayment: boolean
isFlashDelivery: boolean
userNotes: string | null
createdAt: Date
orderItems: Array<{
id: number
productId: number
quantity: string
price: string
discountedPrice: string | null
is_packaged: boolean
product: {
id: number
name: string
images: unknown
}
}>
slot: {
deliveryTime: Date
} | null
paymentInfo: {
id: number
status: string
} | null
orderStatus: Array<{
id: number
isCancelled: boolean
isDelivered: boolean
paymentStatus: string
cancelReason: string | null
}>
refunds: Array<{
refundStatus: string
refundAmount: string | null
}>
}
export interface CouponValidationResult {
id: number
couponCode: string
isInvalidated: boolean
validTill: Date | null
maxLimitForUser: number | null
minOrder: string | null
discountPercent: string | null
flatDiscount: string | null
maxValue: string | null
usages: Array<{
id: number
userId: number
}>
}
export interface CouponUsageWithCoupon {
id: number
couponId: number
orderId: number | null
coupon: {
id: number
couponCode: string
discountPercent: string | null
flatDiscount: string | null
maxValue: string | null
}
}
export async function validateAndGetCoupon(
couponId: number | undefined,
userId: number,
totalAmount: number
): Promise<CouponValidationResult | null> {
if (!couponId) return null
const coupon = await db.query.coupons.findFirst({
where: eq(coupons.id, couponId),
with: {
usages: { where: eq(couponUsage.userId, userId) },
},
})
if (!coupon) throw new Error('Invalid coupon')
if (coupon.isInvalidated) throw new Error('Coupon is no longer valid')
if (coupon.validTill && new Date(coupon.validTill) < new Date())
throw new Error('Coupon has expired')
if (
coupon.maxLimitForUser &&
coupon.usages.length >= coupon.maxLimitForUser
)
throw new Error('Coupon usage limit exceeded')
if (
coupon.minOrder &&
parseFloat(coupon.minOrder.toString()) > totalAmount
)
throw new Error('Order amount does not meet coupon minimum requirement')
return coupon as CouponValidationResult
}
export function applyDiscountToOrder(
orderTotal: number,
appliedCoupon: CouponValidationResult | null,
proportion: number
): { finalOrderTotal: number; orderGroupProportion: number } {
let finalOrderTotal = orderTotal
if (appliedCoupon) {
if (appliedCoupon.discountPercent) {
const discount = Math.min(
(orderTotal *
parseFloat(appliedCoupon.discountPercent.toString())) /
100,
appliedCoupon.maxValue
? parseFloat(appliedCoupon.maxValue.toString()) * proportion
: Infinity
)
finalOrderTotal -= discount
} else if (appliedCoupon.flatDiscount) {
const discount = Math.min(
parseFloat(appliedCoupon.flatDiscount.toString()) * proportion,
appliedCoupon.maxValue
? parseFloat(appliedCoupon.maxValue.toString()) * proportion
: finalOrderTotal
)
finalOrderTotal -= discount
}
}
return { finalOrderTotal, orderGroupProportion: proportion }
}
export async function getAddressByIdAndUser(
addressId: number,
userId: number
) {
return db.query.addresses.findFirst({
where: and(eq(addresses.userId, userId), eq(addresses.id, addressId)),
})
}
export async function getProductById(productId: number) {
return db.query.productInfo.findFirst({
where: eq(productInfo.id, productId),
})
}
export async function checkUserSuspended(userId: number): Promise<boolean> {
const userDetail = await db.query.userDetails.findFirst({
where: eq(userDetails.userId, userId),
})
return userDetail?.isSuspended ?? false
}
export async function getSlotCapacityStatus(slotId: number): Promise<boolean> {
const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, slotId),
columns: {
isCapacityFull: true,
},
})
return slot?.isCapacityFull ?? false
}
export async function placeOrderTransaction(params: {
userId: number
ordersData: Array<{
order: Omit<typeof orders.$inferInsert, 'id'>
orderItems: Omit<typeof orderItems.$inferInsert, 'id'>[]
orderStatus: Omit<typeof orderStatus.$inferInsert, 'id'>
}>
paymentMethod: 'online' | 'cod'
totalWithDelivery: number
}): Promise<PlacedOrder[]> {
const { userId, ordersData, paymentMethod } = params
return db.transaction(async (tx) => {
let sharedPaymentInfoId: number | null = null
if (paymentMethod === 'online') {
const [paymentInfo] = await tx
.insert(paymentInfoTable)
.values({
status: 'pending',
gateway: 'razorpay',
merchantOrderId: `multi_order_${Date.now()}`,
})
.returning()
sharedPaymentInfoId = paymentInfo.id
}
const ordersToInsert: Omit<typeof orders.$inferInsert, 'id'>[] =
ordersData.map((od) => ({
...od.order,
paymentInfoId: sharedPaymentInfoId,
}))
const insertedOrders = await tx.insert(orders).values(ordersToInsert).returning()
const allOrderItems: Omit<typeof orderItems.$inferInsert, 'id'>[] = []
const allOrderStatuses: Omit<typeof orderStatus.$inferInsert, 'id'>[] = []
insertedOrders.forEach((order, index) => {
const od = ordersData[index]
od.orderItems.forEach((item) => {
allOrderItems.push({ ...item, orderId: order.id })
})
allOrderStatuses.push({
...od.orderStatus,
orderId: order.id,
})
})
await tx.insert(orderItems).values(allOrderItems)
await tx.insert(orderStatus).values(allOrderStatuses)
return insertedOrders as PlacedOrder[]
})
}
export async function deleteCartItemsForOrder(
userId: number,
productIds: number[]
): Promise<void> {
await db.delete(cartItems).where(
and(
eq(cartItems.userId, userId),
inArray(cartItems.productId, productIds)
)
)
}
export async function recordCouponUsage(
userId: number,
couponId: number,
orderId: number
): Promise<void> {
await db.insert(couponUsage).values({
userId,
couponId,
orderId,
orderItemId: null,
usedAt: new Date(),
})
}
export async function getOrdersWithRelations(
userId: number,
offset: number,
pageSize: number
): Promise<OrderWithRelations[]> {
return db.query.orders.findMany({
where: eq(orders.userId, userId),
with: {
orderItems: {
with: {
product: {
columns: {
id: true,
name: true,
images: true,
},
},
},
},
slot: {
columns: {
deliveryTime: true,
},
},
paymentInfo: {
columns: {
id: true,
status: true,
},
},
orderStatus: {
columns: {
id: true,
isCancelled: true,
isDelivered: true,
paymentStatus: true,
cancelReason: true,
},
},
refunds: {
columns: {
refundStatus: true,
refundAmount: true,
},
},
},
orderBy: (ordersTable: typeof orders) => [desc(ordersTable.createdAt)],
limit: pageSize,
offset: offset,
}) as Promise<OrderWithRelations[]>
}
export async function getOrderCount(userId: number): Promise<number> {
const result = await db
.select({ count: sql`count(*)` })
.from(orders)
.where(eq(orders.userId, userId))
return Number(result[0]?.count ?? 0)
}
export async function getOrderByIdWithRelations(
orderId: number,
userId: number
): Promise<OrderDetailWithRelations | null> {
const order = await db.query.orders.findFirst({
where: and(eq(orders.id, orderId), eq(orders.userId, userId)),
with: {
orderItems: {
with: {
product: {
columns: {
id: true,
name: true,
images: true,
},
},
},
},
slot: {
columns: {
deliveryTime: true,
},
},
paymentInfo: {
columns: {
id: true,
status: true,
},
},
orderStatus: {
columns: {
id: true,
isCancelled: true,
isDelivered: true,
paymentStatus: true,
cancelReason: true,
},
with: {
refundCoupon: {
columns: {
id: true,
couponCode: true,
},
},
},
},
refunds: {
columns: {
refundStatus: true,
refundAmount: true,
},
},
},
})
return order as OrderDetailWithRelations | null
}
export async function getCouponUsageForOrder(
orderId: number
): Promise<CouponUsageWithCoupon[]> {
return db.query.couponUsage.findMany({
where: eq(couponUsage.orderId, orderId),
with: {
coupon: {
columns: {
id: true,
couponCode: true,
discountPercent: true,
flatDiscount: true,
maxValue: true,
},
},
},
}) as Promise<CouponUsageWithCoupon[]>
}
export async function getOrderBasic(orderId: number) {
return db.query.orders.findFirst({
where: eq(orders.id, orderId),
with: {
orderStatus: {
columns: {
id: true,
isCancelled: true,
isDelivered: true,
},
},
},
})
}
export async function cancelOrderTransaction(
orderId: number,
statusId: number,
reason: string,
isCod: boolean
): Promise<void> {
await db.transaction(async (tx) => {
await tx
.update(orderStatus)
.set({
isCancelled: true,
cancelReason: reason,
cancellationUserNotes: reason,
cancellationReviewed: false,
})
.where(eq(orderStatus.id, statusId))
const refundStatus = isCod ? 'na' : 'pending'
await tx.insert(refunds).values({
orderId,
refundStatus,
})
})
}
export async function updateOrderNotes(
orderId: number,
userNotes: string
): Promise<void> {
await db
.update(orders)
.set({
userNotes: userNotes || null,
})
.where(eq(orders.id, orderId))
}
export async function getRecentlyDeliveredOrderIds(
userId: number,
limit: number,
since: Date
): Promise<number[]> {
const recentOrders = await db
.select({ id: orders.id })
.from(orders)
.innerJoin(orderStatus, eq(orders.id, orderStatus.orderId))
.where(
and(
eq(orders.userId, userId),
eq(orderStatus.isDelivered, true),
gte(orders.createdAt, since)
)
)
.orderBy(desc(orders.createdAt))
.limit(limit)
return recentOrders.map((order) => order.id)
}
export async function getProductIdsFromOrders(
orderIds: number[]
): Promise<number[]> {
const orderItemsResult = await db
.select({ productId: orderItems.productId })
.from(orderItems)
.where(inArray(orderItems.orderId, orderIds))
return [...new Set(orderItemsResult.map((item) => item.productId))]
}
export interface RecentProductData {
id: number
name: string
shortDescription: string | null
price: string
images: unknown
isOutOfStock: boolean
unitShortNotation: string
incrementStep: number
}
export async function getProductsForRecentOrders(
productIds: number[],
limit: number
): Promise<RecentProductData[]> {
const results = await db
.select({
id: productInfo.id,
name: productInfo.name,
shortDescription: productInfo.shortDescription,
price: productInfo.price,
images: productInfo.images,
isOutOfStock: productInfo.isOutOfStock,
unitShortNotation: units.shortNotation,
incrementStep: productInfo.incrementStep,
})
.from(productInfo)
.innerJoin(units, eq(productInfo.unitId, units.id))
.where(
and(
inArray(productInfo.id, productIds),
eq(productInfo.isSuspended, false)
)
)
.orderBy(desc(productInfo.createdAt))
.limit(limit)
return results.map((product) => ({
...product,
price: String(product.price ?? '0'),
}))
}
// ============================================================================
// Post-Order Handler Helpers (for Telegram notifications)
// ============================================================================
export interface OrderWithFullData {
id: number
totalAmount: string
isFlashDelivery: boolean
address: {
name: string | null
addressLine1: string | null
addressLine2: string | null
city: string | null
state: string | null
pincode: string | null
phone: string | null
} | null
orderItems: Array<{
quantity: string
product: {
name: string
} | null
}>
slot: {
deliveryTime: Date
} | null
}
export async function getOrdersByIdsWithFullData(
orderIds: number[]
): Promise<OrderWithFullData[]> {
return db.query.orders.findMany({
where: inArray(orders.id, orderIds),
with: {
address: {
columns: {
name: true,
addressLine1: true,
addressLine2: true,
city: true,
state: true,
pincode: true,
phone: true,
},
},
orderItems: {
with: {
product: {
columns: {
name: true,
},
},
},
},
slot: {
columns: {
deliveryTime: true,
},
},
},
}) as Promise<OrderWithFullData[]>
}
export interface OrderWithCancellationData extends OrderWithFullData {
refunds: Array<{
refundStatus: string
}>
}
export async function getOrderByIdWithFullData(
orderId: number
): Promise<OrderWithCancellationData | null> {
return db.query.orders.findFirst({
where: eq(orders.id, orderId),
with: {
address: {
columns: {
name: true,
addressLine1: true,
addressLine2: true,
city: true,
state: true,
pincode: true,
phone: true,
},
},
orderItems: {
with: {
product: {
columns: {
name: true,
},
},
},
},
slot: {
columns: {
deliveryTime: true,
},
},
refunds: {
columns: {
refundStatus: true,
},
},
},
}) as Promise<OrderWithCancellationData | null>
}

View file

@ -0,0 +1,51 @@
import { db } from '../db/db_index'
import { orders, payments, orderStatus } from '../db/schema'
import { eq } from 'drizzle-orm'
export async function getOrderById(orderId: number) {
return db.query.orders.findFirst({
where: eq(orders.id, orderId),
})
}
export async function getPaymentByOrderId(orderId: number) {
return db.query.payments.findFirst({
where: eq(payments.orderId, orderId),
})
}
export async function getPaymentByMerchantOrderId(merchantOrderId: string) {
return db.query.payments.findFirst({
where: eq(payments.merchantOrderId, merchantOrderId),
})
}
export async function updatePaymentSuccess(merchantOrderId: string, payload: unknown) {
const [updatedPayment] = await db
.update(payments)
.set({
status: 'success',
payload,
})
.where(eq(payments.merchantOrderId, merchantOrderId))
.returning({
id: payments.id,
orderId: payments.orderId,
})
return updatedPayment || null
}
export async function updateOrderPaymentStatus(orderId: number, status: 'pending' | 'success' | 'cod' | 'failed') {
await db
.update(orderStatus)
.set({ paymentStatus: status })
.where(eq(orderStatus.orderId, orderId))
}
export async function markPaymentFailed(paymentId: number) {
await db
.update(payments)
.set({ status: 'failed' })
.where(eq(payments.id, paymentId))
}

View file

@ -0,0 +1,271 @@
import { db } from '../db/db_index'
import { deliverySlotInfo, productInfo, productReviews, productSlots, productTags, specialDeals, storeInfo, units, users } from '../db/schema'
import { and, desc, eq, gt, inArray, sql } from 'drizzle-orm'
import type { UserProductDetailData, UserProductReview } from '@packages/shared'
const getStringArray = (value: unknown): string[] | null => {
if (!Array.isArray(value)) return null
return value.map((item) => String(item))
}
export async function getProductDetailById(productId: number): Promise<UserProductDetailData | null> {
const productData = await db
.select({
id: productInfo.id,
name: productInfo.name,
shortDescription: productInfo.shortDescription,
longDescription: productInfo.longDescription,
price: productInfo.price,
marketPrice: productInfo.marketPrice,
images: productInfo.images,
isOutOfStock: productInfo.isOutOfStock,
storeId: productInfo.storeId,
unitShortNotation: units.shortNotation,
incrementStep: productInfo.incrementStep,
productQuantity: productInfo.productQuantity,
isFlashAvailable: productInfo.isFlashAvailable,
flashPrice: productInfo.flashPrice,
})
.from(productInfo)
.innerJoin(units, eq(productInfo.unitId, units.id))
.where(eq(productInfo.id, productId))
.limit(1)
if (productData.length === 0) {
return null
}
const product = productData[0]
const storeData = product.storeId ? await db.query.storeInfo.findFirst({
where: eq(storeInfo.id, product.storeId),
columns: { id: true, name: true, description: true },
}) : null
const deliverySlotsData = await db
.select({
id: deliverySlotInfo.id,
deliveryTime: deliverySlotInfo.deliveryTime,
freezeTime: deliverySlotInfo.freezeTime,
})
.from(productSlots)
.innerJoin(deliverySlotInfo, eq(productSlots.slotId, deliverySlotInfo.id))
.where(
and(
eq(productSlots.productId, productId),
eq(deliverySlotInfo.isActive, true),
eq(deliverySlotInfo.isCapacityFull, false),
gt(deliverySlotInfo.deliveryTime, sql`CURRENT_TIMESTAMP`),
gt(deliverySlotInfo.freezeTime, sql`CURRENT_TIMESTAMP`)
)
)
.orderBy(deliverySlotInfo.deliveryTime)
const specialDealsData = await db
.select({
quantity: specialDeals.quantity,
price: specialDeals.price,
validTill: specialDeals.validTill,
})
.from(specialDeals)
.where(
and(
eq(specialDeals.productId, productId),
gt(specialDeals.validTill, sql`CURRENT_TIMESTAMP`)
)
)
.orderBy(specialDeals.quantity)
return {
id: product.id,
name: product.name,
shortDescription: product.shortDescription ?? null,
longDescription: product.longDescription ?? null,
price: String(product.price ?? '0'),
marketPrice: product.marketPrice ? String(product.marketPrice) : null,
unitNotation: product.unitShortNotation,
images: getStringArray(product.images),
isOutOfStock: product.isOutOfStock,
store: storeData ? {
id: storeData.id,
name: storeData.name,
description: storeData.description ?? null,
} : null,
incrementStep: product.incrementStep,
productQuantity: product.productQuantity,
isFlashAvailable: product.isFlashAvailable,
flashPrice: product.flashPrice?.toString() || null,
deliverySlots: deliverySlotsData,
specialDeals: specialDealsData.map((deal) => ({
quantity: String(deal.quantity ?? '0'),
price: String(deal.price ?? '0'),
validTill: deal.validTill,
})),
}
}
export async function getProductReviews(productId: number, limit: number, offset: number) {
const reviews = await db
.select({
id: productReviews.id,
reviewBody: productReviews.reviewBody,
ratings: productReviews.ratings,
imageUrls: productReviews.imageUrls,
reviewTime: productReviews.reviewTime,
userName: users.name,
})
.from(productReviews)
.innerJoin(users, eq(productReviews.userId, users.id))
.where(eq(productReviews.productId, productId))
.orderBy(desc(productReviews.reviewTime))
.limit(limit)
.offset(offset)
const totalCountResult = await db
.select({ count: sql`count(*)` })
.from(productReviews)
.where(eq(productReviews.productId, productId))
const totalCount = Number(totalCountResult[0].count)
const mappedReviews: UserProductReview[] = reviews.map((review) => ({
id: review.id,
reviewBody: review.reviewBody,
ratings: review.ratings,
imageUrls: getStringArray(review.imageUrls),
reviewTime: review.reviewTime,
userName: review.userName ?? null,
}))
return {
reviews: mappedReviews,
totalCount,
}
}
export async function getProductById(productId: number) {
return db.query.productInfo.findFirst({
where: eq(productInfo.id, productId),
})
}
export async function createProductReview(
userId: number,
productId: number,
reviewBody: string,
ratings: number,
imageUrls: string[]
): Promise<UserProductReview> {
const [newReview] = await db.insert(productReviews).values({
userId,
productId,
reviewBody,
ratings,
imageUrls,
}).returning({
id: productReviews.id,
reviewBody: productReviews.reviewBody,
ratings: productReviews.ratings,
imageUrls: productReviews.imageUrls,
reviewTime: productReviews.reviewTime,
})
return {
id: newReview.id,
reviewBody: newReview.reviewBody,
ratings: newReview.ratings,
imageUrls: getStringArray(newReview.imageUrls),
reviewTime: newReview.reviewTime,
userName: null,
}
}
export interface ProductSummaryData {
id: number
name: string
shortDescription: string | null
price: string
marketPrice: string | null
images: unknown
isOutOfStock: boolean
unitShortNotation: string
productQuantity: number
}
export async function getAllProductsWithUnits(tagId?: number): Promise<ProductSummaryData[]> {
let productIds: number[] | null = null
// If tagId is provided, get products that have this tag
if (tagId) {
const taggedProducts = await db
.select({ productId: productTags.productId })
.from(productTags)
.where(eq(productTags.tagId, tagId))
productIds = taggedProducts.map(tp => tp.productId)
}
let whereCondition = undefined
// Filter by product IDs if tag filtering is applied
if (productIds && productIds.length > 0) {
whereCondition = inArray(productInfo.id, productIds)
}
const results = await db
.select({
id: productInfo.id,
name: productInfo.name,
shortDescription: productInfo.shortDescription,
price: productInfo.price,
marketPrice: productInfo.marketPrice,
images: productInfo.images,
isOutOfStock: productInfo.isOutOfStock,
unitShortNotation: units.shortNotation,
productQuantity: productInfo.productQuantity,
})
.from(productInfo)
.innerJoin(units, eq(productInfo.unitId, units.id))
.where(whereCondition)
return results.map((product) => ({
...product,
price: String(product.price ?? '0'),
marketPrice: product.marketPrice ? String(product.marketPrice) : null,
}))
}
/**
* Get all suspended product IDs
*/
export async function getSuspendedProductIds(): Promise<number[]> {
const suspendedProducts = await db
.select({ id: productInfo.id })
.from(productInfo)
.where(eq(productInfo.isSuspended, true))
return suspendedProducts.map(sp => sp.id)
}
/**
* Get next delivery date for a product (with capacity check)
* This version filters by both isActive AND isCapacityFull
*/
export async function getNextDeliveryDateWithCapacity(productId: number): Promise<Date | null> {
const result = await db
.select({ deliveryTime: deliverySlotInfo.deliveryTime })
.from(productSlots)
.innerJoin(deliverySlotInfo, eq(productSlots.slotId, deliverySlotInfo.id))
.where(
and(
eq(productSlots.productId, productId),
eq(deliverySlotInfo.isActive, true),
eq(deliverySlotInfo.isCapacityFull, false),
gt(deliverySlotInfo.deliveryTime, sql`CURRENT_TIMESTAMP`)
)
)
.orderBy(deliverySlotInfo.deliveryTime)
.limit(1)
return result[0]?.deliveryTime || null
}

View file

@ -0,0 +1,46 @@
import { db } from '../db/db_index'
import { deliverySlotInfo, productInfo } from '../db/schema'
import { asc, eq } from 'drizzle-orm'
import type { InferSelectModel } from 'drizzle-orm'
import type { UserDeliverySlot, UserSlotAvailability } from '@packages/shared'
type SlotRow = InferSelectModel<typeof deliverySlotInfo>
const mapSlot = (slot: SlotRow): UserDeliverySlot => ({
id: slot.id,
deliveryTime: slot.deliveryTime,
freezeTime: slot.freezeTime,
isActive: slot.isActive,
isFlash: slot.isFlash,
isCapacityFull: slot.isCapacityFull,
deliverySequence: slot.deliverySequence,
groupIds: slot.groupIds,
})
export async function getActiveSlotsList(): Promise<UserDeliverySlot[]> {
const slots = await db.query.deliverySlotInfo.findMany({
where: eq(deliverySlotInfo.isActive, true),
orderBy: asc(deliverySlotInfo.deliveryTime),
})
return slots.map(mapSlot)
}
export async function getProductAvailability(): Promise<UserSlotAvailability[]> {
const products = await db
.select({
id: productInfo.id,
name: productInfo.name,
isOutOfStock: productInfo.isOutOfStock,
isFlashAvailable: productInfo.isFlashAvailable,
})
.from(productInfo)
.where(eq(productInfo.isSuspended, false))
return products.map((product) => ({
id: product.id,
name: product.name,
isOutOfStock: product.isOutOfStock,
isFlashAvailable: product.isFlashAvailable,
}))
}

View file

@ -0,0 +1,141 @@
import { db } from '../db/db_index'
import { productInfo, storeInfo, units } from '../db/schema'
import { and, eq, sql } from 'drizzle-orm'
import type { InferSelectModel } from 'drizzle-orm'
import type { UserStoreDetailData, UserStoreProductData, UserStoreSummaryData, StoreSummary } from '@packages/shared'
type StoreRow = InferSelectModel<typeof storeInfo>
type StoreProductRow = {
id: number
name: string
shortDescription: string | null
price: string | null
marketPrice: string | null
images: unknown
isOutOfStock: boolean
incrementStep: number
unitShortNotation: string
productQuantity: number
}
const getStringArray = (value: unknown): string[] | null => {
if (!Array.isArray(value)) return null
return value.map((item) => String(item))
}
export async function getStoreSummaries(): Promise<UserStoreSummaryData[]> {
const storesData = await db
.select({
id: storeInfo.id,
name: storeInfo.name,
description: storeInfo.description,
imageUrl: storeInfo.imageUrl,
productCount: sql<number>`count(${productInfo.id})`.as('productCount'),
})
.from(storeInfo)
.leftJoin(
productInfo,
and(eq(productInfo.storeId, storeInfo.id), eq(productInfo.isSuspended, false))
)
.groupBy(storeInfo.id)
const storesWithDetails = await Promise.all(
storesData.map(async (store) => {
const sampleProducts = await db
.select({
id: productInfo.id,
name: productInfo.name,
images: productInfo.images,
})
.from(productInfo)
.where(and(eq(productInfo.storeId, store.id), eq(productInfo.isSuspended, false)))
.limit(3)
return {
id: store.id,
name: store.name,
description: store.description ?? null,
imageUrl: store.imageUrl ?? null,
productCount: store.productCount || 0,
sampleProducts: sampleProducts.map((product) => ({
id: product.id,
name: product.name,
images: getStringArray(product.images),
})),
}
})
)
return storesWithDetails
}
export async function getStoreDetail(storeId: number): Promise<UserStoreDetailData | null> {
const storeData = await db.query.storeInfo.findFirst({
where: eq(storeInfo.id, storeId),
columns: {
id: true,
name: true,
description: true,
imageUrl: true,
},
})
if (!storeData) {
return null
}
const productsData = await db
.select({
id: productInfo.id,
name: productInfo.name,
shortDescription: productInfo.shortDescription,
price: productInfo.price,
marketPrice: productInfo.marketPrice,
images: productInfo.images,
isOutOfStock: productInfo.isOutOfStock,
incrementStep: productInfo.incrementStep,
unitShortNotation: units.shortNotation,
productQuantity: productInfo.productQuantity,
})
.from(productInfo)
.innerJoin(units, eq(productInfo.unitId, units.id))
.where(and(eq(productInfo.storeId, storeId), eq(productInfo.isSuspended, false)))
const products = productsData.map((product: StoreProductRow): UserStoreProductData => ({
id: product.id,
name: product.name,
shortDescription: product.shortDescription ?? null,
price: String(product.price ?? '0'),
marketPrice: product.marketPrice ? String(product.marketPrice) : null,
incrementStep: product.incrementStep,
unit: product.unitShortNotation,
unitNotation: product.unitShortNotation,
images: getStringArray(product.images),
isOutOfStock: product.isOutOfStock,
productQuantity: product.productQuantity,
}))
return {
store: {
id: storeData.id,
name: storeData.name,
description: storeData.description ?? null,
imageUrl: storeData.imageUrl ?? null,
},
products,
}
}
/**
* Get simple store summary (id, name, description only)
* Used for common API endpoints
*/
export async function getStoresSummary(): Promise<StoreSummary[]> {
return db.query.storeInfo.findMany({
columns: {
id: true,
name: true,
description: true,
},
})
}

View file

@ -0,0 +1,28 @@
import { db } from '../db/db_index'
import { productTags } from '../db/schema'
import { eq } from 'drizzle-orm'
export async function getAllTags(): Promise<any[]> {
return db.query.productTags.findMany({
with: {
// products: {
// with: {
// product: true,
// },
// },
},
})
}
export async function getTagById(id: number): Promise<any | null> {
return db.query.productTags.findFirst({
where: eq(productTags.id, id),
with: {
// products: {
// with: {
// product: true,
// },
// },
},
})
}

View file

@ -0,0 +1,75 @@
import { db } from '../db/db_index'
import { notifCreds, unloggedUserTokens, userCreds, userDetails, users } from '../db/schema'
import { and, eq } from 'drizzle-orm'
export async function getUserById(userId: number) {
const [user] = await db.select().from(users).where(eq(users.id, userId)).limit(1)
return user || null
}
export async function getUserDetailByUserId(userId: number) {
const [detail] = await db.select().from(userDetails).where(eq(userDetails.userId, userId)).limit(1)
return detail || null
}
export async function getUserWithCreds(userId: number) {
const result = await db
.select()
.from(users)
.leftJoin(userCreds, eq(users.id, userCreds.userId))
.where(eq(users.id, userId))
.limit(1)
if (result.length === 0) return null
return {
user: result[0].users,
creds: result[0].user_creds,
}
}
export async function getNotifCred(userId: number, token: string) {
return db.query.notifCreds.findFirst({
where: and(eq(notifCreds.userId, userId), eq(notifCreds.token, token)),
})
}
export async function upsertNotifCred(userId: number, token: string): Promise<void> {
const existing = await getNotifCred(userId, token)
if (existing) {
await db.update(notifCreds)
.set({ lastVerified: new Date() })
.where(eq(notifCreds.id, existing.id))
return
}
await db.insert(notifCreds).values({
userId,
token,
lastVerified: new Date(),
})
}
export async function deleteUnloggedToken(token: string): Promise<void> {
await db.delete(unloggedUserTokens).where(eq(unloggedUserTokens.token, token))
}
export async function getUnloggedToken(token: string) {
return db.query.unloggedUserTokens.findFirst({
where: eq(unloggedUserTokens.token, token),
})
}
export async function upsertUnloggedToken(token: string): Promise<void> {
const existing = await getUnloggedToken(token)
if (existing) {
await db.update(unloggedUserTokens)
.set({ lastVerified: new Date() })
.where(eq(unloggedUserTokens.id, existing.id))
return
}
await db.insert(unloggedUserTokens).values({
token,
lastVerified: new Date(),
})
}

View file

@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"paths": {
"@/*": ["./*"]
},
"resolveJsonModule": true,
"outDir": "./dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}

View file

@ -363,6 +363,7 @@ export interface AdminProduct {
price: string;
marketPrice: string | null;
images: string[] | null;
imageKeys: string[] | null;
isOutOfStock: boolean;
isSuspended: boolean;
isFlashAvailable: boolean;

View file

@ -25,6 +25,7 @@ import ImageCarousel from "./src/components/ImageCarousel";
import ImageGallery from "./src/components/ImageGallery";
import ImageGalleryWithDelete from "./src/components/ImageGalleryWithDelete";
import ImageUploader from "./src/components/ImageUploader";
import ImageUploaderNeo, { ImageUploaderNeoItem, ImageUploaderNeoPayload } from "./src/components/ImageUploaderNeo";
import ProfileImage from "./src/components/profile-image";
import Checkbox from "./src/components/checkbox";
import AppContainer from "./src/components/app-container";
@ -100,6 +101,9 @@ export {
ImageGallery,
ImageGalleryWithDelete,
ImageUploader,
ImageUploaderNeo,
ImageUploaderNeoItem,
ImageUploaderNeoPayload,
ProfileImage,
Checkbox,
AppContainer,

View file

@ -0,0 +1,98 @@
import React from 'react'
import { View } from 'react-native'
import { Image } from 'expo-image'
import Ionicons from '@expo/vector-icons/Ionicons'
import { MaterialIcons } from '@expo/vector-icons'
import tw from '../lib/tailwind'
import MyText from './text'
import MyTouchableOpacity from './touchable-opacity'
import usePickImage from './use-pick-image'
export interface ImageUploaderNeoItem {
imgUrl: string
mimeType: string | null
}
export interface ImageUploaderNeoPayload {
url: string
mimeType: string | null
}
interface ImageUploaderNeoProps {
images: ImageUploaderNeoItem[]
onImageAdd: (images: ImageUploaderNeoPayload[]) => void
onImageRemove: (image: ImageUploaderNeoPayload) => void
allowMultiple?: boolean
}
const ImageUploaderNeo: React.FC<ImageUploaderNeoProps> = ({
images,
onImageAdd,
onImageRemove,
allowMultiple = true,
}) => {
const totalImageCount = images.length
const handleAddImages = (files: any) => {
if (!files) return
const assets = Array.isArray(files) ? files : [files]
const payload = assets.map((asset) => ({
url: asset.uri,
mimeType: asset.mimeType ?? null,
}))
onImageAdd(payload)
}
const handlePickImage = usePickImage({
setFile: handleAddImages,
multiple: allowMultiple,
})
// console.log({images})
return (
<View style={tw`mb-4`}>
<View style={tw`flex-row flex-wrap -mx-1`}>
{images.map((image, index) => (
<View key={`neo-${index}`} style={tw`w-1/3 px-1 mb-2 relative`}>
<Image
source={{ uri: image.imgUrl }}
style={tw`w-full aspect-square rounded`}
/>
<MyTouchableOpacity
onPress={() =>
onImageRemove({
url: image.imgUrl,
mimeType: image.mimeType ?? null,
})
}
style={tw`absolute top-0 right-0 bg-red-500 rounded-full p-1`}
>
<Ionicons name='close' size={16} color='white' />
</MyTouchableOpacity>
</View>
))}
<MyTouchableOpacity
disabled={!allowMultiple && totalImageCount >= 1}
onPress={handlePickImage}
style={tw`w-1/3 px-1 mb-2`}
>
<View style={tw`w-full aspect-square bg-gray-200 rounded justify-center items-center opacity-75`}>
{!allowMultiple && totalImageCount >= 1 ? (
<View style={tw`absolute inset-0 bg-white/70 rounded justify-center items-center`}>
<MyText style={tw`text-center text-gray-500`}>
Only one image allowed
</MyText>
</View>
) : (
<MaterialIcons name='add' size={32} color='black' />
)}
</View>
</MyTouchableOpacity>
</View>
</View>
)
}
export default ImageUploaderNeo