enh
This commit is contained in:
parent
5e9bc3e38e
commit
ca7d8df1c8
89 changed files with 10704 additions and 1148 deletions
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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.');
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
118
apps/admin-ui/hooks/useUploadToObjectStore.ts
Normal file
118
apps/admin-ui/hooks/useUploadToObjectStore.ts
Normal 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);
|
||||
}
|
||||
|
|
@ -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'] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 = '';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 = '';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
*/
|
||||
|
|
@ -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;
|
||||
|
|
@ -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'
|
||||
*/
|
||||
|
|
@ -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;
|
||||
|
|
@ -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"
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use('/av', avRouter);
|
||||
router.use('/cm', commonRouter);
|
||||
router.use('/uv', uvRouter);
|
||||
|
||||
|
||||
const v1Router = router;
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
119
apps/user-ui/hooks/useUploadToObjectStore.ts
Normal file
119
apps/user-ui/hooks/useUploadToObjectStore.ts
Normal 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)
|
||||
}
|
||||
|
|
@ -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
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
@ -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 }));
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
2
packages/db_helper_sqlite/.env.example
Normal file
2
packages/db_helper_sqlite/.env.example
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# Copy this to .env and fill in your D1 database URL or local path
|
||||
DATABASE_URL=file:./dev.db
|
||||
43
packages/db_helper_sqlite/README.md
Normal file
43
packages/db_helper_sqlite/README.md
Normal 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
|
||||
11
packages/db_helper_sqlite/drizzle.config.ts
Normal file
11
packages/db_helper_sqlite/drizzle.config.ts
Normal 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!,
|
||||
},
|
||||
})
|
||||
393
packages/db_helper_sqlite/index.ts
Normal file
393
packages/db_helper_sqlite/index.ts
Normal 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'
|
||||
24
packages/db_helper_sqlite/package.json
Normal file
24
packages/db_helper_sqlite/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
114
packages/db_helper_sqlite/src/admin-apis/banner.ts
Normal file
114
packages/db_helper_sqlite/src/admin-apis/banner.ts
Normal 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))
|
||||
}
|
||||
74
packages/db_helper_sqlite/src/admin-apis/complaint.ts
Normal file
74
packages/db_helper_sqlite/src/admin-apis/complaint.ts
Normal 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))
|
||||
}
|
||||
29
packages/db_helper_sqlite/src/admin-apis/const.ts
Normal file
29
packages/db_helper_sqlite/src/admin-apis/const.ts
Normal 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 },
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
496
packages/db_helper_sqlite/src/admin-apis/coupon.ts
Normal file
496
packages/db_helper_sqlite/src/admin-apis/coupon.ts
Normal 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,
|
||||
}))
|
||||
}
|
||||
}
|
||||
710
packages/db_helper_sqlite/src/admin-apis/order.ts
Normal file
710
packages/db_helper_sqlite/src/admin-apis/order.ts
Normal 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))
|
||||
})
|
||||
}
|
||||
827
packages/db_helper_sqlite/src/admin-apis/product.ts
Normal file
827
packages/db_helper_sqlite/src/admin-apis/product.ts
Normal 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)
|
||||
}
|
||||
351
packages/db_helper_sqlite/src/admin-apis/slots.ts
Normal file
351
packages/db_helper_sqlite/src/admin-apis/slots.ts
Normal 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'}`,
|
||||
}
|
||||
}
|
||||
154
packages/db_helper_sqlite/src/admin-apis/staff-user.ts
Normal file
154
packages/db_helper_sqlite/src/admin-apis/staff-user.ts
Normal 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
|
||||
}
|
||||
145
packages/db_helper_sqlite/src/admin-apis/store.ts
Normal file
145
packages/db_helper_sqlite/src/admin-apis/store.ts
Normal 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',
|
||||
}
|
||||
})
|
||||
}
|
||||
270
packages/db_helper_sqlite/src/admin-apis/user.ts
Normal file
270
packages/db_helper_sqlite/src/admin-apis/user.ts
Normal 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
|
||||
}
|
||||
250
packages/db_helper_sqlite/src/admin-apis/vendor-snippets.ts
Normal file
250
packages/db_helper_sqlite/src/admin-apis/vendor-snippets.ts
Normal 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 }
|
||||
}
|
||||
19
packages/db_helper_sqlite/src/common-apis/utils.ts
Normal file
19
packages/db_helper_sqlite/src/common-apis/utils.ts
Normal 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
|
||||
}
|
||||
26
packages/db_helper_sqlite/src/db/db_index.ts
Normal file
26
packages/db_helper_sqlite/src/db/db_index.ts
Normal 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]
|
||||
},
|
||||
})
|
||||
125
packages/db_helper_sqlite/src/db/porter.ts
Normal file
125
packages/db_helper_sqlite/src/db/porter.ts
Normal 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)
|
||||
})
|
||||
728
packages/db_helper_sqlite/src/db/schema.ts
Normal file
728
packages/db_helper_sqlite/src/db/schema.ts
Normal 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] }),
|
||||
}))
|
||||
147
packages/db_helper_sqlite/src/db/seed.ts
Normal file
147
packages/db_helper_sqlite/src/db/seed.ts
Normal 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.')
|
||||
}
|
||||
47
packages/db_helper_sqlite/src/db/types.ts
Normal file
47
packages/db_helper_sqlite/src/db/types.ts
Normal 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
|
||||
}
|
||||
114
packages/db_helper_sqlite/src/helper_methods/banner.ts
Normal file
114
packages/db_helper_sqlite/src/helper_methods/banner.ts
Normal 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))
|
||||
}
|
||||
74
packages/db_helper_sqlite/src/helper_methods/complaint.ts
Normal file
74
packages/db_helper_sqlite/src/helper_methods/complaint.ts
Normal 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))
|
||||
}
|
||||
29
packages/db_helper_sqlite/src/helper_methods/const.ts
Normal file
29
packages/db_helper_sqlite/src/helper_methods/const.ts
Normal 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 },
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
632
packages/db_helper_sqlite/src/helper_methods/coupon.ts
Normal file
632
packages/db_helper_sqlite/src/helper_methods/coupon.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
}
|
||||
269
packages/db_helper_sqlite/src/helper_methods/order.ts
Normal file
269
packages/db_helper_sqlite/src/helper_methods/order.ts
Normal 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;
|
||||
}
|
||||
130
packages/db_helper_sqlite/src/helper_methods/product.ts
Normal file
130
packages/db_helper_sqlite/src/helper_methods/product.ts
Normal 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)
|
||||
));
|
||||
}
|
||||
101
packages/db_helper_sqlite/src/helper_methods/slots.ts
Normal file
101
packages/db_helper_sqlite/src/helper_methods/slots.ts
Normal 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));
|
||||
}
|
||||
153
packages/db_helper_sqlite/src/helper_methods/staff-user.ts
Normal file
153
packages/db_helper_sqlite/src/helper_methods/staff-user.ts
Normal 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;
|
||||
}
|
||||
150
packages/db_helper_sqlite/src/helper_methods/store.ts
Normal file
150
packages/db_helper_sqlite/src/helper_methods/store.ts
Normal 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",
|
||||
};
|
||||
});
|
||||
}
|
||||
20
packages/db_helper_sqlite/src/helper_methods/upload-url.ts
Normal file
20
packages/db_helper_sqlite/src/helper_methods/upload-url.ts
Normal 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
|
||||
}
|
||||
270
packages/db_helper_sqlite/src/helper_methods/user.ts
Normal file
270
packages/db_helper_sqlite/src/helper_methods/user.ts
Normal 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;
|
||||
}
|
||||
130
packages/db_helper_sqlite/src/helper_methods/vendor-snippets.ts
Normal file
130
packages/db_helper_sqlite/src/helper_methods/vendor-snippets.ts
Normal 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));
|
||||
}
|
||||
41
packages/db_helper_sqlite/src/lib/automated-jobs.ts
Normal file
41
packages/db_helper_sqlite/src/lib/automated-jobs.ts
Normal 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)
|
||||
}
|
||||
49
packages/db_helper_sqlite/src/lib/const-keys.ts
Normal file
49
packages/db_helper_sqlite/src/lib/const-keys.ts
Normal 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[]
|
||||
38
packages/db_helper_sqlite/src/lib/delete-orders.ts
Normal file
38
packages/db_helper_sqlite/src/lib/delete-orders.ts
Normal 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))
|
||||
}
|
||||
55
packages/db_helper_sqlite/src/lib/env-exporter.ts
Normal file
55
packages/db_helper_sqlite/src/lib/env-exporter.ts
Normal 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'
|
||||
18
packages/db_helper_sqlite/src/lib/health-check.ts
Normal file
18
packages/db_helper_sqlite/src/lib/health-check.ts
Normal 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' }
|
||||
}
|
||||
}
|
||||
127
packages/db_helper_sqlite/src/lib/seed.ts
Normal file
127
packages/db_helper_sqlite/src/lib/seed.ts
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
294
packages/db_helper_sqlite/src/stores/store-helpers.ts
Normal file
294
packages/db_helper_sqlite/src/stores/store-helpers.ts
Normal 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)
|
||||
}
|
||||
148
packages/db_helper_sqlite/src/user-apis/address.ts
Normal file
148
packages/db_helper_sqlite/src/user-apis/address.ts
Normal 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
|
||||
}
|
||||
229
packages/db_helper_sqlite/src/user-apis/auth.ts
Normal file
229
packages/db_helper_sqlite/src/user-apis/auth.ts
Normal 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))
|
||||
})
|
||||
}
|
||||
29
packages/db_helper_sqlite/src/user-apis/banners.ts
Normal file
29
packages/db_helper_sqlite/src/user-apis/banners.ts
Normal 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)
|
||||
}
|
||||
99
packages/db_helper_sqlite/src/user-apis/cart.ts
Normal file
99
packages/db_helper_sqlite/src/user-apis/cart.ts
Normal 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))
|
||||
}
|
||||
45
packages/db_helper_sqlite/src/user-apis/complaint.ts
Normal file
45
packages/db_helper_sqlite/src/user-apis/complaint.ts
Normal 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,
|
||||
})
|
||||
}
|
||||
146
packages/db_helper_sqlite/src/user-apis/coupon.ts
Normal file
146
packages/db_helper_sqlite/src/user-apis/coupon.ts
Normal 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)
|
||||
}
|
||||
738
packages/db_helper_sqlite/src/user-apis/order.ts
Normal file
738
packages/db_helper_sqlite/src/user-apis/order.ts
Normal 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>
|
||||
}
|
||||
51
packages/db_helper_sqlite/src/user-apis/payments.ts
Normal file
51
packages/db_helper_sqlite/src/user-apis/payments.ts
Normal 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))
|
||||
}
|
||||
271
packages/db_helper_sqlite/src/user-apis/product.ts
Normal file
271
packages/db_helper_sqlite/src/user-apis/product.ts
Normal 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
|
||||
}
|
||||
46
packages/db_helper_sqlite/src/user-apis/slots.ts
Normal file
46
packages/db_helper_sqlite/src/user-apis/slots.ts
Normal 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,
|
||||
}))
|
||||
}
|
||||
141
packages/db_helper_sqlite/src/user-apis/stores.ts
Normal file
141
packages/db_helper_sqlite/src/user-apis/stores.ts
Normal 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,
|
||||
},
|
||||
})
|
||||
}
|
||||
28
packages/db_helper_sqlite/src/user-apis/tags.ts
Normal file
28
packages/db_helper_sqlite/src/user-apis/tags.ts
Normal 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,
|
||||
// },
|
||||
// },
|
||||
},
|
||||
})
|
||||
}
|
||||
75
packages/db_helper_sqlite/src/user-apis/user.ts
Normal file
75
packages/db_helper_sqlite/src/user-apis/user.ts
Normal 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(),
|
||||
})
|
||||
}
|
||||
16
packages/db_helper_sqlite/tsconfig.json
Normal file
16
packages/db_helper_sqlite/tsconfig.json
Normal 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/**/*"]
|
||||
}
|
||||
|
|
@ -363,6 +363,7 @@ export interface AdminProduct {
|
|||
price: string;
|
||||
marketPrice: string | null;
|
||||
images: string[] | null;
|
||||
imageKeys: string[] | null;
|
||||
isOutOfStock: boolean;
|
||||
isSuspended: boolean;
|
||||
isFlashAvailable: boolean;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
98
packages/ui/src/components/ImageUploaderNeo.tsx
Normal file
98
packages/ui/src/components/ImageUploaderNeo.tsx
Normal 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
|
||||
Loading…
Add table
Reference in a new issue