enh
This commit is contained in:
parent
5e9bc3e38e
commit
ca7d8df1c8
89 changed files with 10704 additions and 1148 deletions
|
|
@ -1,62 +1,52 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Alert } from 'react-native';
|
import { Alert } from 'react-native';
|
||||||
import { AppContainer } from 'common-ui';
|
import { AppContainer, ImageUploaderNeoPayload } from 'common-ui';
|
||||||
import ProductForm from '@/src/components/ProductForm';
|
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() {
|
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 handleSubmit = async (values: any, images: ImageUploaderNeoPayload[]) => {
|
||||||
const payload: CreateProductPayload = {
|
try {
|
||||||
name: values.name,
|
let uploadUrls: string[] = [];
|
||||||
shortDescription: values.shortDescription,
|
|
||||||
longDescription: values.longDescription,
|
|
||||||
unitId: parseInt(values.unitId),
|
|
||||||
storeId: parseInt(values.storeId),
|
|
||||||
price: parseFloat(values.price),
|
|
||||||
marketPrice: values.marketPrice ? parseFloat(values.marketPrice) : undefined,
|
|
||||||
incrementStep: 1,
|
|
||||||
productQuantity: values.productQuantity || 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
const formData = new FormData();
|
if (images.length > 0) {
|
||||||
Object.entries(payload).forEach(([key, value]) => {
|
const blobs = await Promise.all(
|
||||||
if (value !== undefined && value !== null) {
|
images.map(async (img) => {
|
||||||
formData.append(key, value as string);
|
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;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Append tag IDs
|
await createProduct.mutateAsync({
|
||||||
if (values.tagIds && values.tagIds.length > 0) {
|
name: values.name,
|
||||||
values.tagIds.forEach((tagId: number) => {
|
shortDescription: values.shortDescription,
|
||||||
formData.append('tagIds', tagId.toString());
|
longDescription: values.longDescription,
|
||||||
|
unitId: parseInt(values.unitId),
|
||||||
|
storeId: parseInt(values.storeId),
|
||||||
|
price: parseFloat(values.price),
|
||||||
|
marketPrice: values.marketPrice ? parseFloat(values.marketPrice) : undefined,
|
||||||
|
incrementStep: 1,
|
||||||
|
productQuantity: values.productQuantity || 1,
|
||||||
|
isSuspended: values.isSuspended || false,
|
||||||
|
isFlashAvailable: values.isFlashAvailable || false,
|
||||||
|
flashPrice: values.flashPrice ? parseFloat(values.flashPrice) : undefined,
|
||||||
|
uploadUrls,
|
||||||
|
tagIds: values.tagIds || [],
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// Append images
|
Alert.alert('Success', 'Product created successfully!');
|
||||||
if (images) {
|
} catch (error: any) {
|
||||||
images.forEach((image, index) => {
|
Alert.alert('Error', error.message || 'Failed to create product');
|
||||||
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) => {
|
|
||||||
Alert.alert('Error', error.message || 'Failed to create product');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
|
|
@ -81,8 +71,7 @@ export default function AddProduct() {
|
||||||
mode="create"
|
mode="create"
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
isLoading={isCreating}
|
isLoading={createProduct.isPending || isUploading}
|
||||||
existingImages={[]}
|
|
||||||
/>
|
/>
|
||||||
</AppContainer>
|
</AppContainer>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { tw, AppContainer, MyText, useMarkDataFetchers, BottomDialog, ImageUploa
|
||||||
import { MaterialIcons, FontAwesome5, Ionicons, Feather, MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialIcons, FontAwesome5, Ionicons, Feather, MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { trpc } from '@/src/trpc-client';
|
import { trpc } from '@/src/trpc-client';
|
||||||
import usePickImage from 'common-ui/src/components/use-pick-image';
|
import usePickImage from 'common-ui/src/components/use-pick-image';
|
||||||
|
import { useUploadToObjectStorage } from '@/hooks/useUploadToObjectStore';
|
||||||
import { Formik } from 'formik';
|
import { Formik } from 'formik';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { BlurView } from 'expo-blur';
|
import { BlurView } from 'expo-blur';
|
||||||
|
|
@ -23,10 +24,9 @@ const ReviewResponseForm: React.FC<ReviewResponseFormProps> = ({ reviewId, onClo
|
||||||
const [adminResponse, setAdminResponse] = useState('');
|
const [adminResponse, setAdminResponse] = useState('');
|
||||||
const [selectedImages, setSelectedImages] = useState<{ blob: Blob; mimeType: string }[]>([]);
|
const [selectedImages, setSelectedImages] = useState<{ blob: Blob; mimeType: string }[]>([]);
|
||||||
const [displayImages, setDisplayImages] = useState<{ uri?: string }[]>([]);
|
const [displayImages, setDisplayImages] = useState<{ uri?: string }[]>([]);
|
||||||
const [uploadUrls, setUploadUrls] = useState<string[]>([]);
|
|
||||||
|
|
||||||
const respondToReview = trpc.admin.product.respondToReview.useMutation();
|
const respondToReview = trpc.admin.product.respondToReview.useMutation();
|
||||||
const generateUploadUrls = trpc.user.fileUpload.generateUploadUrls.useMutation();
|
const { upload } = useUploadToObjectStorage();
|
||||||
|
|
||||||
const handleImagePick = usePickImage({
|
const handleImagePick = usePickImage({
|
||||||
setFile: async (assets: any) => {
|
setFile: async (assets: any) => {
|
||||||
|
|
@ -62,37 +62,16 @@ const ReviewResponseForm: React.FC<ReviewResponseFormProps> = ({ reviewId, onClo
|
||||||
|
|
||||||
const handleSubmit = async (adminResponse: string) => {
|
const handleSubmit = async (adminResponse: string) => {
|
||||||
try {
|
try {
|
||||||
const mimeTypes = selectedImages.map(s => s.mimeType);
|
const { keys, presignedUrls } = await upload({
|
||||||
const { uploadUrls: generatedUrls } = await generateUploadUrls.mutateAsync({
|
images: selectedImages,
|
||||||
contextString: 'review',
|
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({
|
await respondToReview.mutateAsync({
|
||||||
reviewId,
|
reviewId,
|
||||||
adminResponse,
|
adminResponse,
|
||||||
adminResponseImages: keys,
|
adminResponseImages: keys,
|
||||||
uploadUrls: generatedUrls,
|
uploadUrls: presignedUrls,
|
||||||
});
|
});
|
||||||
|
|
||||||
Alert.alert('Success', 'Response submitted');
|
Alert.alert('Success', 'Response submitted');
|
||||||
|
|
@ -100,9 +79,7 @@ const ReviewResponseForm: React.FC<ReviewResponseFormProps> = ({ reviewId, onClo
|
||||||
setAdminResponse('');
|
setAdminResponse('');
|
||||||
setSelectedImages([]);
|
setSelectedImages([]);
|
||||||
setDisplayImages([]);
|
setDisplayImages([]);
|
||||||
setUploadUrls([]);
|
|
||||||
} catch (error:any) {
|
} catch (error:any) {
|
||||||
|
|
||||||
Alert.alert('Error', error.message || 'Failed to submit response.');
|
Alert.alert('Error', error.message || 'Failed to submit response.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,95 +1,69 @@
|
||||||
import React, { useRef } from 'react';
|
import React, { useRef } from 'react';
|
||||||
import { View, Text, Alert } from 'react-native';
|
import { View, Alert } from 'react-native';
|
||||||
import { useLocalSearchParams } from 'expo-router';
|
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 ProductForm, { ProductFormRef } from '@/src/components/ProductForm';
|
||||||
import { useUpdateProduct } from '@/src/api-hooks/product.api';
|
|
||||||
import { trpc } from '@/src/trpc-client';
|
import { trpc } from '@/src/trpc-client';
|
||||||
|
import { useUploadToObjectStorage } from '@/hooks/useUploadToObjectStore';
|
||||||
|
|
||||||
export default function EditProduct() {
|
export default function EditProduct() {
|
||||||
const { id } = useLocalSearchParams();
|
const { id } = useLocalSearchParams();
|
||||||
const productId = Number(id);
|
const productId = Number(id);
|
||||||
const productFormRef = useRef<ProductFormRef>(null);
|
const productFormRef = useRef<ProductFormRef>(null);
|
||||||
|
|
||||||
// const { data: product, isLoading: isFetching, refetch } = useGetProduct(productId);
|
|
||||||
const { data: product, isLoading: isFetching, refetch } = trpc.admin.product.getProductById.useQuery(
|
const { data: product, isLoading: isFetching, refetch } = trpc.admin.product.getProductById.useQuery(
|
||||||
{ id: productId },
|
{ id: productId },
|
||||||
{ enabled: !!productId }
|
{ enabled: !!productId }
|
||||||
);
|
);
|
||||||
//
|
|
||||||
const { mutate: updateProduct, isPending: isUpdating } = useUpdateProduct();
|
const updateProduct = trpc.admin.product.updateProduct.useMutation();
|
||||||
|
const { upload, isUploading } = useUploadToObjectStorage();
|
||||||
|
|
||||||
useManualRefresh(() => refetch());
|
useManualRefresh(() => refetch());
|
||||||
|
|
||||||
const handleSubmit = (values: any, newImages?: { uri?: string }[], imagesToDelete?: string[]) => {
|
const handleSubmit = async (values: any, images: ImageUploaderNeoPayload[], imagesToDelete: string[]) => {
|
||||||
const payload = {
|
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,
|
name: values.name,
|
||||||
shortDescription: values.shortDescription,
|
shortDescription: values.shortDescription,
|
||||||
longDescription: values.longDescription,
|
longDescription: values.longDescription,
|
||||||
unitId: parseInt(values.unitId),
|
unitId: parseInt(values.unitId),
|
||||||
storeId: parseInt(values.storeId),
|
storeId: parseInt(values.storeId),
|
||||||
price: parseFloat(values.price),
|
price: parseFloat(values.price),
|
||||||
marketPrice: values.marketPrice ? parseFloat(values.marketPrice) : undefined,
|
marketPrice: values.marketPrice ? parseFloat(values.marketPrice) : undefined,
|
||||||
incrementStep: 1,
|
incrementStep: 1,
|
||||||
productQuantity: values.productQuantity || 1,
|
productQuantity: values.productQuantity || 1,
|
||||||
deals: values.deals?.filter((deal: any) =>
|
isSuspended: values.isSuspended || false,
|
||||||
deal.quantity && deal.price && deal.validTill
|
isFlashAvailable: values.isFlashAvailable || false,
|
||||||
).map((deal: any) => ({
|
flashPrice: values.flashPrice ? parseFloat(values.flashPrice) : null,
|
||||||
quantity: parseInt(deal.quantity),
|
uploadUrls,
|
||||||
price: parseFloat(deal.price),
|
imagesToDelete,
|
||||||
validTill: deal.validTill instanceof Date
|
tagIds: values.tagIds || [],
|
||||||
? 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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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
|
Alert.alert('Success', 'Product updated successfully!');
|
||||||
if (imagesToDelete && imagesToDelete.length > 0) {
|
productFormRef.current?.clearImages();
|
||||||
formData.append('imagesToDelete', JSON.stringify(imagesToDelete));
|
} catch (error: any) {
|
||||||
|
Alert.alert('Error', error.message || 'Failed to update product');
|
||||||
}
|
}
|
||||||
|
|
||||||
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) => {
|
|
||||||
Alert.alert('Error', error.message || 'Failed to update product');
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isFetching) {
|
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 = {
|
const initialValues = {
|
||||||
name: productData.name,
|
name: productData.name,
|
||||||
|
|
@ -125,7 +105,7 @@ export default function EditProduct() {
|
||||||
deals: productData.deals?.map(deal => ({
|
deals: productData.deals?.map(deal => ({
|
||||||
quantity: deal.quantity,
|
quantity: deal.quantity,
|
||||||
price: deal.price,
|
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 }],
|
})) || [{ quantity: '', price: '', validTill: null }],
|
||||||
tagIds: productData.tags?.map((tag: any) => tag.id) || [],
|
tagIds: productData.tags?.map((tag: any) => tag.id) || [],
|
||||||
isSuspended: productData.isSuspended || false,
|
isSuspended: productData.isSuspended || false,
|
||||||
|
|
@ -141,8 +121,9 @@ export default function EditProduct() {
|
||||||
mode="edit"
|
mode="edit"
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
isLoading={isUpdating}
|
isLoading={updateProduct.isPending || isUploading}
|
||||||
existingImages={productData.images || []}
|
existingImages={existingImages}
|
||||||
|
existingImageKeys={existingImageKeys}
|
||||||
/>
|
/>
|
||||||
</AppContainer>
|
</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 { AppContainer, MyText, tw, MyButton, useManualRefresh, MyTextInput, SearchBar, useMarkDataFetchers } from 'common-ui';
|
||||||
|
|
||||||
import { trpc } from '@/src/trpc-client';
|
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';
|
type FilterType = 'all' | 'in-stock' | 'out-of-stock';
|
||||||
|
|
||||||
|
|
@ -54,7 +54,7 @@ export default function Products() {
|
||||||
|
|
||||||
|
|
||||||
// const handleToggleStock = (product: any) => {
|
// 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';
|
const action = product.isOutOfStock ? 'mark as in stock' : 'mark as out of stock';
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
'Update Stock Status',
|
'Update Stock Status',
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import {
|
||||||
} from 'common-ui';
|
} from 'common-ui';
|
||||||
import { trpc } from '@/src/trpc-client';
|
import { trpc } from '@/src/trpc-client';
|
||||||
import usePickImage from 'common-ui/src/components/use-pick-image';
|
import usePickImage from 'common-ui/src/components/use-pick-image';
|
||||||
|
import { useUploadToObjectStorage } from '@/hooks/useUploadToObjectStore';
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -26,12 +27,6 @@ interface User {
|
||||||
isEligibleForNotif: boolean;
|
isEligibleForNotif: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const extractKeyFromUrl = (url: string): string => {
|
|
||||||
const u = new URL(url);
|
|
||||||
const rawKey = u.pathname.replace(/^\/+/, '');
|
|
||||||
return decodeURIComponent(rawKey);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function SendNotifications() {
|
export default function SendNotifications() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [selectedUserIds, setSelectedUserIds] = useState<number[]>([]);
|
const [selectedUserIds, setSelectedUserIds] = useState<number[]>([]);
|
||||||
|
|
@ -46,8 +41,7 @@ export default function SendNotifications() {
|
||||||
search: searchQuery,
|
search: searchQuery,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Generate upload URLs mutation
|
const { uploadSingle } = useUploadToObjectStorage();
|
||||||
const generateUploadUrls = trpc.user.fileUpload.generateUploadUrls.useMutation();
|
|
||||||
|
|
||||||
// Send notification mutation
|
// Send notification mutation
|
||||||
const sendNotification = trpc.admin.user.sendNotification.useMutation({
|
const sendNotification = trpc.admin.user.sendNotification.useMutation({
|
||||||
|
|
@ -127,28 +121,8 @@ export default function SendNotifications() {
|
||||||
|
|
||||||
// Upload image if selected
|
// Upload image if selected
|
||||||
if (selectedImage) {
|
if (selectedImage) {
|
||||||
const { uploadUrls } = await generateUploadUrls.mutateAsync({
|
const { key } = await uploadSingle(selectedImage.blob, selectedImage.mimeType, 'notification');
|
||||||
contextString: 'notification',
|
imageUrl = key;
|
||||||
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}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send notification
|
// Send notification
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { DropdownOption } from 'common-ui/src/components/bottom-dropdown';
|
||||||
import ProductsSelector from './ProductsSelector';
|
import ProductsSelector from './ProductsSelector';
|
||||||
import { trpc } from '../src/trpc-client';
|
import { trpc } from '../src/trpc-client';
|
||||||
import usePickImage from 'common-ui/src/components/use-pick-image';
|
import usePickImage from 'common-ui/src/components/use-pick-image';
|
||||||
|
import { useUploadToObjectStorage } from '../hooks/useUploadToObjectStore';
|
||||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||||
|
|
||||||
export interface BannerFormData {
|
export interface BannerFormData {
|
||||||
|
|
@ -52,10 +53,10 @@ export default function BannerForm({
|
||||||
const [selectedImages, setSelectedImages] = useState<{ blob: Blob; mimeType: string }[]>([]);
|
const [selectedImages, setSelectedImages] = useState<{ blob: Blob; mimeType: string }[]>([]);
|
||||||
const [displayImages, setDisplayImages] = useState<{ uri?: string }[]>([]);
|
const [displayImages, setDisplayImages] = useState<{ uri?: string }[]>([]);
|
||||||
|
|
||||||
const generateUploadUrls = trpc.common.generateUploadUrls.useMutation();
|
const { uploadSingle } = useUploadToObjectStorage();
|
||||||
|
|
||||||
// Fetch products for dropdown
|
// Fetch products for dropdown
|
||||||
const { data: productsData } = trpc.common.product.getAllProductsSummary.useQuery({});
|
const { data: productsData } = trpc.common.product.getAllProductsSummary.useQuery();
|
||||||
const products = productsData?.products || [];
|
const products = productsData?.products || [];
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -97,33 +98,11 @@ export default function BannerForm({
|
||||||
let imageUrl: string | undefined;
|
let imageUrl: string | undefined;
|
||||||
|
|
||||||
if (selectedImages.length > 0) {
|
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 { blob, mimeType } = selectedImages[0];
|
||||||
|
const { presignedUrl } = await uploadSingle(blob, mimeType, 'store');
|
||||||
const uploadResponse = await fetch(uploadUrl, {
|
imageUrl = presignedUrl;
|
||||||
method: 'PUT',
|
|
||||||
body: blob,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': mimeType,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!uploadResponse.ok) {
|
|
||||||
throw new Error(`Upload failed with status ${uploadResponse.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
imageUrl = uploadUrl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call onSubmit with form values and imageUrl
|
|
||||||
await onSubmit(values, imageUrl);
|
await onSubmit(values, imageUrl);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Upload error:', error);
|
console.error('Upload error:', error);
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { MyTextInput, BottomDropdown, MyText, tw, ImageUploader } from 'common-u
|
||||||
import ProductsSelector from './ProductsSelector';
|
import ProductsSelector from './ProductsSelector';
|
||||||
import { trpc } from '../src/trpc-client';
|
import { trpc } from '../src/trpc-client';
|
||||||
import usePickImage from 'common-ui/src/components/use-pick-image';
|
import usePickImage from 'common-ui/src/components/use-pick-image';
|
||||||
|
import { useUploadToObjectStorage } from '../hooks/useUploadToObjectStore';
|
||||||
|
|
||||||
export interface StoreFormData {
|
export interface StoreFormData {
|
||||||
name: string;
|
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({
|
const handleImagePick = usePickImage({
|
||||||
setFile: async (assets: any) => {
|
setFile: async (assets: any) => {
|
||||||
|
|
@ -113,39 +114,11 @@ const StoreForm = forwardRef<StoreFormRef, StoreFormProps>((props, ref) => {
|
||||||
let imageUrl: string | undefined;
|
let imageUrl: string | undefined;
|
||||||
|
|
||||||
if (selectedImages.length > 0) {
|
if (selectedImages.length > 0) {
|
||||||
// Generate upload URLs
|
const { blob, mimeType } = selectedImages[0];
|
||||||
const mimeTypes = selectedImages.map(s => s.mimeType);
|
const { presignedUrl } = await uploadSingle(blob, mimeType, 'store');
|
||||||
const { uploadUrls } = await generateUploadUrls.mutateAsync({
|
imageUrl = presignedUrl;
|
||||||
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}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 });
|
onSubmit({ ...values, imageUrl });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Upload error:', error);
|
console.error('Upload error:', error);
|
||||||
|
|
@ -204,11 +177,11 @@ const StoreForm = forwardRef<StoreFormRef, StoreFormProps>((props, ref) => {
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={submit}
|
onPress={submit}
|
||||||
disabled={isLoading || generateUploadUrls.isPending}
|
disabled={isLoading || isUploading}
|
||||||
style={tw`px-4 py-2 rounded-lg shadow-lg items-center mt-2 ${isLoading || generateUploadUrls.isPending ? 'bg-gray-400' : 'bg-blue-500'}`}
|
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`}>
|
<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>
|
</MyText>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</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 { View, TouchableOpacity } from 'react-native';
|
||||||
import { Image } from 'expo-image';
|
|
||||||
import { Formik, FieldArray } from 'formik';
|
import { Formik, FieldArray } from 'formik';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import { MyTextInput, BottomDropdown, MyText, ImageUploader, ImageGalleryWithDelete, useTheme, DatePicker, tw, useFocusCallback, Checkbox } from 'common-ui';
|
import { MyTextInput, BottomDropdown, MyText, useTheme, DatePicker, tw, useFocusCallback, Checkbox, ImageUploaderNeo, ImageUploaderNeoItem, ImageUploaderNeoPayload } from 'common-ui';
|
||||||
import usePickImage from 'common-ui/src/components/use-pick-image';
|
|
||||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||||
import { trpc } from '../trpc-client';
|
import { trpc } from '../trpc-client';
|
||||||
import { useGetTags } from '../api-hooks/tag.api';
|
import { useGetTags } from '../api-hooks/tag.api';
|
||||||
|
|
@ -38,9 +36,10 @@ export interface ProductFormRef {
|
||||||
interface ProductFormProps {
|
interface ProductFormProps {
|
||||||
mode: 'create' | 'edit';
|
mode: 'create' | 'edit';
|
||||||
initialValues: ProductFormData;
|
initialValues: ProductFormData;
|
||||||
onSubmit: (values: ProductFormData, images?: { uri?: string }[], imagesToDelete?: string[]) => void;
|
onSubmit: (values: ProductFormData, images: ImageUploaderNeoPayload[], imagesToDelete: string[]) => void;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
existingImages?: string[];
|
existingImages?: ImageUploaderNeoItem[];
|
||||||
|
existingImageKeys?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const unitOptions = [
|
const unitOptions = [
|
||||||
|
|
@ -50,18 +49,21 @@ const unitOptions = [
|
||||||
{ label: 'Unit Piece', value: 4 },
|
{ label: 'Unit Piece', value: 4 },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
||||||
mode,
|
mode,
|
||||||
initialValues,
|
initialValues,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
isLoading,
|
isLoading,
|
||||||
existingImages = []
|
existingImages = [],
|
||||||
|
existingImageKeys = [],
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const [images, setImages] = useState<{ uri?: string }[]>([]);
|
const [images, setImages] = useState<ImageUploaderNeoItem[]>(existingImages);
|
||||||
const [existingImagesState, setExistingImagesState] = useState<string[]>(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 { data: storesData } = trpc.common.getStoresSummary.useQuery();
|
||||||
const storeOptions = storesData?.stores.map(store => ({
|
const storeOptions = storesData?.stores.map(store => ({
|
||||||
|
|
@ -75,38 +77,44 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
||||||
value: tag.id.toString(),
|
value: tag.id.toString(),
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
// Initialize existing images state when existingImages prop changes
|
// Build signed URL -> S3 key mapping for existing images
|
||||||
useEffect(() => {
|
const signedUrlToKey = useMemo(() => {
|
||||||
console.log('changing existing imaes statte')
|
const map: Record<string, string> = {};
|
||||||
|
existingImages.forEach((img, i) => {
|
||||||
setExistingImagesState(existingImages);
|
if (existingImageKeys[i]) {
|
||||||
}, [existingImages]);
|
map[img.imgUrl] = existingImageKeys[i];
|
||||||
|
}
|
||||||
const pickImage = usePickImage({
|
});
|
||||||
setFile: (files) => setImages(prev => [...prev, ...files]),
|
return map;
|
||||||
multiple: true,
|
}, [existingImages, existingImageKeys]);
|
||||||
});
|
|
||||||
|
|
||||||
// Calculate which existing images were deleted
|
|
||||||
const deletedImages = existingImages.filter(img => !existingImagesState.includes(img));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={initialValues}
|
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
|
enableReinitialize
|
||||||
>
|
>
|
||||||
{({ handleChange, handleSubmit, values, setFieldValue, resetForm }) => {
|
{({ handleChange, handleSubmit, values, setFieldValue, resetForm }) => {
|
||||||
// Clear form when screen comes into focus
|
|
||||||
const clearForm = useCallback(() => {
|
const clearForm = useCallback(() => {
|
||||||
setImages([]);
|
setImages([]);
|
||||||
setExistingImagesState([]);
|
|
||||||
resetForm();
|
resetForm();
|
||||||
}, [resetForm]);
|
}, [resetForm]);
|
||||||
|
|
||||||
useFocusCallback(clearForm);
|
useFocusCallback(clearForm);
|
||||||
|
|
||||||
// Update ref with current clearForm function
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
clearImages: clearForm,
|
clearImages: clearForm,
|
||||||
}), [clearForm]);
|
}), [clearForm]);
|
||||||
|
|
@ -141,44 +149,18 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
||||||
style={{ marginBottom: 16 }}
|
style={{ marginBottom: 16 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{mode === 'create' && (
|
<ImageUploaderNeo
|
||||||
<ImageUploader
|
images={images}
|
||||||
images={images}
|
onImageAdd={(payloads) => setImages(prev => [...prev, ...payloads.map(p => ({ imgUrl: p.url, mimeType: p.mimeType }))])}
|
||||||
onAddImage={pickImage}
|
onImageRemove={(payload) => setImages(prev => prev.filter(img => img.imgUrl !== payload.url))}
|
||||||
onRemoveImage={(uri) => setImages(prev => prev.filter(img => img.uri !== uri))}
|
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
|
<BottomDropdown
|
||||||
topLabel='Unit'
|
topLabel='Unit'
|
||||||
label="Unit"
|
label="Unit"
|
||||||
value={values.unitId}
|
value={values.unitId}
|
||||||
options={unitOptions}
|
options={unitOptions}
|
||||||
// onValueChange={(value) => handleChange('unitId')(value+'')}
|
|
||||||
onValueChange={(value) => setFieldValue('unitId', value)}
|
onValueChange={(value) => setFieldValue('unitId', value)}
|
||||||
placeholder="Select unit"
|
placeholder="Select unit"
|
||||||
style={{ marginBottom: 16 }}
|
style={{ marginBottom: 16 }}
|
||||||
|
|
@ -188,18 +170,7 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
||||||
placeholder="Enter product quantity"
|
placeholder="Enter product quantity"
|
||||||
keyboardType="numeric"
|
keyboardType="numeric"
|
||||||
value={values.productQuantity.toString()}
|
value={values.productQuantity.toString()}
|
||||||
onChangeText={(text) => {
|
onChangeText={(text) => setFieldValue('productQuantity', 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);
|
|
||||||
// }
|
|
||||||
}}
|
|
||||||
style={{ marginBottom: 16 }}
|
style={{ marginBottom: 16 }}
|
||||||
/>
|
/>
|
||||||
<BottomDropdown
|
<BottomDropdown
|
||||||
|
|
@ -238,8 +209,6 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
||||||
style={{ marginBottom: 16 }}
|
style={{ marginBottom: 16 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<View style={tw`flex-row items-center mb-4`}>
|
<View style={tw`flex-row items-center mb-4`}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={values.isSuspended}
|
checked={values.isSuspended}
|
||||||
|
|
@ -254,7 +223,7 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
||||||
checked={values.isFlashAvailable}
|
checked={values.isFlashAvailable}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setFieldValue('isFlashAvailable', !values.isFlashAvailable);
|
setFieldValue('isFlashAvailable', !values.isFlashAvailable);
|
||||||
if (values.isFlashAvailable) setFieldValue('flashPrice', ''); // Clear price when disabled
|
if (values.isFlashAvailable) setFieldValue('flashPrice', '');
|
||||||
}}
|
}}
|
||||||
style={tw`mr-3`}
|
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
|
<TouchableOpacity
|
||||||
onPress={submit}
|
onPress={submit}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import 'dotenv/config';
|
||||||
import express, { NextFunction, Request, Response } from "express";
|
import express, { NextFunction, Request, Response } from "express";
|
||||||
import cors from "cors";
|
import cors from "cors";
|
||||||
// import bodyParser from "body-parser";
|
// import bodyParser from "body-parser";
|
||||||
import multer from "multer";
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { getStaffUserById, getUserDetailsByUserId, isUserSuspended } from '@/src/dbService';
|
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 v1Router from "@/src/v1-router"
|
||||||
import testController from "@/src/test-controller"
|
import testController from "@/src/test-controller"
|
||||||
import { authenticateUser } from "@/src/middleware/auth.middleware"
|
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();
|
const router = Router();
|
||||||
|
|
@ -34,12 +32,6 @@ router.use('/v1', v1Router);
|
||||||
// router.use('/av', avRouter);
|
// router.use('/av', avRouter);
|
||||||
router.use('/test', testController);
|
router.use('/test', testController);
|
||||||
|
|
||||||
// User REST APIs
|
|
||||||
router.post('/uv/complaints/raise',
|
|
||||||
uploadHandler.array('images', 5),
|
|
||||||
raiseComplaint
|
|
||||||
);
|
|
||||||
|
|
||||||
// Global error handling middleware
|
// Global error handling middleware
|
||||||
router.use((err: Error, req: Request, res: Response, next: NextFunction) => {
|
router.use((err: Error, req: Request, res: Response, next: NextFunction) => {
|
||||||
console.error('Error:', err);
|
console.error('Error:', err);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
|
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { ApiError } from '@/src/lib/api-error'
|
import { ApiError } from '@/src/lib/api-error'
|
||||||
import { 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 { scheduleStoreInitialization } from '@/src/stores/store-initializer'
|
||||||
import {
|
import {
|
||||||
getAllProducts as getAllProductsInDb,
|
getAllProducts as getAllProductsInDb,
|
||||||
|
|
@ -18,8 +18,18 @@ import {
|
||||||
updateProductGroup as updateProductGroupInDb,
|
updateProductGroup as updateProductGroupInDb,
|
||||||
deleteProductGroup as deleteProductGroupInDb,
|
deleteProductGroup as deleteProductGroupInDb,
|
||||||
updateProductPrices as updateProductPricesInDb,
|
updateProductPrices as updateProductPricesInDb,
|
||||||
|
checkProductExistsByName,
|
||||||
|
checkUnitExists,
|
||||||
|
createProduct as createProductInDb,
|
||||||
|
createSpecialDealsForProduct,
|
||||||
|
replaceProductTags,
|
||||||
|
getProductImagesById,
|
||||||
|
updateProduct as updateProductInDb,
|
||||||
|
updateProductDeals,
|
||||||
} from '@/src/dbService'
|
} from '@/src/dbService'
|
||||||
import type {
|
import type {
|
||||||
|
AdminProduct,
|
||||||
|
AdminSpecialDeal,
|
||||||
AdminProductGroupsResult,
|
AdminProductGroupsResult,
|
||||||
AdminProductGroupResponse,
|
AdminProductGroupResponse,
|
||||||
AdminProductReviewsResult,
|
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
|
updateSlotProducts: protectedProcedure
|
||||||
.input(z.object({
|
.input(z.object({
|
||||||
slotId: z.string(),
|
slotId: z.string(),
|
||||||
|
|
@ -484,7 +656,7 @@ export const productRouter = router({
|
||||||
groups: groups.map(group => ({
|
groups: groups.map(group => ({
|
||||||
...group,
|
...group,
|
||||||
products: group.memberships.map(m => ({
|
products: group.memberships.map(m => ({
|
||||||
...m.product,
|
...(m.product as AdminProduct),
|
||||||
images: (m.product.images as string[]) || null,
|
images: (m.product.images as string[]) || null,
|
||||||
})),
|
})),
|
||||||
productCount: group.memberships.length,
|
productCount: group.memberships.length,
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ export const commonApiRouter = router({
|
||||||
|
|
||||||
generateUploadUrls: protectedProcedure
|
generateUploadUrls: protectedProcedure
|
||||||
.input(z.object({
|
.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()),
|
mimeTypes: z.array(z.string()),
|
||||||
}))
|
}))
|
||||||
.mutation(async ({ input }): Promise<{ uploadUrls: string[] }> => {
|
.mutation(async ({ input }): Promise<{ uploadUrls: string[] }> => {
|
||||||
|
|
@ -102,6 +102,10 @@ export const commonApiRouter = router({
|
||||||
folder = 'store-images';
|
folder = 'store-images';
|
||||||
} else if (contextString === 'review_response') {
|
} else if (contextString === 'review_response') {
|
||||||
folder = 'review-response-images';
|
folder = 'review-response-images';
|
||||||
|
} else if (contextString === 'complaint') {
|
||||||
|
folder = 'complaint-images';
|
||||||
|
} else if (contextString === 'profile') {
|
||||||
|
folder = 'profile-images';
|
||||||
} else {
|
} else {
|
||||||
folder = '';
|
folder = '';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,12 @@ import {
|
||||||
getUserAuthById as getUserAuthByIdInDb,
|
getUserAuthById as getUserAuthByIdInDb,
|
||||||
getUserAuthCreds as getUserAuthCredsInDb,
|
getUserAuthCreds as getUserAuthCredsInDb,
|
||||||
getUserAuthDetails as getUserAuthDetailsInDb,
|
getUserAuthDetails as getUserAuthDetailsInDb,
|
||||||
createUserAuthWithCreds as createUserAuthWithCredsInDb,
|
|
||||||
createUserAuthWithMobile as createUserAuthWithMobileInDb,
|
createUserAuthWithMobile as createUserAuthWithMobileInDb,
|
||||||
upsertUserAuthPassword as upsertUserAuthPasswordInDb,
|
upsertUserAuthPassword as upsertUserAuthPasswordInDb,
|
||||||
deleteUserAuthAccount as deleteUserAuthAccountInDb,
|
deleteUserAuthAccount as deleteUserAuthAccountInDb,
|
||||||
|
createUserWithProfile as createUserWithProfileInDb,
|
||||||
|
updateUserProfile as updateUserProfileInDb,
|
||||||
|
getUserDetailsByUserId as getUserDetailsByUserIdInDb,
|
||||||
} from '@/src/dbService'
|
} from '@/src/dbService'
|
||||||
import type {
|
import type {
|
||||||
UserAuthResult,
|
UserAuthResult,
|
||||||
|
|
@ -36,6 +38,7 @@ interface RegisterRequest {
|
||||||
email: string;
|
email: string;
|
||||||
mobile: string;
|
mobile: string;
|
||||||
password: string;
|
password: string;
|
||||||
|
profileImageUrl?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateToken = (userId: number): string => {
|
const generateToken = (userId: number): string => {
|
||||||
|
|
@ -127,9 +130,10 @@ export const authRouter = router({
|
||||||
email: z.string().email('Invalid email format'),
|
email: z.string().email('Invalid email format'),
|
||||||
mobile: z.string().min(1, 'Mobile is required'),
|
mobile: z.string().min(1, 'Mobile is required'),
|
||||||
password: z.string().min(1, 'Password is required'),
|
password: z.string().min(1, 'Password is required'),
|
||||||
|
profileImageUrl: z.string().nullable().optional(),
|
||||||
}))
|
}))
|
||||||
.mutation(async ({ input }): Promise<UserAuthResult> => {
|
.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) {
|
if (!name || !email || !mobile || !password) {
|
||||||
throw new ApiError('All fields are required', 400);
|
throw new ApiError('All fields are required', 400);
|
||||||
|
|
@ -165,15 +169,20 @@ export const authRouter = router({
|
||||||
const hashedPassword = await bcrypt.hash(password, 12);
|
const hashedPassword = await bcrypt.hash(password, 12);
|
||||||
|
|
||||||
// Create user and credentials in a transaction
|
// Create user and credentials in a transaction
|
||||||
const newUser = await createUserAuthWithCredsInDb({
|
const newUser = await createUserWithProfileInDb({
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
email: email.toLowerCase().trim(),
|
email: email.toLowerCase().trim(),
|
||||||
mobile: cleanMobile,
|
mobile: cleanMobile,
|
||||||
hashedPassword,
|
hashedPassword,
|
||||||
|
profileImage: profileImageUrl ?? null,
|
||||||
})
|
})
|
||||||
|
|
||||||
const token = generateToken(newUser.id);
|
const token = generateToken(newUser.id);
|
||||||
|
|
||||||
|
const profileImageSignedUrl = profileImageUrl
|
||||||
|
? await generateSignedUrlFromS3Url(profileImageUrl)
|
||||||
|
: null
|
||||||
|
|
||||||
const response: UserAuthResponse = {
|
const response: UserAuthResponse = {
|
||||||
token,
|
token,
|
||||||
user: {
|
user: {
|
||||||
|
|
@ -182,7 +191,7 @@ export const authRouter = router({
|
||||||
email: newUser.email,
|
email: newUser.email,
|
||||||
mobile: newUser.mobile,
|
mobile: newUser.mobile,
|
||||||
createdAt: newUser.createdAt.toISOString(),
|
createdAt: newUser.createdAt.toISOString(),
|
||||||
profileImage: null,
|
profileImage: profileImageSignedUrl,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -278,6 +287,102 @@ export const authRouter = router({
|
||||||
return { success: true, message: 'Password updated successfully' }
|
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
|
getProfile: protectedProcedure
|
||||||
.query(async ({ ctx }): Promise<UserProfileResponse> => {
|
.query(async ({ ctx }): Promise<UserProfileResponse> => {
|
||||||
const userId = ctx.user.userId;
|
const userId = ctx.user.userId;
|
||||||
|
|
|
||||||
|
|
@ -49,10 +49,11 @@ export const complaintRouter = router({
|
||||||
.input(z.object({
|
.input(z.object({
|
||||||
orderId: z.string().optional(),
|
orderId: z.string().optional(),
|
||||||
complaintBody: z.string().min(1, 'Complaint body is required'),
|
complaintBody: z.string().min(1, 'Complaint body is required'),
|
||||||
|
imageUrls: z.array(z.string()).optional(),
|
||||||
}))
|
}))
|
||||||
.mutation(async ({ input, ctx }): Promise<UserRaiseComplaintResponse> => {
|
.mutation(async ({ input, ctx }): Promise<UserRaiseComplaintResponse> => {
|
||||||
const userId = ctx.user.userId;
|
const userId = ctx.user.userId;
|
||||||
const { orderId, complaintBody } = input;
|
const { orderId, complaintBody, imageUrls } = input;
|
||||||
|
|
||||||
let orderIdNum: number | null = null;
|
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:
|
// Old implementation - direct DB query:
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { ApiError } from '@/src/lib/api-error';
|
||||||
export const fileUploadRouter = router({
|
export const fileUploadRouter = router({
|
||||||
generateUploadUrls: protectedProcedure
|
generateUploadUrls: protectedProcedure
|
||||||
.input(z.object({
|
.input(z.object({
|
||||||
contextString: z.enum(['review', 'product_info', 'notification']),
|
contextString: z.enum(['review', 'product_info', 'notification', 'complaint', 'profile']),
|
||||||
mimeTypes: z.array(z.string()),
|
mimeTypes: z.array(z.string()),
|
||||||
}))
|
}))
|
||||||
.mutation(async ({ input }): Promise<{ uploadUrls: string[] }> => {
|
.mutation(async ({ input }): Promise<{ uploadUrls: string[] }> => {
|
||||||
|
|
@ -28,6 +28,10 @@ export const fileUploadRouter = router({
|
||||||
// }
|
// }
|
||||||
else if(contextString === 'notification') {
|
else if(contextString === 'notification') {
|
||||||
folder = 'notification-images'
|
folder = 'notification-images'
|
||||||
|
} else if (contextString === 'complaint') {
|
||||||
|
folder = 'complaint-images'
|
||||||
|
} else if (contextString === 'profile') {
|
||||||
|
folder = 'profile-images'
|
||||||
} else {
|
} else {
|
||||||
folder = '';
|
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 { Router } from "express";
|
||||||
import avRouter from "@/src/apis/admin-apis/apis/av-router"
|
import avRouter from "@/src/apis/admin-apis/apis/av-router"
|
||||||
import commonRouter from "@/src/apis/common-apis/apis/common.router"
|
import commonRouter from "@/src/apis/common-apis/apis/common.router"
|
||||||
import uvRouter from "@/src/uv-apis/uv-router"
|
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.use('/av', avRouter);
|
router.use('/av', avRouter);
|
||||||
router.use('/cm', commonRouter);
|
router.use('/cm', commonRouter);
|
||||||
router.use('/uv', uvRouter);
|
|
||||||
|
|
||||||
|
|
||||||
const v1Router = router;
|
const v1Router = router;
|
||||||
|
|
|
||||||
|
|
@ -5,16 +5,17 @@ import { useRouter } from "expo-router";
|
||||||
import { MyText, tw, MyTouchableOpacity } from "common-ui";
|
import { MyText, tw, MyTouchableOpacity } from "common-ui";
|
||||||
import { useAuth } from "@/src/contexts/AuthContext";
|
import { useAuth } from "@/src/contexts/AuthContext";
|
||||||
import RegistrationForm from "@/components/registration-form";
|
import RegistrationForm from "@/components/registration-form";
|
||||||
|
import type { RegisterData, UpdateProfileData } from "@/src/types/auth";
|
||||||
|
|
||||||
function Register() {
|
function Register() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { register } = useAuth();
|
const { register } = useAuth();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const handleRegister = async (formData: FormData) => {
|
const handleRegister = async (formData: RegisterData | UpdateProfileData) => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
await register(formData);
|
await register(formData as RegisterData);
|
||||||
// Auth context will handle navigation on successful registration
|
// Auth context will handle navigation on successful registration
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { AppContainer, MyButton, MyText, tw , BottomDialog } from "common-ui";
|
||||||
import RegistrationForm from "@/components/registration-form";
|
import RegistrationForm from "@/components/registration-form";
|
||||||
import { useUserDetails, useAuth } from "@/src/contexts/AuthContext";
|
import { useUserDetails, useAuth } from "@/src/contexts/AuthContext";
|
||||||
import { useUpdateProfile } from "@/src/api-hooks/auth.api";
|
import { useUpdateProfile } from "@/src/api-hooks/auth.api";
|
||||||
|
import type { RegisterData, UpdateProfileData } from "@/src/types/auth";
|
||||||
import { router } from "expo-router";
|
import { router } from "expo-router";
|
||||||
import { trpc } from '@/src/trpc-client';
|
import { trpc } from '@/src/trpc-client';
|
||||||
|
|
||||||
|
|
@ -20,9 +21,9 @@ function EditProfile() {
|
||||||
// Prevent unnecessary re-renders
|
// Prevent unnecessary re-renders
|
||||||
const mobileInputValue = useMemo(() => enteredMobile, [enteredMobile]);
|
const mobileInputValue = useMemo(() => enteredMobile, [enteredMobile]);
|
||||||
|
|
||||||
const handleUpdate = async (data: FormData) => {
|
const handleUpdate = async (data: RegisterData | UpdateProfileData) => {
|
||||||
try {
|
try {
|
||||||
const response = await updateProfileMutation.mutateAsync(data);
|
const response = await updateProfileMutation.mutateAsync(data as UpdateProfileData);
|
||||||
|
|
||||||
// Update the context with new user details
|
// Update the context with new user details
|
||||||
if (response.user) {
|
if (response.user) {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { View, TextInput, ActivityIndicator, KeyboardAvoidingView, Platform, Alert } from 'react-native';
|
import { View, TextInput, ActivityIndicator, KeyboardAvoidingView, Platform, Alert } from 'react-native';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons'
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { MyText, ImageUploaderNeo, tw, MyTouchableOpacity, type ImageUploaderNeoItem, type ImageUploaderNeoPayload } from 'common-ui'
|
||||||
import { MyText, ImageUploader, tw, MyTouchableOpacity } from 'common-ui';
|
import { trpc } from '@/src/trpc-client'
|
||||||
import usePickImage from 'common-ui/src/components/use-pick-image';
|
import { useUploadToObjectStorage } from '../hooks/useUploadToObjectStore'
|
||||||
import axios from '../services/axios-user-ui';
|
|
||||||
// import axios from 'common-ui/src/services/axios';
|
|
||||||
|
|
||||||
interface ComplaintFormProps {
|
interface ComplaintFormProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
|
@ -15,71 +13,66 @@ interface ComplaintFormProps {
|
||||||
|
|
||||||
export default function ComplaintForm({ open, onClose, orderId }: ComplaintFormProps) {
|
export default function ComplaintForm({ open, onClose, orderId }: ComplaintFormProps) {
|
||||||
const [complaintBody, setComplaintBody] = useState('');
|
const [complaintBody, setComplaintBody] = useState('');
|
||||||
const [complaintImages, setComplaintImages] = useState<{ uri?: string }[]>([]);
|
const [complaintImages, setComplaintImages] = useState<ImageUploaderNeoItem[]>([])
|
||||||
|
|
||||||
// API function
|
const raiseComplaintMutation = trpc.user.complaint.raise.useMutation()
|
||||||
const raiseComplaintApi = async (payload: { complaintBody: string; images: { uri?: string }[] }) => {
|
|
||||||
const formData = new FormData();
|
|
||||||
|
|
||||||
formData.append('orderId', orderId.toString());
|
const { upload, isUploading } = useUploadToObjectStorage()
|
||||||
formData.append('complaintBody', payload.complaintBody);
|
|
||||||
|
|
||||||
// Add images if provided
|
const handleAddImages = (images: ImageUploaderNeoPayload[]) => {
|
||||||
if (payload.images && payload.images.length > 0) {
|
setComplaintImages((prev) => [
|
||||||
payload.images.forEach((image, index) => {
|
...prev,
|
||||||
if (image.uri) {
|
...images.map((image) => ({
|
||||||
const fileName = `complaint-image-${index}.jpg`;
|
imgUrl: image.url,
|
||||||
formData.append('images', {
|
mimeType: image.mimeType,
|
||||||
uri: image.uri,
|
})),
|
||||||
name: fileName,
|
])
|
||||||
type: 'image/jpeg',
|
}
|
||||||
} as any);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await axios.post('/uv/complaints/raise', formData, {
|
const handleRemoveImage = (image: ImageUploaderNeoPayload) => {
|
||||||
headers: {
|
setComplaintImages((prev) => prev.filter((item) => item.imgUrl !== image.url))
|
||||||
'Content-Type': 'multipart/form-data',
|
}
|
||||||
},
|
|
||||||
});
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Hook
|
const handleSubmit = async () => {
|
||||||
const raiseComplaintMutation = useMutation({
|
|
||||||
mutationFn: raiseComplaintApi,
|
|
||||||
});
|
|
||||||
|
|
||||||
const pickComplaintImage = usePickImage({
|
|
||||||
setFile: (files) => setComplaintImages(prev => [...prev, ...files]),
|
|
||||||
multiple: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSubmit = () => {
|
|
||||||
if (!complaintBody.trim()) {
|
if (!complaintBody.trim()) {
|
||||||
Alert.alert('Error', 'Please enter complaint details');
|
Alert.alert('Error', 'Please enter complaint details');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
raiseComplaintMutation.mutate(
|
try {
|
||||||
{
|
let imageUrls: string[] = []
|
||||||
complaintBody: complaintBody.trim(),
|
|
||||||
images: complaintImages,
|
if (complaintImages.length > 0) {
|
||||||
},
|
const uploadImages = await Promise.all(
|
||||||
{
|
complaintImages.map(async (image) => {
|
||||||
onSuccess: () => {
|
const response = await fetch(image.imgUrl)
|
||||||
Alert.alert('Success', 'Complaint raised successfully');
|
const blob = await response.blob()
|
||||||
setComplaintBody('');
|
return { blob, mimeType: image.mimeType || 'image/jpeg' }
|
||||||
setComplaintImages([]);
|
})
|
||||||
onClose();
|
)
|
||||||
},
|
|
||||||
onError: (error: any) => {
|
const { keys } = await upload({
|
||||||
Alert.alert('Error', error.message || 'Failed to raise complaint');
|
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;
|
if (!open) return null;
|
||||||
|
|
||||||
|
|
@ -105,18 +98,18 @@ export default function ComplaintForm({ open, onClose, orderId }: ComplaintFormP
|
||||||
textAlignVertical="top"
|
textAlignVertical="top"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ImageUploader
|
<ImageUploaderNeo
|
||||||
images={complaintImages}
|
images={complaintImages}
|
||||||
onAddImage={pickComplaintImage}
|
onImageAdd={handleAddImages}
|
||||||
onRemoveImage={(uri) => setComplaintImages(prev => prev.filter(img => img.uri !== uri))}
|
onImageRemove={handleRemoveImage}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MyTouchableOpacity
|
<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}
|
onPress={handleSubmit}
|
||||||
disabled={raiseComplaintMutation.isPending}
|
disabled={raiseComplaintMutation.isPending || isUploading}
|
||||||
>
|
>
|
||||||
{raiseComplaintMutation.isPending ? (
|
{raiseComplaintMutation.isPending || isUploading ? (
|
||||||
<ActivityIndicator color="white" />
|
<ActivityIndicator color="white" />
|
||||||
) : (
|
) : (
|
||||||
<MyText style={tw`text-white font-bold text-lg`}>Submit Complaint</MyText>
|
<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 { View, TextInput, Alert } from "react-native";
|
||||||
import { useForm, Controller } from "react-hook-form";
|
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 { trpc } from "@/src/trpc-client";
|
||||||
|
import { useUploadToObjectStorage } from "../hooks/useUploadToObjectStore";
|
||||||
|
import type { RegisterData, UpdateProfileData } from "@/src/types/auth";
|
||||||
|
|
||||||
interface RegisterFormInputs {
|
interface RegisterFormInputs {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -16,7 +18,7 @@ interface RegisterFormInputs {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RegistrationFormProps {
|
interface RegistrationFormProps {
|
||||||
onSubmit: (data: FormData) => void | Promise<void>;
|
onSubmit: (data: RegisterData | UpdateProfileData) => void | Promise<void>;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
initialValues?: Partial<RegisterFormInputs>;
|
initialValues?: Partial<RegisterFormInputs>;
|
||||||
isEdit?: boolean;
|
isEdit?: boolean;
|
||||||
|
|
@ -29,6 +31,7 @@ function RegistrationForm({ onSubmit, isLoading = false, initialValues, isEdit =
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [confirmPassword, setConfirmPassword] = useState('');
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
const updatePasswordMutation = trpc.user.auth.updatePassword.useMutation();
|
const updatePasswordMutation = trpc.user.auth.updatePassword.useMutation();
|
||||||
|
const { uploadSingle, isUploading } = useUploadToObjectStorage()
|
||||||
|
|
||||||
// Set initial profile image URI for edit mode
|
// Set initial profile image URI for edit mode
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|
@ -161,27 +164,39 @@ function RegistrationForm({ onSubmit, isLoading = false, initialValues, isEdit =
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create FormData
|
let profileImageUrl: string | undefined;
|
||||||
const formData = new FormData();
|
if (profileImageFile?.uri) {
|
||||||
formData.append('name', data.name.trim());
|
const response = await fetch(profileImageFile.uri)
|
||||||
formData.append('email', data.email.trim().toLowerCase());
|
const blob = await response.blob()
|
||||||
formData.append('mobile', data.mobile.replace(/\D/g, ''));
|
const mimeType = profileImageFile.mimeType || 'image/jpeg'
|
||||||
|
const { key } = await uploadSingle(blob, mimeType, 'profile')
|
||||||
// Only include password if provided (for edit mode)
|
profileImageUrl = key
|
||||||
if (data.password) {
|
|
||||||
formData.append('password', data.password);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (profileImageFile) {
|
const basePayload = {
|
||||||
|
name: data.name.trim(),
|
||||||
formData.append('profileImage', {
|
email: data.email.trim().toLowerCase(),
|
||||||
uri: profileImageFile.uri,
|
mobile: data.mobile.replace(/\D/g, ''),
|
||||||
type: profileImageFile.mimeType || 'image/jpeg',
|
|
||||||
name: profileImageFile.name || 'profile.jpg',
|
|
||||||
} as any);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 () => {
|
const handleUpdatePassword = async () => {
|
||||||
|
|
@ -407,10 +422,14 @@ function RegistrationForm({ onSubmit, isLoading = false, initialValues, isEdit =
|
||||||
fillColor="brand500"
|
fillColor="brand500"
|
||||||
textColor="white1"
|
textColor="white1"
|
||||||
fullWidth
|
fullWidth
|
||||||
disabled={isLoading}
|
disabled={isLoading || isUploading}
|
||||||
style={tw` rounded-lg`}
|
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>
|
</MyButton>
|
||||||
|
|
||||||
{isEdit && (
|
{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 { trpc } from '@/src/trpc-client'
|
||||||
import axios from 'common-ui/src/services/axios';
|
import { LoginCredentials, RegisterData, UpdateProfileData } from '@/src/types/auth'
|
||||||
import { LoginCredentials, RegisterData } from '@/src/types/auth';
|
|
||||||
|
|
||||||
// API response types
|
// API response types
|
||||||
interface RegisterResponse {
|
interface RegisterResponse {
|
||||||
token: string;
|
token: string;
|
||||||
user: {
|
user: {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name?: string | null;
|
||||||
email: string;
|
email: string | null;
|
||||||
mobile: string;
|
mobile: string | null;
|
||||||
profileImage?: string;
|
profileImage?: string | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -19,7 +18,7 @@ interface UpdateProfileResponse {
|
||||||
token: string;
|
token: string;
|
||||||
user: {
|
user: {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name?: string | null;
|
||||||
email: string | null;
|
email: string | null;
|
||||||
mobile: string | null;
|
mobile: string | null;
|
||||||
profileImage?: 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
|
// React Query hooks
|
||||||
export const useRegister = () => {
|
export const useRegister = () => {
|
||||||
return useMutation({
|
const mutation = trpc.user.auth.register.useMutation()
|
||||||
mutationFn: registerApi,
|
|
||||||
});
|
return {
|
||||||
|
...mutation,
|
||||||
|
mutateAsync: async (data: RegisterData): Promise<RegisterResponse> => {
|
||||||
|
const response = await mutation.mutateAsync(data)
|
||||||
|
return response.data
|
||||||
|
},
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUpdateProfile = () => {
|
export const useUpdateProfile = () => {
|
||||||
return useMutation({
|
const mutation = trpc.user.auth.updateProfile.useMutation()
|
||||||
mutationFn: updateProfileApi,
|
|
||||||
onError: () => {}
|
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 {
|
try {
|
||||||
setAuthState(prev => ({ ...prev, isLoading: true }));
|
setAuthState(prev => ({ ...prev, isLoading: true }));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,13 +37,25 @@ export interface RegisterData {
|
||||||
email: string;
|
email: string;
|
||||||
mobile: string;
|
mobile: string;
|
||||||
password: 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 {
|
export interface AuthContextType extends AuthState {
|
||||||
login: (credentials: LoginCredentials) => Promise<void>;
|
login: (credentials: LoginCredentials) => Promise<void>;
|
||||||
loginWithToken: (token: string, user: User) => Promise<void>;
|
loginWithToken: (token: string, user: User) => Promise<void>;
|
||||||
register: (data: FormData) => Promise<void>;
|
register: (data: RegisterData) => Promise<void>;
|
||||||
logout: () => Promise<void>;
|
logout: () => Promise<void>;
|
||||||
updateUser: (user: Partial<User>) => void;
|
updateUser: (user: Partial<User>) => void;
|
||||||
updateUserDetails: (userDetails: Partial<UserDetails>) => void;
|
updateUserDetails: (userDetails: Partial<UserDetails>) => void;
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ const mapProduct = (product: ProductRow): AdminProduct => ({
|
||||||
price: product.price.toString(),
|
price: product.price.toString(),
|
||||||
marketPrice: product.marketPrice ? product.marketPrice.toString() : null,
|
marketPrice: product.marketPrice ? product.marketPrice.toString() : null,
|
||||||
images: getStringArray(product.images),
|
images: getStringArray(product.images),
|
||||||
|
imageKeys: getStringArray(product.images),
|
||||||
isOutOfStock: product.isOutOfStock,
|
isOutOfStock: product.isOutOfStock,
|
||||||
isSuspended: product.isSuspended,
|
isSuspended: product.isSuspended,
|
||||||
isFlashAvailable: product.isFlashAvailable,
|
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;
|
price: string;
|
||||||
marketPrice: string | null;
|
marketPrice: string | null;
|
||||||
images: string[] | null;
|
images: string[] | null;
|
||||||
|
imageKeys: string[] | null;
|
||||||
isOutOfStock: boolean;
|
isOutOfStock: boolean;
|
||||||
isSuspended: boolean;
|
isSuspended: boolean;
|
||||||
isFlashAvailable: boolean;
|
isFlashAvailable: boolean;
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import ImageCarousel from "./src/components/ImageCarousel";
|
||||||
import ImageGallery from "./src/components/ImageGallery";
|
import ImageGallery from "./src/components/ImageGallery";
|
||||||
import ImageGalleryWithDelete from "./src/components/ImageGalleryWithDelete";
|
import ImageGalleryWithDelete from "./src/components/ImageGalleryWithDelete";
|
||||||
import ImageUploader from "./src/components/ImageUploader";
|
import ImageUploader from "./src/components/ImageUploader";
|
||||||
|
import ImageUploaderNeo, { ImageUploaderNeoItem, ImageUploaderNeoPayload } from "./src/components/ImageUploaderNeo";
|
||||||
import ProfileImage from "./src/components/profile-image";
|
import ProfileImage from "./src/components/profile-image";
|
||||||
import Checkbox from "./src/components/checkbox";
|
import Checkbox from "./src/components/checkbox";
|
||||||
import AppContainer from "./src/components/app-container";
|
import AppContainer from "./src/components/app-container";
|
||||||
|
|
@ -100,6 +101,9 @@ export {
|
||||||
ImageGallery,
|
ImageGallery,
|
||||||
ImageGalleryWithDelete,
|
ImageGalleryWithDelete,
|
||||||
ImageUploader,
|
ImageUploader,
|
||||||
|
ImageUploaderNeo,
|
||||||
|
ImageUploaderNeoItem,
|
||||||
|
ImageUploaderNeoPayload,
|
||||||
ProfileImage,
|
ProfileImage,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
AppContainer,
|
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