merge test #1
106 changed files with 309 additions and 3634 deletions
|
|
@ -1,8 +1,6 @@
|
|||
# Agent Instructions for Meat Farmer Monorepo
|
||||
|
||||
## Important instructions
|
||||
- Don't try to build the code or run or compile it. Just make changes and leave the rest for the user.
|
||||
- Don't run any drizzle migrations. User will handle it.
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
|
|
@ -47,7 +45,3 @@ react-native. They are available in the common-ui as MyText, MyTextInput, MyTouc
|
|||
- Apps: `user-ui`, `admin-ui`, `inspiration-ui`, `inspiration-backend`
|
||||
- Database: Drizzle ORM with PostgreSQL
|
||||
|
||||
## Important Notes
|
||||
- **Do not run build, compile, or migration commands** - These should be handled manually by developers
|
||||
- Avoid running `npm run build`, `tsc`, `drizzle-kit generate`, or similar compilation/migration commands
|
||||
- Don't do anything with git. Don't do git add or git commit. That will be managed entirely by the user
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -5,19 +5,19 @@ import cors from "cors";
|
|||
import multer from "multer";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import { db } from './src/db/db_index';
|
||||
import { staffUsers, userDetails } from './src/db/schema';
|
||||
import { db } from '@/src/db/db_index';
|
||||
import { staffUsers, userDetails } from '@/src/db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import mainRouter from './src/main-router';
|
||||
import initFunc from './src/lib/init';
|
||||
import mainRouter from '@/src/main-router';
|
||||
import initFunc from '@/src/lib/init';
|
||||
import { createExpressMiddleware } from '@trpc/server/adapters/express';
|
||||
import { appRouter } from './src/trpc/router';
|
||||
import { appRouter } from '@/src/trpc/router';
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import jwt from 'jsonwebtoken'
|
||||
import signedUrlCache from 'src/lib/signed-url-cache';
|
||||
import { seed } from 'src/db/seed';
|
||||
import './src/jobs/jobs-index';
|
||||
import { startAutomatedJobs } from './src/lib/automatedJobs';
|
||||
import signedUrlCache from '@/src/lib/signed-url-cache';
|
||||
import { seed } from '@/src/db/seed';
|
||||
import '@/src/jobs/jobs-index';
|
||||
import { startAutomatedJobs } from '@/src/lib/automatedJobs';
|
||||
|
||||
seed()
|
||||
initFunc()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Router } from "express";
|
||||
import { authenticateStaff } from "../middleware/staff-auth";
|
||||
import productRouter from "./product.router";
|
||||
import tagRouter from "./tag.router";
|
||||
import { authenticateStaff } from "@/src/middleware/staff-auth";
|
||||
import productRouter from "@/src/apis/admin-apis/apis/product.router"
|
||||
import tagRouter from "@/src/apis/admin-apis/apis/tag.router"
|
||||
|
||||
const router = Router();
|
||||
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import { Request, Response } from "express";
|
||||
import { db } from "../db/db_index";
|
||||
import { productTagInfo } from "../db/schema";
|
||||
import { db } from "@/src/db/db_index";
|
||||
import { productTagInfo } from "@/src/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { ApiError } from "../lib/api-error";
|
||||
import { imageUploadS3, generateSignedUrlFromS3Url } from "../lib/s3-client";
|
||||
import { deleteS3Image } from "../lib/delete-image";
|
||||
import { initializeAllStores } from '../stores/store-initializer';
|
||||
import { ApiError } from "@/src/lib/api-error";
|
||||
import { imageUploadS3, generateSignedUrlFromS3Url } from "@/src/lib/s3-client";
|
||||
import { deleteS3Image } from "@/src/lib/delete-image";
|
||||
import { initializeAllStores } from '@/src/stores/store-initializer';
|
||||
|
||||
/**
|
||||
* Create a new product tag
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import { Request, Response } from "express";
|
||||
import { db } from "../db/db_index";
|
||||
import { productInfo, units, specialDeals, productTags } from "../db/schema";
|
||||
import { db } from "@/src/db/db_index";
|
||||
import { productInfo, units, specialDeals, productTags } from "@/src/db/schema";
|
||||
import { eq, inArray } from "drizzle-orm";
|
||||
import { ApiError } from "../lib/api-error";
|
||||
import { imageUploadS3, getOriginalUrlFromSignedUrl } from "../lib/s3-client";
|
||||
import { deleteS3Image } from "../lib/delete-image";
|
||||
import type { SpecialDeal } from "../db/types";
|
||||
import { initializeAllStores } from '../stores/store-initializer';
|
||||
import { ApiError } from "@/src/lib/api-error";
|
||||
import { imageUploadS3, getOriginalUrlFromSignedUrl } from "@/src/lib/s3-client";
|
||||
import { deleteS3Image } from "@/src/lib/delete-image";
|
||||
import type { SpecialDeal } from "@/src/db/types";
|
||||
import { initializeAllStores } from '@/src/stores/store-initializer';
|
||||
|
||||
type CreateDeal = {
|
||||
quantity: number;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { Router } from "express";
|
||||
import { createProduct, updateProduct } from "./product.controller";
|
||||
import uploadHandler from '../lib/upload-handler';
|
||||
import { createProduct, updateProduct } from "@/src/apis/admin-apis/apis/product.controller"
|
||||
import uploadHandler from '@/src/lib/upload-handler';
|
||||
|
||||
const router = Router();
|
||||
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { Router } from "express";
|
||||
import { createTag, getAllTags, getTagById, updateTag, deleteTag } from "./product-tags.controller";
|
||||
import uploadHandler from '../lib/upload-handler';
|
||||
import { createTag, getAllTags, getTagById, updateTag, deleteTag } from "@/src/apis/admin-apis/apis/product-tags.controller"
|
||||
import uploadHandler from '@/src/lib/upload-handler';
|
||||
|
||||
const router = Router();
|
||||
|
||||
0
apps/backend/src/apis/admin-apis/dataAccessors/demo.txt
Normal file
0
apps/backend/src/apis/admin-apis/dataAccessors/demo.txt
Normal file
|
|
@ -1,8 +1,8 @@
|
|||
import { eq, gt, and, sql, inArray } from "drizzle-orm";
|
||||
import { Request, Response } from "express";
|
||||
import { db } from "../db/db_index";
|
||||
import { productInfo, units, productSlots, deliverySlotInfo, productTags } from "../db/schema";
|
||||
import { scaffoldAssetUrl } from "../lib/s3-client";
|
||||
import { db } from "@/src/db/db_index"
|
||||
import { productInfo, units, productSlots, deliverySlotInfo, productTags } from "@/src/db/schema"
|
||||
import { scaffoldAssetUrl } from "@/src/lib/s3-client"
|
||||
|
||||
/**
|
||||
* Get next delivery date for a product
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { Router } from "express";
|
||||
import { getAllProductsSummary } from "./common-product.controller";
|
||||
import { getAllProductsSummary } from "@/src/apis/common-apis/apis/common-product.controller"
|
||||
|
||||
const router = Router();
|
||||
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { Router } from "express";
|
||||
import commonProductsRouter from "./common-product.router";
|
||||
import commonProductsRouter from "@/src/apis/common-apis/apis/common-product.router"
|
||||
|
||||
const router = Router();
|
||||
|
||||
0
apps/backend/src/apis/common-apis/dataAccessors/demo.txt
Normal file
0
apps/backend/src/apis/common-apis/dataAccessors/demo.txt
Normal file
|
|
@ -1,7 +1,7 @@
|
|||
import { drizzle } from "drizzle-orm/node-postgres"
|
||||
import { migrate } from "drizzle-orm/node-postgres/migrator"
|
||||
import path from "path"
|
||||
import * as schema from "./schema"
|
||||
import * as schema from "@/src/db/schema"
|
||||
|
||||
const db = drizzle({ connection: process.env.DATABASE_URL!, casing: "snake_case", schema: schema })
|
||||
// const db = drizzle('postgresql://postgres:postgres@localhost:2345/pooler');
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@
|
|||
* This was a one time script to change the composition of the signed urls
|
||||
*/
|
||||
|
||||
import { db } from './db_index';
|
||||
import { db } from '@/src/db/db_index'
|
||||
import {
|
||||
userDetails,
|
||||
productInfo,
|
||||
productTagInfo,
|
||||
complaints
|
||||
} from './schema';
|
||||
} from '@/src/db/schema';
|
||||
import { eq, not, isNull } from 'drizzle-orm';
|
||||
|
||||
const S3_DOMAIN = 'https://s3.sgp.io.cloud.ovh.net';
|
||||
|
|
@ -122,4 +122,4 @@ runMigration()
|
|||
.catch((error) => {
|
||||
console.error('Process failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { db } from "./db_index";
|
||||
import { units, productInfo, deliverySlotInfo, productSlots, keyValStore, staffRoles, staffPermissions, staffRolePermissions } from "./schema";
|
||||
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 '../lib/env-exporter';
|
||||
import { CONST_KEYS } from '../lib/const-keys';
|
||||
import { minOrderValue, deliveryCharge } from '@/src/lib/env-exporter'
|
||||
import { CONST_KEYS } from '@/src/lib/const-keys'
|
||||
|
||||
export async function seed() {
|
||||
console.log("Seeding database...");
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import type {
|
|||
productCategories,
|
||||
cartItems,
|
||||
coupons,
|
||||
} from "./schema";
|
||||
} from "@/src/db/schema";
|
||||
|
||||
export type User = InferSelectModel<typeof users>;
|
||||
export type Address = InferSelectModel<typeof addresses>;
|
||||
|
|
@ -44,4 +44,4 @@ export type OrderWithItems = Order & {
|
|||
|
||||
export type CartItemWithProduct = CartItem & {
|
||||
product: ProductInfo;
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import * as cron from 'node-cron';
|
||||
import { checkPendingPayments, checkRefundStatuses } from './payment-status-checker';
|
||||
import { checkPendingPayments, checkRefundStatuses } from '@/src/jobs/payment-status-checker'
|
||||
|
||||
const runCombinedJob = async () => {
|
||||
const start = Date.now();
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import * as cron from 'node-cron';
|
||||
import { db } from '../db/db_index';
|
||||
import { payments, orders, deliverySlotInfo, refunds } from '../db/schema';
|
||||
import { db } from '@/src/db/db_index'
|
||||
import { payments, orders, deliverySlotInfo, refunds } from '@/src/db/schema'
|
||||
import { eq, and, gt, isNotNull } from 'drizzle-orm';
|
||||
import { RazorpayPaymentService } from '../lib/payments-utils';
|
||||
import { RazorpayPaymentService } from '@/src/lib/payments-utils'
|
||||
|
||||
interface PendingPaymentRecord {
|
||||
payment: typeof payments.$inferSelect;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import * as cron from 'node-cron';
|
||||
import { db } from '../db/db_index';
|
||||
import { productInfo, keyValStore } from '../db/schema';
|
||||
import { db } from '@/src/db/db_index'
|
||||
import { productInfo, keyValStore } from '@/src/db/schema'
|
||||
import { inArray, eq } from 'drizzle-orm';
|
||||
import { CONST_KEYS } from '../lib/const-keys';
|
||||
import { computeConstants } from '../lib/const-store';
|
||||
import { CONST_KEYS } from '@/src/lib/const-keys'
|
||||
import { computeConstants } from '@/src/lib/const-store'
|
||||
|
||||
|
||||
const MUTTON_ITEMS = [
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import axiosParent from "axios";
|
||||
import { phonePeBaseUrl } from "./env-exporter";
|
||||
import { phonePeBaseUrl } from "@/src/lib/env-exporter"
|
||||
|
||||
export const phonepeAxios = axiosParent.create({
|
||||
baseURL: phonePeBaseUrl,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { db } from '../db/db_index';
|
||||
import { keyValStore } from '../db/schema';
|
||||
import redisClient from './redis-client';
|
||||
import { CONST_KEYS, CONST_KEYS_ARRAY, type ConstKey } from './const-keys';
|
||||
import { db } from '@/src/db/db_index'
|
||||
import { keyValStore } from '@/src/db/schema'
|
||||
import redisClient from '@/src/lib/redis-client'
|
||||
import { CONST_KEYS, CONST_KEYS_ARRAY, type ConstKey } from '@/src/lib/const-keys'
|
||||
|
||||
const CONST_REDIS_PREFIX = 'const:';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { eq } from "drizzle-orm";
|
||||
import { db } from "../db/db_index";
|
||||
import { deleteImageUtil, getOriginalUrlFromSignedUrl } from "./s3-client";
|
||||
import { s3Url } from "./env-exporter";
|
||||
import { db } from "@/src/db/db_index"
|
||||
import { deleteImageUtil, getOriginalUrlFromSignedUrl } from "@/src/lib/s3-client"
|
||||
import { s3Url } from "@/src/lib/env-exporter"
|
||||
|
||||
function extractS3Key(url: string): string | null {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { db } from '../db/db_index';
|
||||
import { orders, orderItems, orderStatus, payments, refunds, couponUsage, complaints } from '../db/schema';
|
||||
import { db } from '@/src/db/db_index'
|
||||
import { orders, orderItems, orderStatus, payments, refunds, couponUsage, complaints } from '@/src/db/schema'
|
||||
import { eq, inArray } from 'drizzle-orm';
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import redisClient from './redis-client';
|
||||
import redisClient from '@/src/lib/redis-client'
|
||||
|
||||
export async function enqueue(queueName: string, eventData: any): Promise<boolean> {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Expo } from "expo-server-sdk";
|
||||
import { title } from "process";
|
||||
import { expoAccessToken } from "./env-exporter";
|
||||
import { expoAccessToken } from "@/src/lib/env-exporter"
|
||||
|
||||
const expo = new Expo({
|
||||
accessToken: expoAccessToken,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import './notif-job';
|
||||
import { initializeAllStores } from '../stores/store-initializer';
|
||||
import { initializeUserNegativityStore } from '../stores/user-negativity-store';
|
||||
import { startOrderHandler, startCancellationHandler, publishOrder } from './post-order-handler';
|
||||
import { deleteOrders } from './delete-orders';
|
||||
import '@/src/lib/notif-job'
|
||||
import { initializeAllStores } from '@/src/stores/store-initializer'
|
||||
import { initializeUserNegativityStore } from '@/src/stores/user-negativity-store'
|
||||
import { startOrderHandler, startCancellationHandler, publishOrder } from '@/src/lib/post-order-handler'
|
||||
import { deleteOrders } from '@/src/lib/delete-orders'
|
||||
|
||||
/**
|
||||
* Initialize all application services
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { Queue, Worker } from 'bullmq';
|
||||
import { Expo } from 'expo-server-sdk';
|
||||
import { redisUrl } from './env-exporter';
|
||||
import { db } from '../db/db_index';
|
||||
import { generateSignedUrlFromS3Url } from './s3-client';
|
||||
import { redisUrl } from '@/src/lib/env-exporter'
|
||||
import { db } from '@/src/db/db_index'
|
||||
import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client'
|
||||
import {
|
||||
NOTIFS_QUEUE,
|
||||
ORDER_PLACED_MESSAGE,
|
||||
|
|
@ -12,7 +12,7 @@ import {
|
|||
ORDER_DELIVERED_MESSAGE,
|
||||
ORDER_CANCELLED_MESSAGE,
|
||||
REFUND_INITIATED_MESSAGE
|
||||
} from './const-strings';
|
||||
} from '@/src/lib/const-strings';
|
||||
|
||||
export const notificationQueue = new Queue(NOTIFS_QUEUE, {
|
||||
connection: { url: redisUrl },
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { db } from "../db/db_index";
|
||||
import { sendPushNotificationsMany } from "./expo-service";
|
||||
// import { usersTable, notifCredsTable, notificationTable } from "../db/schema";
|
||||
import { db } from "@/src/db/db_index"
|
||||
import { sendPushNotificationsMany } from "@/src/lib/expo-service"
|
||||
// import { usersTable, notifCredsTable, notificationTable } from "@/src/db/schema";
|
||||
import { eq, inArray } from "drizzle-orm";
|
||||
|
||||
// Core notification dispatch methods (renamed for clarity)
|
||||
|
|
@ -244,4 +244,4 @@ export const sendNotifToSingleUser = dispatchUserNotification;
|
|||
/**
|
||||
* @deprecated Use notifyNewOffer() or other purpose-specific methods instead
|
||||
*/
|
||||
export const sendNotifToManyUsers = dispatchBulkNotification;
|
||||
export const sendNotifToManyUsers = dispatchBulkNotification;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { ApiError } from './api-error';
|
||||
import { otpSenderAuthToken } from './env-exporter';
|
||||
import { ApiError } from '@/src/lib/api-error'
|
||||
import { otpSenderAuthToken } from '@/src/lib/env-exporter'
|
||||
|
||||
const otpStore = new Map<string, string>();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import Razorpay from "razorpay";
|
||||
import { razorpayId, razorpaySecret } from "./env-exporter";
|
||||
import { db } from "../db/db_index";
|
||||
import { payments } from "../db/schema";
|
||||
import { razorpayId, razorpaySecret } from "@/src/lib/env-exporter"
|
||||
import { db } from "@/src/db/db_index"
|
||||
import { payments } from "@/src/db/schema"
|
||||
|
||||
type Tx = Parameters<Parameters<typeof db.transaction>[0]>[0];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { db } from '../db/db_index';
|
||||
import { orders, orderStatus } from '../db/schema';
|
||||
import redisClient from './redis-client';
|
||||
import { sendTelegramMessage } from './telegram-service';
|
||||
import { db } from '@/src/db/db_index'
|
||||
import { orders, orderStatus } from '@/src/db/schema'
|
||||
import redisClient from '@/src/lib/redis-client'
|
||||
import { sendTelegramMessage } from '@/src/lib/telegram-service'
|
||||
import { inArray, eq } from 'drizzle-orm';
|
||||
|
||||
const ORDER_CHANNEL = 'orders:placed';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { createClient, RedisClientType } from 'redis';
|
||||
import { redisUrl } from './env-exporter';
|
||||
import { redisUrl } from '@/src/lib/env-exporter'
|
||||
|
||||
class RedisClient {
|
||||
private client: RedisClientType;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { db } from "../db/db_index";
|
||||
import { db } from "@/src/db/db_index"
|
||||
|
||||
/**
|
||||
* Constants for role names to avoid hardcoding and typos
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
// import { s3A, awsBucketName, awsRegion, awsSecretAccessKey } from "../lib/env-exporter"
|
||||
// import { s3A, awsBucketName, awsRegion, awsSecretAccessKey } from "@/src/lib/env-exporter"
|
||||
import { DeleteObjectCommand, DeleteObjectsCommand, PutObjectCommand, S3Client, GetObjectCommand } from "@aws-sdk/client-s3"
|
||||
import { getSignedUrl } from "@aws-sdk/s3-request-presigner"
|
||||
import signedUrlCache from "./signed-url-cache"
|
||||
import { s3AccessKeyId, s3Region, s3Url, s3SecretAccessKey, s3BucketName, assetsDomain } from "./env-exporter";
|
||||
import { db } from "../db/db_index"; // Adjust path if needed
|
||||
import { uploadUrlStatus } from "../db/schema";
|
||||
import signedUrlCache from "@/src/lib/signed-url-cache"
|
||||
import { s3AccessKeyId, s3Region, s3Url, s3SecretAccessKey, s3BucketName, assetsDomain } from "@/src/lib/env-exporter"
|
||||
import { db } from "@/src/db/db_index"; // Adjust path if needed
|
||||
import { uploadUrlStatus } from "@/src/db/schema"
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
|
||||
const s3Client = new S3Client({
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import axios from 'axios';
|
||||
import { isDevMode, telegramBotToken, telegramChatIds } from './env-exporter';
|
||||
import { isDevMode, telegramBotToken, telegramChatIds } from '@/src/lib/env-exporter'
|
||||
|
||||
const BOT_TOKEN = telegramBotToken;
|
||||
const CHAT_IDS = telegramChatIds;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { Router, Request, Response, NextFunction } from "express";
|
||||
import avRouter from "./admin-apis/av-router";
|
||||
import { ApiError } from "./lib/api-error";
|
||||
import v1Router from "./v1-router";
|
||||
import testController from "./test-controller";
|
||||
import { authenticateUser } from "./middleware/auth.middleware";
|
||||
import { raiseComplaint } from "./uv-apis/user-rest.controller";
|
||||
import uploadHandler from "./lib/upload-handler";
|
||||
import avRouter from "@/src/apis/admin-apis/apis/av-router"
|
||||
import { ApiError } from "@/src/lib/api-error"
|
||||
import v1Router from "@/src/v1-router"
|
||||
import testController from "@/src/test-controller"
|
||||
import { authenticateUser } from "@/src/middleware/auth.middleware"
|
||||
import { raiseComplaint } from "@/src/uv-apis/user-rest.controller"
|
||||
import uploadHandler from "@/src/lib/upload-handler"
|
||||
|
||||
|
||||
const router = Router();
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { Request, Response, NextFunction } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { db } from '../db/db_index';
|
||||
import { staffUsers, userDetails } from '../db/schema';
|
||||
import { db } from '@/src/db/db_index'
|
||||
import { staffUsers, userDetails } from '@/src/db/schema'
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { ApiError } from '../lib/api-error';
|
||||
import { ApiError } from '@/src/lib/api-error'
|
||||
|
||||
interface AuthenticatedRequest extends Request {
|
||||
user?: {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Request, Response, NextFunction } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { ApiError } from '../lib/api-error';
|
||||
import { ApiError } from '@/src/lib/api-error'
|
||||
|
||||
// Extend the Request interface to include user property
|
||||
declare global {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { Request, Response, NextFunction } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { db } from '../db/db_index';
|
||||
import { staffUsers } from '../db/schema';
|
||||
import { db } from '@/src/db/db_index'
|
||||
import { staffUsers } from '@/src/db/schema'
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { ApiError } from '../lib/api-error';
|
||||
import { ApiError } from '@/src/lib/api-error'
|
||||
|
||||
// Extend Request interface to include staffUser
|
||||
declare global {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { db } from '../../db/db_index'
|
||||
import { db } from '@/src/db/db_index'
|
||||
import {
|
||||
orders,
|
||||
orderItems,
|
||||
|
|
@ -13,7 +13,7 @@ import {
|
|||
refunds,
|
||||
units,
|
||||
userDetails,
|
||||
} from '../../db/schema'
|
||||
} from '@/src/db/schema'
|
||||
import { eq, and, inArray, desc, gte } from 'drizzle-orm'
|
||||
|
||||
// ============ User/Auth Queries ============
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { db } from '../../db/db_index'
|
||||
import { productInfo, units, productSlots, deliverySlotInfo, specialDeals, storeInfo, productReviews, users } from '../../db/schema'
|
||||
import { db } from '@/src/db/db_index'
|
||||
import { productInfo, units, productSlots, deliverySlotInfo, specialDeals, storeInfo, productReviews, users } from '@/src/db/schema'
|
||||
import { eq, and, gt, sql, desc } from 'drizzle-orm'
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
// import redisClient from './redis-client';
|
||||
import redisClient from 'src/lib/redis-client';
|
||||
import { db } from '../db/db_index';
|
||||
import { homeBanners } from '../db/schema';
|
||||
// import redisClient from '@/src/stores/redis-client';
|
||||
import redisClient from '@/src/lib/redis-client';
|
||||
import { db } from '@/src/db/db_index'
|
||||
import { homeBanners } from '@/src/db/schema'
|
||||
import { isNotNull, asc } from 'drizzle-orm';
|
||||
import { scaffoldAssetUrl } from 'src/lib/s3-client';
|
||||
import { scaffoldAssetUrl } from '@/src/lib/s3-client';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Banner Type (matches getBanners return)
|
||||
interface Banner {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
// import redisClient from './redis-client';
|
||||
import redisClient from 'src/lib/redis-client';
|
||||
import { db } from '../db/db_index';
|
||||
import { productInfo, units, productSlots, deliverySlotInfo, specialDeals, storeInfo, productTags, productTagInfo } from '../db/schema';
|
||||
// import redisClient from '@/src/stores/redis-client';
|
||||
import redisClient from '@/src/lib/redis-client';
|
||||
import { db } from '@/src/db/db_index'
|
||||
import { productInfo, units, productSlots, deliverySlotInfo, specialDeals, storeInfo, productTags, productTagInfo } from '@/src/db/schema'
|
||||
import { eq, and, gt, sql } from 'drizzle-orm';
|
||||
import { generateSignedUrlsFromS3Urls, scaffoldAssetUrl } from 'src/lib/s3-client';
|
||||
import { generateSignedUrlsFromS3Urls, scaffoldAssetUrl } from '@/src/lib/s3-client';
|
||||
|
||||
// Uniform Product Type (matches getProductDetails return)
|
||||
interface Product {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
// import redisClient from './redis-client';
|
||||
import redisClient from 'src/lib/redis-client';
|
||||
import { db } from '../db/db_index';
|
||||
import { productTagInfo, productTags } from '../db/schema';
|
||||
// import redisClient from '@/src/stores/redis-client';
|
||||
import redisClient from '@/src/lib/redis-client';
|
||||
import { db } from '@/src/db/db_index'
|
||||
import { productTagInfo, productTags } from '@/src/db/schema'
|
||||
import { eq, inArray } from 'drizzle-orm';
|
||||
import { generateSignedUrlFromS3Url } from 'src/lib/s3-client';
|
||||
import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client';
|
||||
|
||||
// Tag Type (matches getDashboardTags return)
|
||||
interface Tag {
|
||||
|
|
@ -168,4 +168,4 @@ export async function getTagsByStoreId(storeId: number): Promise<Tag[]> {
|
|||
console.error(`Error getting tags for store ${storeId}:`, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import redisClient from 'src/lib/redis-client';
|
||||
import { db } from '../db/db_index';
|
||||
import { deliverySlotInfo, productSlots, productInfo, units } from '../db/schema';
|
||||
import redisClient from '@/src/lib/redis-client';
|
||||
import { db } from '@/src/db/db_index'
|
||||
import { deliverySlotInfo, productSlots, productInfo, units } from '@/src/db/schema'
|
||||
import { eq, and, gt, asc } from 'drizzle-orm';
|
||||
import { generateSignedUrlsFromS3Urls, scaffoldAssetUrl } from 'src/lib/s3-client';
|
||||
import { generateSignedUrlsFromS3Urls, scaffoldAssetUrl } from '@/src/lib/s3-client';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
// Define the structure for slot with products
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import roleManager from '../lib/roles-manager';
|
||||
import { computeConstants } from '../lib/const-store';
|
||||
import { initializeProducts } from './product-store';
|
||||
import { initializeProductTagStore } from './product-tag-store';
|
||||
import { initializeSlotStore } from './slot-store';
|
||||
import { initializeBannerStore } from './banner-store';
|
||||
import roleManager from '@/src/lib/roles-manager'
|
||||
import { computeConstants } from '@/src/lib/const-store'
|
||||
import { initializeProducts } from '@/src/stores/product-store'
|
||||
import { initializeProductTagStore } from '@/src/stores/product-tag-store'
|
||||
import { initializeSlotStore } from '@/src/stores/slot-store'
|
||||
import { initializeBannerStore } from '@/src/stores/banner-store'
|
||||
|
||||
/**
|
||||
* Initialize all application stores
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import redisClient from 'src/lib/redis-client';
|
||||
import { db } from '../db/db_index';
|
||||
import { userIncidents } from '../db/schema';
|
||||
import redisClient from '@/src/lib/redis-client';
|
||||
import { db } from '@/src/db/db_index'
|
||||
import { userIncidents } from '@/src/db/schema'
|
||||
import { eq, sum } from 'drizzle-orm';
|
||||
|
||||
export async function initializeUserNegativityStore(): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
import { router } from '../trpc-index';
|
||||
import { complaintRouter } from './complaint';
|
||||
import { couponRouter } from './coupon';
|
||||
import { cancelledOrdersRouter } from './cancelled-orders';
|
||||
import { orderRouter } from './order';
|
||||
import { vendorSnippetsRouter } from './vendor-snippets';
|
||||
import { slotsRouter } from './slots';
|
||||
import { productRouter } from './product';
|
||||
import { staffUserRouter } from './staff-user';
|
||||
import { storeRouter } from './store';
|
||||
import { adminPaymentsRouter } from './payments';
|
||||
import addressRouter from './address';
|
||||
import { bannerRouter } from './banner';
|
||||
import { userRouter } from './user';
|
||||
import { constRouter } from './const';
|
||||
|
||||
export const adminRouter = router({
|
||||
complaint: complaintRouter,
|
||||
coupon: couponRouter,
|
||||
cancelledOrders: cancelledOrdersRouter,
|
||||
order: orderRouter,
|
||||
vendorSnippets: vendorSnippetsRouter,
|
||||
slots: slotsRouter,
|
||||
product: productRouter,
|
||||
staffUser: staffUserRouter,
|
||||
store: storeRouter,
|
||||
payments: adminPaymentsRouter,
|
||||
address: addressRouter,
|
||||
banner: bannerRouter,
|
||||
user: userRouter,
|
||||
const: constRouter,
|
||||
});
|
||||
|
||||
export type AdminRouter = typeof adminRouter;
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import { z } from 'zod';
|
||||
import { addressZones, addressAreas } from '../../db/schema';
|
||||
import { addressZones, addressAreas } from '@/src/db/schema'
|
||||
import { eq, desc } from 'drizzle-orm';
|
||||
import { db } from '../../db/db_index';
|
||||
import { router,protectedProcedure } from '../trpc-index';
|
||||
import { db } from '@/src/db/db_index'
|
||||
import { router,protectedProcedure } from '@/src/trpc/trpc-index'
|
||||
|
||||
const addressRouter = router({
|
||||
getZones: protectedProcedure.query(async () => {
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
// import { router } from '@/src/trpc/trpc-index';
|
||||
import { router } from '@/src/trpc/trpc-index'
|
||||
import { complaintRouter } from '@/src/trpc/apis/admin-apis/apis/complaint'
|
||||
import { couponRouter } from '@/src/trpc/apis/admin-apis/apis/coupon'
|
||||
import { cancelledOrdersRouter } from '@/src/trpc/apis/admin-apis/apis/cancelled-orders'
|
||||
import { orderRouter } from '@/src/trpc/apis/admin-apis/apis/order'
|
||||
import { vendorSnippetsRouter } from '@/src/trpc/apis/admin-apis/apis/vendor-snippets'
|
||||
import { slotsRouter } from '@/src/trpc/apis/admin-apis/apis/slots'
|
||||
import { productRouter } from '@/src/trpc/apis/admin-apis/apis/product'
|
||||
import { staffUserRouter } from '@/src/trpc/apis/admin-apis/apis/staff-user'
|
||||
import { storeRouter } from '@/src/trpc/apis/admin-apis/apis/store'
|
||||
import { adminPaymentsRouter } from '@/src/trpc/apis/admin-apis/apis/payments'
|
||||
import addressRouter from '@/src/trpc/apis/admin-apis/apis/address'
|
||||
import { bannerRouter } from '@/src/trpc/apis/admin-apis/apis/banner'
|
||||
import { userRouter } from '@/src/trpc/apis/admin-apis/apis/user'
|
||||
import { constRouter } from '@/src/trpc/apis/admin-apis/apis/const'
|
||||
|
||||
export const adminRouter = router({
|
||||
complaint: complaintRouter,
|
||||
coupon: couponRouter,
|
||||
cancelledOrders: cancelledOrdersRouter,
|
||||
order: orderRouter,
|
||||
vendorSnippets: vendorSnippetsRouter,
|
||||
slots: slotsRouter,
|
||||
product: productRouter,
|
||||
staffUser: staffUserRouter,
|
||||
store: storeRouter,
|
||||
payments: adminPaymentsRouter,
|
||||
address: addressRouter,
|
||||
banner: bannerRouter,
|
||||
user: userRouter,
|
||||
const: constRouter,
|
||||
});
|
||||
|
||||
export type AdminRouter = typeof adminRouter;
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import { z } from 'zod';
|
||||
import { db } from '../../db/db_index';
|
||||
import { homeBanners } from '../../db/schema';
|
||||
import { db } from '@/src/db/db_index'
|
||||
import { homeBanners } from '@/src/db/schema'
|
||||
import { eq, and, desc, sql } from 'drizzle-orm';
|
||||
import { protectedProcedure, router } from '../trpc-index';
|
||||
import { extractKeyFromPresignedUrl, generateSignedUrlFromS3Url } from '../../lib/s3-client';
|
||||
import { ApiError } from 'src/lib/api-error';
|
||||
import { initializeAllStores } from '../../stores/store-initializer';
|
||||
import { protectedProcedure, router } from '@/src/trpc/trpc-index'
|
||||
import { extractKeyFromPresignedUrl, generateSignedUrlFromS3Url } from '@/src/lib/s3-client'
|
||||
import { ApiError } from '@/src/lib/api-error';
|
||||
import { initializeAllStores } from '@/src/stores/store-initializer'
|
||||
|
||||
export const bannerRouter = router({
|
||||
// Get all banners
|
||||
|
|
@ -171,4 +171,4 @@ export const bannerRouter = router({
|
|||
|
||||
return { success: true };
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { router, protectedProcedure } from '../trpc-index';
|
||||
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
|
||||
import { z } from 'zod';
|
||||
import { db } from '../../db/db_index';
|
||||
import { orders, orderStatus, users, addresses, orderItems, productInfo, units, refunds } from '../../db/schema';
|
||||
import { db } from '@/src/db/db_index'
|
||||
import { orders, orderStatus, users, addresses, orderItems, productInfo, units, refunds } from '@/src/db/schema'
|
||||
import { eq, desc } from 'drizzle-orm';
|
||||
|
||||
const updateCancellationReviewSchema = z.object({
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import { router, protectedProcedure } from '../trpc-index';
|
||||
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
|
||||
import { z } from 'zod';
|
||||
import { db } from '../../db/db_index';
|
||||
import { complaints, users } from '../../db/schema';
|
||||
import { db } from '@/src/db/db_index'
|
||||
import { complaints, users } from '@/src/db/schema'
|
||||
import { eq, desc, lt, and } from 'drizzle-orm';
|
||||
import { generateSignedUrlsFromS3Urls } from '../../lib/s3-client';
|
||||
import { generateSignedUrlsFromS3Urls } from '@/src/lib/s3-client'
|
||||
|
||||
export const complaintRouter = router({
|
||||
getAll: protectedProcedure
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import { router, protectedProcedure } from '../trpc-index';
|
||||
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
|
||||
import { z } from 'zod';
|
||||
import { db } from '../../db/db_index';
|
||||
import { keyValStore } from '../../db/schema';
|
||||
import { computeConstants } from '../../lib/const-store';
|
||||
import { CONST_KEYS } from '../../lib/const-keys';
|
||||
import { db } from '@/src/db/db_index'
|
||||
import { keyValStore } from '@/src/db/schema'
|
||||
import { computeConstants } from '@/src/lib/const-store'
|
||||
import { CONST_KEYS } from '@/src/lib/const-keys'
|
||||
|
||||
export const constRouter = router({
|
||||
getConstants: protectedProcedure
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { router, protectedProcedure } from '../trpc-index';
|
||||
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
|
||||
import { z } from 'zod';
|
||||
import { db } from '../../db/db_index';
|
||||
import { coupons, users, staffUsers, orders, couponApplicableUsers, couponApplicableProducts, orderStatus, reservedCoupons } from '../../db/schema';
|
||||
import { db } from '@/src/db/db_index'
|
||||
import { coupons, users, staffUsers, orders, couponApplicableUsers, couponApplicableProducts, orderStatus, reservedCoupons } from '@/src/db/schema'
|
||||
import { eq, and, like, or, inArray, lt } from 'drizzle-orm';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { router, protectedProcedure } from "../trpc-index";
|
||||
import { router, protectedProcedure } from "@/src/trpc/trpc-index"
|
||||
import { z } from "zod";
|
||||
import { db } from "../../db/db_index";
|
||||
import { db } from "@/src/db/db_index"
|
||||
import {
|
||||
orders,
|
||||
orderItems,
|
||||
|
|
@ -10,17 +10,17 @@ import {
|
|||
refunds,
|
||||
coupons,
|
||||
couponUsage,
|
||||
} from "../../db/schema";
|
||||
} from "@/src/db/schema";
|
||||
import { eq, and, gte, lt, desc, SQL, inArray } from "drizzle-orm";
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import { ApiError } from "../../lib/api-error";
|
||||
import { ApiError } from "@/src/lib/api-error"
|
||||
import {
|
||||
sendOrderPackagedNotification,
|
||||
sendOrderDeliveredNotification,
|
||||
} from "../../lib/notif-job";
|
||||
import { publishCancellation } from "../../lib/post-order-handler";
|
||||
import { getMultipleUserNegativityScores } from "../../stores/user-negativity-store";
|
||||
} from "@/src/lib/notif-job";
|
||||
import { publishCancellation } from "@/src/lib/post-order-handler"
|
||||
import { getMultipleUserNegativityScores } from "@/src/stores/user-negativity-store"
|
||||
|
||||
const updateOrderNotesSchema = z.object({
|
||||
orderId: z.number(),
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
import { router, protectedProcedure } from "../trpc-index";
|
||||
import { router, protectedProcedure } from "@/src/trpc/trpc-index"
|
||||
import { z } from "zod";
|
||||
import { db } from "../../db/db_index";
|
||||
import { db } from "@/src/db/db_index"
|
||||
import {
|
||||
orders,
|
||||
orderStatus,
|
||||
payments,
|
||||
refunds,
|
||||
} from "../../db/schema";
|
||||
} from "@/src/db/schema";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { ApiError } from "../../lib/api-error";
|
||||
import { RazorpayPaymentService } from "../../lib/payments-utils";
|
||||
import { ApiError } from "@/src/lib/api-error"
|
||||
import { RazorpayPaymentService } from "@/src/lib/payments-utils"
|
||||
|
||||
const initiateRefundSchema = z
|
||||
.object({
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
import { router, protectedProcedure } from '../trpc-index';
|
||||
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
|
||||
import { z } from 'zod';
|
||||
import { db } from '../../db/db_index';
|
||||
import { productInfo, units, specialDeals, productSlots, productTags, productReviews, users, productGroupInfo, productGroupMembership } from '../../db/schema';
|
||||
import { db } from '@/src/db/db_index'
|
||||
import { productInfo, units, specialDeals, productSlots, productTags, productReviews, users, productGroupInfo, productGroupMembership } from '@/src/db/schema'
|
||||
import { eq, and, inArray, desc, sql } from 'drizzle-orm';
|
||||
import { ApiError } from '../../lib/api-error';
|
||||
import { imageUploadS3, generateSignedUrlsFromS3Urls, getOriginalUrlFromSignedUrl, claimUploadUrl } from '../../lib/s3-client';
|
||||
import { deleteS3Image } from '../../lib/delete-image';
|
||||
import type { SpecialDeal } from '../../db/types';
|
||||
import { initializeAllStores } from '../../stores/store-initializer';
|
||||
import { ApiError } from '@/src/lib/api-error'
|
||||
import { imageUploadS3, generateSignedUrlsFromS3Urls, getOriginalUrlFromSignedUrl, claimUploadUrl } from '@/src/lib/s3-client'
|
||||
import { deleteS3Image } from '@/src/lib/delete-image'
|
||||
import type { SpecialDeal } from '@/src/db/types'
|
||||
import { initializeAllStores } from '@/src/stores/store-initializer'
|
||||
|
||||
type CreateDeal = {
|
||||
quantity: number;
|
||||
|
|
@ -335,7 +335,7 @@ export const productRouter = router({
|
|||
|
||||
// Claim upload URLs
|
||||
if (uploadUrls && uploadUrls.length > 0) {
|
||||
// const { claimUploadUrl } = await import('../../lib/s3-client');
|
||||
// const { claimUploadUrl } = await import('@/src/lib/s3-client');
|
||||
await Promise.all(uploadUrls.map(url => claimUploadUrl(url)));
|
||||
}
|
||||
|
||||
|
|
@ -531,4 +531,4 @@ export const productRouter = router({
|
|||
updatedCount: updates.length,
|
||||
};
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
import { router, protectedProcedure } from "../trpc-index";
|
||||
import { router, protectedProcedure } from "@/src/trpc/trpc-index"
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { db } from "../../db/db_index";
|
||||
import { deliverySlotInfo, productSlots, productInfo, vendorSnippets, productGroupInfo } from "../../db/schema";
|
||||
import { db } from "@/src/db/db_index"
|
||||
import { deliverySlotInfo, productSlots, productInfo, vendorSnippets, productGroupInfo } from "@/src/db/schema"
|
||||
import { eq, inArray, and, desc } from "drizzle-orm";
|
||||
import { ApiError } from "../../lib/api-error";
|
||||
import { appUrl } from "../../lib/env-exporter";
|
||||
import redisClient from "../../lib/redis-client";
|
||||
import { getSlotSequenceKey } from "../../lib/redisKeyGetters";
|
||||
import { initializeAllStores } from '../../stores/store-initializer';
|
||||
import { ApiError } from "@/src/lib/api-error"
|
||||
import { appUrl } from "@/src/lib/env-exporter"
|
||||
import redisClient from "@/src/lib/redis-client"
|
||||
import { getSlotSequenceKey } from "@/src/lib/redisKeyGetters"
|
||||
import { initializeAllStores } from '@/src/stores/store-initializer'
|
||||
|
||||
interface CachedDeliverySequence {
|
||||
[userId: string]: number[];
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import { router, publicProcedure, protectedProcedure } from '../trpc-index';
|
||||
import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index'
|
||||
import { z } from 'zod';
|
||||
import { db } from '../../db/db_index';
|
||||
import { staffUsers, staffRoles, users, userDetails, orders } from '../../db/schema';
|
||||
import { db } from '@/src/db/db_index'
|
||||
import { staffUsers, staffRoles, users, userDetails, orders } from '@/src/db/schema'
|
||||
import { eq, or, ilike, and, lt, desc } from 'drizzle-orm';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { ApiError } from '../../lib/api-error';
|
||||
import { ApiError } from '@/src/lib/api-error'
|
||||
|
||||
export const staffUserRouter = router({
|
||||
login: publicProcedure
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import { router, protectedProcedure } from '../trpc-index';
|
||||
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
|
||||
import { z } from 'zod';
|
||||
import { db } from '../../db/db_index';
|
||||
import { storeInfo, productInfo } from '../../db/schema';
|
||||
import { db } from '@/src/db/db_index'
|
||||
import { storeInfo, productInfo } from '@/src/db/schema'
|
||||
import { eq, inArray } from 'drizzle-orm';
|
||||
import { ApiError } from '../../lib/api-error';
|
||||
import { extractKeyFromPresignedUrl, deleteImageUtil, generateSignedUrlFromS3Url } from '../../lib/s3-client';
|
||||
import { ApiError } from '@/src/lib/api-error'
|
||||
import { extractKeyFromPresignedUrl, deleteImageUtil, generateSignedUrlFromS3Url } from '@/src/lib/s3-client'
|
||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||
import { initializeAllStores } from '../../stores/store-initializer';
|
||||
import { initializeAllStores } from '@/src/stores/store-initializer'
|
||||
|
||||
export const storeRouter = router({
|
||||
getStores: protectedProcedure
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import { protectedProcedure } from '../trpc-index';
|
||||
import { protectedProcedure } from '@/src/trpc/trpc-index';
|
||||
import { z } from 'zod';
|
||||
import { db } from '../../db/db_index';
|
||||
import { users, complaints, orders, orderItems, notifCreds, unloggedUserTokens, userDetails, userIncidents } from '../../db/schema';
|
||||
import { db } from '@/src/db/db_index';
|
||||
import { users, complaints, orders, orderItems, notifCreds, unloggedUserTokens, userDetails, userIncidents } from '@/src/db/schema';
|
||||
import { eq, sql, desc, asc, count, max, inArray } from 'drizzle-orm';
|
||||
import { ApiError } from '../../lib/api-error';
|
||||
import { notificationQueue } from '../../lib/notif-job';
|
||||
import { recomputeUserNegativityScore } from '../../stores/user-negativity-store';
|
||||
import { ApiError } from '@/src/lib/api-error';
|
||||
import { notificationQueue } from '@/src/lib/notif-job';
|
||||
import { recomputeUserNegativityScore } from '@/src/stores/user-negativity-store';
|
||||
|
||||
async function createUserByMobile(mobile: string): Promise<typeof users.$inferSelect> {
|
||||
// Clean mobile number (remove non-digits)
|
||||
|
|
@ -212,7 +212,7 @@ export const userRouter = {
|
|||
let orderStatuses: { orderId: number; isDelivered: boolean; isCancelled: boolean }[] = [];
|
||||
|
||||
if (orderIds.length > 0) {
|
||||
const { orderStatus } = await import('../../db/schema');
|
||||
const { orderStatus } = await import('@/src/db/schema');
|
||||
orderStatuses = await db
|
||||
.select({
|
||||
orderId: orderStatus.orderId,
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import { router, publicProcedure, protectedProcedure } from '../trpc-index';
|
||||
import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index'
|
||||
import { z } from 'zod';
|
||||
import dayjs from 'dayjs';
|
||||
import { db } from '../../db/db_index';
|
||||
import { vendorSnippets, deliverySlotInfo, productInfo, orders, orderItems, users, orderStatus } from '../../db/schema';
|
||||
import { db } from '@/src/db/db_index'
|
||||
import { vendorSnippets, deliverySlotInfo, productInfo, orders, orderItems, users, orderStatus } from '@/src/db/schema'
|
||||
import { eq, and, inArray, isNotNull, gt, sql, asc, ne } from 'drizzle-orm';
|
||||
import { appUrl } from '../../lib/env-exporter';
|
||||
import { appUrl } from '@/src/lib/env-exporter'
|
||||
|
||||
const createSnippetSchema = z.object({
|
||||
snippetCode: z.string().min(1, "Snippet code is required"),
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
import { router, publicProcedure, protectedProcedure } from '../trpc-index';
|
||||
import { commonRouter } from './common';
|
||||
import { db } from '../../db/db_index';
|
||||
import { keyValStore, productInfo, storeInfo } from '../../db/schema';
|
||||
import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index'
|
||||
import { commonRouter } from '@/src/trpc/apis/common-apis/common'
|
||||
import { db } from '@/src/db/db_index'
|
||||
import { keyValStore, productInfo, storeInfo } from '@/src/db/schema'
|
||||
import * as turf from '@turf/turf';
|
||||
import { z } from 'zod';
|
||||
import { mbnrGeoJson } from '../../lib/mbnr-geojson';
|
||||
import { generateUploadUrl } from '../../lib/s3-client';
|
||||
import { ApiError } from '../../lib/api-error';
|
||||
import { getAllConstValues } from '../../lib/const-store';
|
||||
import { CONST_KEYS } from '../../lib/const-keys';
|
||||
import { mbnrGeoJson } from '@/src/lib/mbnr-geojson'
|
||||
import { generateUploadUrl } from '@/src/lib/s3-client'
|
||||
import { ApiError } from '@/src/lib/api-error'
|
||||
import { getAllConstValues } from '@/src/lib/const-store'
|
||||
import { CONST_KEYS } from '@/src/lib/const-keys'
|
||||
|
||||
const polygon = turf.polygon(mbnrGeoJson.features[0].geometry.coordinates);
|
||||
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import { router, publicProcedure } from '../trpc-index';
|
||||
import { db } from '../../db/db_index';
|
||||
import { productInfo, units, productSlots, deliverySlotInfo, storeInfo, productTags, productTagInfo } from '../../db/schema';
|
||||
import { router, publicProcedure } from '@/src/trpc/trpc-index'
|
||||
import { db } from '@/src/db/db_index'
|
||||
import { productInfo, units, productSlots, deliverySlotInfo, storeInfo, productTags, productTagInfo } from '@/src/db/schema'
|
||||
import { eq, gt, and, sql, inArray } from 'drizzle-orm';
|
||||
import { generateSignedUrlsFromS3Urls, generateSignedUrlFromS3Url } from '../../lib/s3-client';
|
||||
import { generateSignedUrlsFromS3Urls, generateSignedUrlFromS3Url } from '@/src/lib/s3-client'
|
||||
import { z } from 'zod';
|
||||
import { getAllProducts as getAllProductsFromCache } from '../../stores/product-store';
|
||||
import { getDashboardTags as getDashboardTagsFromCache } from '../../stores/product-tag-store';
|
||||
import { getAllProducts as getAllProductsFromCache } from '@/src/stores/product-store'
|
||||
import { getDashboardTags as getDashboardTagsFromCache } from '@/src/stores/product-tag-store'
|
||||
import Fuse from 'fuse.js';
|
||||
|
||||
export const getNextDeliveryDate = async (productId: number): Promise<Date | null> => {
|
||||
0
apps/backend/src/trpc/apis/user-apis/apis/address.ts
Normal file
0
apps/backend/src/trpc/apis/user-apis/apis/address.ts
Normal file
0
apps/backend/src/trpc/apis/user-apis/apis/auth.ts
Normal file
0
apps/backend/src/trpc/apis/user-apis/apis/auth.ts
Normal file
0
apps/backend/src/trpc/apis/user-apis/apis/banners.ts
Normal file
0
apps/backend/src/trpc/apis/user-apis/apis/banners.ts
Normal file
0
apps/backend/src/trpc/apis/user-apis/apis/cart.ts
Normal file
0
apps/backend/src/trpc/apis/user-apis/apis/cart.ts
Normal file
0
apps/backend/src/trpc/apis/user-apis/apis/complaint.ts
Normal file
0
apps/backend/src/trpc/apis/user-apis/apis/complaint.ts
Normal file
0
apps/backend/src/trpc/apis/user-apis/apis/coupon.ts
Normal file
0
apps/backend/src/trpc/apis/user-apis/apis/coupon.ts
Normal file
0
apps/backend/src/trpc/apis/user-apis/apis/file-upload.ts
Normal file
0
apps/backend/src/trpc/apis/user-apis/apis/file-upload.ts
Normal file
0
apps/backend/src/trpc/apis/user-apis/apis/order.ts
Normal file
0
apps/backend/src/trpc/apis/user-apis/apis/order.ts
Normal file
0
apps/backend/src/trpc/apis/user-apis/apis/payments.ts
Normal file
0
apps/backend/src/trpc/apis/user-apis/apis/payments.ts
Normal file
0
apps/backend/src/trpc/apis/user-apis/apis/product.ts
Normal file
0
apps/backend/src/trpc/apis/user-apis/apis/product.ts
Normal file
0
apps/backend/src/trpc/apis/user-apis/apis/slots.ts
Normal file
0
apps/backend/src/trpc/apis/user-apis/apis/slots.ts
Normal file
0
apps/backend/src/trpc/apis/user-apis/apis/stores.ts
Normal file
0
apps/backend/src/trpc/apis/user-apis/apis/stores.ts
Normal file
0
apps/backend/src/trpc/apis/user-apis/apis/tags.ts
Normal file
0
apps/backend/src/trpc/apis/user-apis/apis/tags.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { router } from '@/src/trpc/trpc-index'
|
||||
|
||||
export const userRouter = router({})
|
||||
|
||||
export type UserRouter = typeof userRouter
|
||||
0
apps/backend/src/trpc/apis/user-apis/apis/user.ts
Normal file
0
apps/backend/src/trpc/apis/user-apis/apis/user.ts
Normal file
|
|
@ -1,8 +1,8 @@
|
|||
import { router, publicProcedure } from './trpc-index';
|
||||
import { router, publicProcedure } from '@/src/trpc/trpc-index'
|
||||
import { z } from 'zod';
|
||||
import { adminRouter } from './admin-apis/admin-trpc-index';
|
||||
import { userRouter } from './user-apis/user-trpc-index';
|
||||
import { commonApiRouter } from './common-apis/common-trpc-index';
|
||||
import { adminRouter } from '@/src/trpc/apis/admin-apis/apis/admin-trpc-index'
|
||||
import { userRouter } from '@/src/trpc/apis/user-apis/apis/user-trpc-index'
|
||||
import { commonApiRouter } from '@/src/trpc/apis/common-apis/common-trpc-index'
|
||||
|
||||
// Create the main app router
|
||||
export const appRouter = router({
|
||||
|
|
|
|||
|
|
@ -1,194 +0,0 @@
|
|||
import { router, protectedProcedure } from '../trpc-index';
|
||||
import { z } from 'zod';
|
||||
import { db } from '../../db/db_index';
|
||||
import { addresses, orders, orderStatus, deliverySlotInfo } from '../../db/schema';
|
||||
import { eq, and, gte } from 'drizzle-orm';
|
||||
import dayjs from 'dayjs';
|
||||
import { extractCoordsFromRedirectUrl } from '../../lib/license-util';
|
||||
|
||||
export const addressRouter = router({
|
||||
getDefaultAddress: protectedProcedure
|
||||
.query(async ({ ctx }) => {
|
||||
const userId = ctx.user.userId;
|
||||
|
||||
const [defaultAddress] = await db
|
||||
.select()
|
||||
.from(addresses)
|
||||
.where(and(eq(addresses.userId, userId), eq(addresses.isDefault, true)))
|
||||
.limit(1);
|
||||
|
||||
return { success: true, data: defaultAddress || null };
|
||||
}),
|
||||
|
||||
getUserAddresses: protectedProcedure
|
||||
.query(async ({ ctx }) => {
|
||||
const userId = ctx.user.userId;
|
||||
const userAddresses = await db.select().from(addresses).where(eq(addresses.userId, userId));
|
||||
return { success: true, data: userAddresses };
|
||||
}),
|
||||
|
||||
createAddress: protectedProcedure
|
||||
.input(z.object({
|
||||
name: z.string().min(1, 'Name is required'),
|
||||
phone: z.string().min(1, 'Phone is required'),
|
||||
addressLine1: z.string().min(1, 'Address line 1 is required'),
|
||||
addressLine2: z.string().optional(),
|
||||
city: z.string().min(1, 'City is required'),
|
||||
state: z.string().min(1, 'State is required'),
|
||||
pincode: z.string().min(1, 'Pincode is required'),
|
||||
isDefault: z.boolean().optional(),
|
||||
latitude: z.number().optional(),
|
||||
longitude: z.number().optional(),
|
||||
googleMapsUrl: z.string().optional(),
|
||||
}))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const userId = ctx.user.userId;
|
||||
const { name, phone, addressLine1, addressLine2, city, state, pincode, isDefault, googleMapsUrl } = input;
|
||||
|
||||
let { latitude, longitude } = input;
|
||||
|
||||
if (googleMapsUrl && latitude === undefined && longitude === undefined) {
|
||||
const coords = await extractCoordsFromRedirectUrl(googleMapsUrl);
|
||||
if (coords) {
|
||||
latitude = Number(coords.latitude);
|
||||
longitude = Number(coords.longitude);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (!name || !phone || !addressLine1 || !city || !state || !pincode) {
|
||||
throw new Error('Missing required fields');
|
||||
}
|
||||
|
||||
// If setting as default, unset other defaults
|
||||
if (isDefault) {
|
||||
await db.update(addresses).set({ isDefault: false }).where(eq(addresses.userId, userId));
|
||||
}
|
||||
|
||||
const [newAddress] = await db.insert(addresses).values({
|
||||
userId,
|
||||
name,
|
||||
phone,
|
||||
addressLine1,
|
||||
addressLine2,
|
||||
city,
|
||||
state,
|
||||
pincode,
|
||||
isDefault: isDefault || false,
|
||||
latitude,
|
||||
longitude,
|
||||
googleMapsUrl,
|
||||
}).returning();
|
||||
|
||||
return { success: true, data: newAddress };
|
||||
}),
|
||||
|
||||
updateAddress: protectedProcedure
|
||||
.input(z.object({
|
||||
id: z.number().int().positive(),
|
||||
name: z.string().min(1, 'Name is required'),
|
||||
phone: z.string().min(1, 'Phone is required'),
|
||||
addressLine1: z.string().min(1, 'Address line 1 is required'),
|
||||
addressLine2: z.string().optional(),
|
||||
city: z.string().min(1, 'City is required'),
|
||||
state: z.string().min(1, 'State is required'),
|
||||
pincode: z.string().min(1, 'Pincode is required'),
|
||||
isDefault: z.boolean().optional(),
|
||||
latitude: z.number().optional(),
|
||||
longitude: z.number().optional(),
|
||||
googleMapsUrl: z.string().optional(),
|
||||
}))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const userId = ctx.user.userId;
|
||||
const { id, name, phone, addressLine1, addressLine2, city, state, pincode, isDefault, googleMapsUrl } = input;
|
||||
|
||||
let { latitude, longitude } = input;
|
||||
|
||||
if (googleMapsUrl && latitude === undefined && longitude === undefined) {
|
||||
const coords = await extractCoordsFromRedirectUrl(googleMapsUrl);
|
||||
if (coords) {
|
||||
latitude = Number(coords.latitude);
|
||||
longitude = Number(coords.longitude);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if address exists and belongs to user
|
||||
const existingAddress = await db.select().from(addresses).where(and(eq(addresses.id, id), eq(addresses.userId, userId))).limit(1);
|
||||
if (existingAddress.length === 0) {
|
||||
throw new Error('Address not found');
|
||||
}
|
||||
|
||||
// If setting as default, unset other defaults
|
||||
if (isDefault) {
|
||||
await db.update(addresses).set({ isDefault: false }).where(eq(addresses.userId, userId));
|
||||
}
|
||||
|
||||
const updateData: any = {
|
||||
name,
|
||||
phone,
|
||||
addressLine1,
|
||||
addressLine2,
|
||||
city,
|
||||
state,
|
||||
pincode,
|
||||
isDefault: isDefault || false,
|
||||
googleMapsUrl,
|
||||
};
|
||||
|
||||
if (latitude !== undefined) {
|
||||
updateData.latitude = latitude;
|
||||
}
|
||||
if (longitude !== undefined) {
|
||||
updateData.longitude = longitude;
|
||||
}
|
||||
|
||||
const [updatedAddress] = await db.update(addresses).set(updateData).where(and(eq(addresses.id, id), eq(addresses.userId, userId))).returning();
|
||||
|
||||
return { success: true, data: updatedAddress };
|
||||
}),
|
||||
|
||||
deleteAddress: protectedProcedure
|
||||
.input(z.object({
|
||||
id: z.number().int().positive(),
|
||||
}))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const userId = ctx.user.userId;
|
||||
const { id } = input;
|
||||
|
||||
// Check if address exists and belongs to user
|
||||
const existingAddress = await db.select().from(addresses).where(and(eq(addresses.id, id), eq(addresses.userId, userId))).limit(1);
|
||||
if (existingAddress.length === 0) {
|
||||
throw new Error('Address not found or does not belong to user');
|
||||
}
|
||||
|
||||
// Check if address is attached to any ongoing orders using joins
|
||||
const ongoingOrders = await db.select({
|
||||
order: orders,
|
||||
status: orderStatus,
|
||||
slot: deliverySlotInfo
|
||||
})
|
||||
.from(orders)
|
||||
.innerJoin(orderStatus, eq(orders.id, orderStatus.orderId))
|
||||
.innerJoin(deliverySlotInfo, eq(orders.slotId, deliverySlotInfo.id))
|
||||
.where(and(
|
||||
eq(orders.addressId, id),
|
||||
eq(orderStatus.isCancelled, false),
|
||||
gte(deliverySlotInfo.deliveryTime, new Date())
|
||||
))
|
||||
.limit(1);
|
||||
|
||||
if (ongoingOrders.length > 0) {
|
||||
throw new Error('Address is attached to an ongoing order. Please cancel the order first.');
|
||||
}
|
||||
|
||||
// Prevent deletion of default address
|
||||
if (existingAddress[0].isDefault) {
|
||||
throw new Error('Cannot delete default address. Please set another address as default first.');
|
||||
}
|
||||
|
||||
// Delete the address
|
||||
await db.delete(addresses).where(and(eq(addresses.id, id), eq(addresses.userId, userId)));
|
||||
|
||||
return { success: true, message: 'Address deleted successfully' };
|
||||
}),
|
||||
});
|
||||
|
|
@ -1,447 +0,0 @@
|
|||
import { router, publicProcedure, protectedProcedure } from '../trpc-index';
|
||||
import { z } from 'zod';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { eq } from 'drizzle-orm';
|
||||
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 { generateSignedUrlFromS3Url } from '../../lib/s3-client';
|
||||
import { ApiError } from '../../lib/api-error';
|
||||
import catchAsync from '../../lib/catch-async';
|
||||
import { jwtSecret } from 'src/lib/env-exporter';
|
||||
import { sendOtp, verifyOtpUtil, getOtpCreds } from '../../lib/otp-utils';
|
||||
|
||||
interface LoginRequest {
|
||||
identifier: string; // email or mobile
|
||||
password: string;
|
||||
}
|
||||
|
||||
interface RegisterRequest {
|
||||
name: string;
|
||||
email: string;
|
||||
mobile: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
interface AuthResponse {
|
||||
token: string;
|
||||
user: {
|
||||
id: number;
|
||||
name?: string | null;
|
||||
email: string | null;
|
||||
mobile: string | null;
|
||||
createdAt: string;
|
||||
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 authRouter = router({
|
||||
login: publicProcedure
|
||||
.input(z.object({
|
||||
identifier: z.string().min(1, 'Email/mobile is required'),
|
||||
password: z.string().min(1, 'Password is required'),
|
||||
}))
|
||||
.mutation(async ({ input }) => {
|
||||
const { identifier, password }: LoginRequest = input;
|
||||
|
||||
if (!identifier || !password) {
|
||||
throw new ApiError('Email/mobile and password are required', 400);
|
||||
}
|
||||
|
||||
// Find user by email or mobile
|
||||
const [user] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.email, identifier.toLowerCase()))
|
||||
.limit(1);
|
||||
|
||||
let foundUser = user;
|
||||
|
||||
if (!foundUser) {
|
||||
// Try mobile if email didn't work
|
||||
const [userByMobile] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.mobile, identifier))
|
||||
.limit(1);
|
||||
foundUser = userByMobile;
|
||||
}
|
||||
|
||||
if (!foundUser) {
|
||||
throw new ApiError('Invalid credentials', 401);
|
||||
}
|
||||
|
||||
// Get user credentials
|
||||
const [userCredentials] = await db
|
||||
.select()
|
||||
.from(userCreds)
|
||||
.where(eq(userCreds.userId, foundUser.id))
|
||||
.limit(1);
|
||||
|
||||
if (!userCredentials) {
|
||||
throw new ApiError('Account setup incomplete. Please contact support.', 401);
|
||||
}
|
||||
|
||||
// Get user details for profile image
|
||||
const [userDetail] = await db
|
||||
.select()
|
||||
.from(userDetails)
|
||||
.where(eq(userDetails.userId, foundUser.id))
|
||||
.limit(1);
|
||||
|
||||
// Generate signed URL for profile image if it exists
|
||||
const profileImageSignedUrl = userDetail?.profileImage
|
||||
? await generateSignedUrlFromS3Url(userDetail.profileImage)
|
||||
: null;
|
||||
|
||||
// Verify password
|
||||
const isPasswordValid = await bcrypt.compare(password, userCredentials.userPassword);
|
||||
if (!isPasswordValid) {
|
||||
throw new ApiError('Invalid credentials', 401);
|
||||
}
|
||||
|
||||
const token = generateToken(foundUser.id);
|
||||
|
||||
const response: AuthResponse = {
|
||||
token,
|
||||
user: {
|
||||
id: foundUser.id,
|
||||
name: foundUser.name,
|
||||
email: foundUser.email,
|
||||
mobile: foundUser.mobile,
|
||||
createdAt: foundUser.createdAt.toISOString(),
|
||||
profileImage: profileImageSignedUrl,
|
||||
bio: userDetail?.bio || null,
|
||||
dateOfBirth: userDetail?.dateOfBirth || null,
|
||||
gender: userDetail?.gender || null,
|
||||
occupation: userDetail?.occupation || null,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: response,
|
||||
};
|
||||
}),
|
||||
|
||||
register: publicProcedure
|
||||
.input(z.object({
|
||||
name: z.string().min(1, 'Name is required'),
|
||||
email: z.string().email('Invalid email format'),
|
||||
mobile: z.string().min(1, 'Mobile is required'),
|
||||
password: z.string().min(1, 'Password is required'),
|
||||
}))
|
||||
.mutation(async ({ input }) => {
|
||||
const { name, email, mobile, password }: RegisterRequest = input;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// Check if email already exists
|
||||
const [existingEmail] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.email, email.toLowerCase()))
|
||||
.limit(1);
|
||||
|
||||
if (existingEmail) {
|
||||
throw new ApiError('Email already registered', 409);
|
||||
}
|
||||
|
||||
// Check if mobile already exists
|
||||
const [existingMobile] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.mobile, cleanMobile))
|
||||
.limit(1);
|
||||
|
||||
if (existingMobile) {
|
||||
throw new ApiError('Mobile number already registered', 409);
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const hashedPassword = await bcrypt.hash(password, 12);
|
||||
|
||||
// Create user and credentials in a transaction
|
||||
const newUser = await db.transaction(async (tx) => {
|
||||
// Create user
|
||||
const [user] = await tx
|
||||
.insert(users)
|
||||
.values({
|
||||
name: name.trim(),
|
||||
email: email.toLowerCase().trim(),
|
||||
mobile: cleanMobile,
|
||||
})
|
||||
.returning();
|
||||
|
||||
// Create user credentials
|
||||
await tx
|
||||
.insert(userCreds)
|
||||
.values({
|
||||
userId: user.id,
|
||||
userPassword: hashedPassword,
|
||||
});
|
||||
|
||||
return user;
|
||||
});
|
||||
|
||||
const token = generateToken(newUser.id);
|
||||
|
||||
const response: AuthResponse = {
|
||||
token,
|
||||
user: {
|
||||
id: newUser.id,
|
||||
name: newUser.name,
|
||||
email: newUser.email,
|
||||
mobile: newUser.mobile,
|
||||
createdAt: newUser.createdAt.toISOString(),
|
||||
profileImage: null,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: response,
|
||||
};
|
||||
}),
|
||||
|
||||
sendOtp: publicProcedure
|
||||
.input(z.object({
|
||||
mobile: z.string(),
|
||||
}))
|
||||
.mutation(async ({ input }) => {
|
||||
|
||||
return await sendOtp(input.mobile);
|
||||
}),
|
||||
|
||||
verifyOtp: publicProcedure
|
||||
.input(z.object({
|
||||
mobile: z.string(),
|
||||
otp: z.string(),
|
||||
}))
|
||||
.mutation(async ({ input }) => {
|
||||
const verificationId = getOtpCreds(input.mobile);
|
||||
if (!verificationId) {
|
||||
throw new ApiError("OTP not sent or expired", 400);
|
||||
}
|
||||
const isVerified = await verifyOtpUtil(input.mobile, input.otp, verificationId);
|
||||
|
||||
if (!isVerified) {
|
||||
throw new ApiError("Invalid OTP", 400);
|
||||
}
|
||||
|
||||
// Find user
|
||||
let user = await db.query.users.findFirst({
|
||||
where: eq(users.mobile, input.mobile),
|
||||
});
|
||||
|
||||
// If user doesn't exist, create one
|
||||
if (!user) {
|
||||
const [newUser] = await db
|
||||
.insert(users)
|
||||
.values({
|
||||
name: null,
|
||||
email: null,
|
||||
mobile: input.mobile,
|
||||
})
|
||||
.returning();
|
||||
user = newUser;
|
||||
}
|
||||
|
||||
// Generate JWT
|
||||
const token = generateToken(user.id);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
token,
|
||||
user: {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
mobile: user.mobile,
|
||||
createdAt: user.createdAt.toISOString(),
|
||||
profileImage: null,
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
updatePassword: protectedProcedure
|
||||
.input(z.object({
|
||||
password: z.string().min(6, 'Password must be at least 6 characters'),
|
||||
}))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const userId = ctx.user.userId;
|
||||
if (!userId) {
|
||||
throw new ApiError('User not authenticated', 401);
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(input.password, 10);
|
||||
|
||||
// Insert if not exists, then update if exists
|
||||
try {
|
||||
await db.insert(userCreds).values({
|
||||
userId: userId,
|
||||
userPassword: hashedPassword,
|
||||
});
|
||||
// Insert succeeded - new credentials created
|
||||
} catch (error: any) {
|
||||
// Insert failed - check if it's a unique constraint violation
|
||||
if (error.code === '23505') { // PostgreSQL unique constraint violation
|
||||
// Update existing credentials
|
||||
await db.update(userCreds).set({
|
||||
userPassword: hashedPassword,
|
||||
}).where(eq(userCreds.userId, userId));
|
||||
} else {
|
||||
// Re-throw if it's a different error
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, message: 'Password updated successfully' };
|
||||
}),
|
||||
|
||||
getProfile: protectedProcedure
|
||||
.query(async ({ ctx }) => {
|
||||
const userId = ctx.user.userId;
|
||||
|
||||
if (!userId) {
|
||||
throw new ApiError('User not authenticated', 401);
|
||||
}
|
||||
|
||||
const [user] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.id, userId))
|
||||
.limit(1);
|
||||
|
||||
if (!user) {
|
||||
throw new ApiError('User not found', 404);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
mobile: user.mobile,
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
deleteAccount: protectedProcedure
|
||||
.input(z.object({
|
||||
mobile: z.string().min(10, 'Mobile number is required'),
|
||||
}))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user.userId;
|
||||
const { mobile } = input;
|
||||
|
||||
if (!userId) {
|
||||
throw new ApiError('User not authenticated', 401);
|
||||
}
|
||||
|
||||
// Double-check: verify user exists and is the authenticated user
|
||||
const existingUser = await db.query.users.findFirst({
|
||||
where: eq(users.id, userId),
|
||||
columns: { id: true, mobile: true },
|
||||
});
|
||||
|
||||
if (!existingUser) {
|
||||
throw new ApiError('User not found', 404);
|
||||
}
|
||||
|
||||
// Additional verification: ensure we're not deleting someone else's data
|
||||
// The JWT token should already ensure this, but double-checking
|
||||
if (existingUser.id !== userId) {
|
||||
throw new ApiError('Unauthorized: Cannot delete another user\'s account', 403);
|
||||
}
|
||||
|
||||
// Verify mobile number matches user's registered mobile
|
||||
const cleanInputMobile = mobile.replace(/\D/g, '');
|
||||
const cleanUserMobile = existingUser.mobile?.replace(/\D/g, '');
|
||||
|
||||
if (cleanInputMobile !== cleanUserMobile) {
|
||||
throw new ApiError('Mobile number does not match your registered number', 400);
|
||||
}
|
||||
|
||||
// Use transaction for atomic deletion
|
||||
await db.transaction(async (tx) => {
|
||||
// Phase 1: Direct references (safe to delete first)
|
||||
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));
|
||||
|
||||
// Update reserved coupons (set redeemedBy to null)
|
||||
await tx.update(reservedCoupons)
|
||||
.set({ redeemedBy: null })
|
||||
.where(eq(reservedCoupons.redeemedBy, userId));
|
||||
|
||||
// Phase 2: Order dependencies
|
||||
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));
|
||||
// Additional coupon usage entries linked to specific orders
|
||||
await tx.delete(couponUsage).where(eq(couponUsage.orderId, order.id));
|
||||
await tx.delete(complaints).where(eq(complaints.orderId, order.id));
|
||||
}
|
||||
|
||||
// Delete orders
|
||||
await tx.delete(orders).where(eq(orders.userId, userId));
|
||||
|
||||
// Phase 3: Addresses (now safe since orders are deleted)
|
||||
await tx.delete(addresses).where(eq(addresses.userId, userId));
|
||||
|
||||
// Phase 4: Core user data
|
||||
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));
|
||||
});
|
||||
|
||||
return { success: true, message: 'Account deleted successfully' };
|
||||
}),
|
||||
});
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
import { db } from '../../db/db_index';
|
||||
import { homeBanners } from '../../db/schema';
|
||||
import { publicProcedure, router } from '../trpc-index';
|
||||
import { generateSignedUrlFromS3Url } from '../../lib/s3-client';
|
||||
import { isNotNull, asc } from 'drizzle-orm';
|
||||
|
||||
export const bannerRouter = router({
|
||||
getBanners: publicProcedure
|
||||
.query(async () => {
|
||||
const banners = await db.query.homeBanners.findMany({
|
||||
where: isNotNull(homeBanners.serialNum), // Only show assigned banners
|
||||
orderBy: asc(homeBanners.serialNum), // Order by slot number 1-4
|
||||
});
|
||||
|
||||
// Convert S3 keys to signed URLs for client
|
||||
const bannersWithSignedUrls = await Promise.all(
|
||||
banners.map(async (banner) => {
|
||||
try {
|
||||
return {
|
||||
...banner,
|
||||
imageUrl: banner.imageUrl ? await generateSignedUrlFromS3Url(banner.imageUrl) : banner.imageUrl,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Failed to generate signed URL for banner ${banner.id}:`, error);
|
||||
return {
|
||||
...banner,
|
||||
imageUrl: banner.imageUrl, // Keep original on error
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
return {
|
||||
banners: bannersWithSignedUrls,
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
|
@ -1,244 +0,0 @@
|
|||
import { router, protectedProcedure, publicProcedure } from '../trpc-index';
|
||||
import { z } from 'zod';
|
||||
import { db } from '../../db/db_index';
|
||||
import { cartItems, productInfo, units, productSlots, deliverySlotInfo } from '../../db/schema';
|
||||
import { eq, and, sql, inArray, gt } from 'drizzle-orm';
|
||||
import { ApiError } from '../../lib/api-error';
|
||||
import { generateSignedUrlsFromS3Urls, scaffoldAssetUrl } from '../../lib/s3-client';
|
||||
import { getProductSlots, getMultipleProductsSlots } from '../../stores/slot-store';
|
||||
|
||||
interface CartResponse {
|
||||
items: any[];
|
||||
totalItems: number;
|
||||
totalAmount: number;
|
||||
}
|
||||
|
||||
const getCartData = async (userId: number): Promise<CartResponse> => {
|
||||
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));
|
||||
|
||||
// Generate signed URLs for images
|
||||
const cartWithSignedUrls = await Promise.all(
|
||||
cartItemsWithProducts.map(async (item) => ({
|
||||
id: item.cartId,
|
||||
productId: item.productId,
|
||||
quantity: parseFloat(item.quantity),
|
||||
addedAt: item.addedAt,
|
||||
product: {
|
||||
id: item.productId,
|
||||
name: item.productName,
|
||||
price: item.productPrice,
|
||||
productQuantity: item.productQuantity,
|
||||
unit: item.unitShortNotation,
|
||||
isOutOfStock: item.isOutOfStock,
|
||||
images: scaffoldAssetUrl((item.productImages as string[]) || []),
|
||||
},
|
||||
subtotal: parseFloat(item.productPrice.toString()) * parseFloat(item.quantity),
|
||||
}))
|
||||
);
|
||||
|
||||
const totalAmount = cartWithSignedUrls.reduce((sum, item) => sum + item.subtotal, 0);
|
||||
|
||||
return {
|
||||
items: cartWithSignedUrls,
|
||||
totalItems: cartWithSignedUrls.length,
|
||||
totalAmount,
|
||||
};
|
||||
};
|
||||
|
||||
export const cartRouter = router({
|
||||
getCart: protectedProcedure
|
||||
.query(async ({ ctx }): Promise<CartResponse> => {
|
||||
const userId = ctx.user.userId;
|
||||
return await getCartData(userId);
|
||||
}),
|
||||
|
||||
addToCart: protectedProcedure
|
||||
.input(z.object({
|
||||
productId: z.number().int().positive(),
|
||||
quantity: z.number().int().positive(),
|
||||
}))
|
||||
.mutation(async ({ input, ctx }): Promise<CartResponse> => {
|
||||
const userId = ctx.user.userId;
|
||||
const { productId, quantity } = input;
|
||||
|
||||
// Validate input
|
||||
if (!productId || !quantity || quantity <= 0) {
|
||||
throw new ApiError("Product ID and positive quantity required", 400);
|
||||
}
|
||||
|
||||
// Check if product exists
|
||||
const product = await db.query.productInfo.findFirst({
|
||||
where: eq(productInfo.id, productId),
|
||||
});
|
||||
|
||||
if (!product) {
|
||||
throw new ApiError("Product not found", 404);
|
||||
}
|
||||
|
||||
// Check if item already exists in cart
|
||||
const existingItem = await db.query.cartItems.findFirst({
|
||||
where: and(eq(cartItems.userId, userId), eq(cartItems.productId, productId)),
|
||||
});
|
||||
|
||||
if (existingItem) {
|
||||
// Update quantity
|
||||
await db.update(cartItems)
|
||||
.set({
|
||||
quantity: sql`${cartItems.quantity} + ${quantity}`,
|
||||
})
|
||||
.where(eq(cartItems.id, existingItem.id));
|
||||
} else {
|
||||
// Insert new item
|
||||
await db.insert(cartItems).values({
|
||||
userId,
|
||||
productId,
|
||||
quantity: quantity.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
// Return updated cart
|
||||
return await getCartData(userId);
|
||||
}),
|
||||
|
||||
updateCartItem: protectedProcedure
|
||||
.input(z.object({
|
||||
itemId: z.number().int().positive(),
|
||||
quantity: z.number().int().min(0),
|
||||
}))
|
||||
.mutation(async ({ input, ctx }): Promise<CartResponse> => {
|
||||
const userId = ctx.user.userId;
|
||||
const { itemId, quantity } = input;
|
||||
|
||||
if (!quantity || quantity <= 0) {
|
||||
throw new ApiError("Positive quantity required", 400);
|
||||
}
|
||||
|
||||
const [updatedItem] = await db.update(cartItems)
|
||||
.set({ quantity: quantity.toString() })
|
||||
.where(and(
|
||||
eq(cartItems.id, itemId),
|
||||
eq(cartItems.userId, userId)
|
||||
))
|
||||
.returning();
|
||||
|
||||
if (!updatedItem) {
|
||||
throw new ApiError("Cart item not found", 404);
|
||||
}
|
||||
|
||||
// Return updated cart
|
||||
return await getCartData(userId);
|
||||
}),
|
||||
|
||||
removeFromCart: protectedProcedure
|
||||
.input(z.object({
|
||||
itemId: z.number().int().positive(),
|
||||
}))
|
||||
.mutation(async ({ input, ctx }): Promise<CartResponse> => {
|
||||
const userId = ctx.user.userId;
|
||||
const { itemId } = input;
|
||||
|
||||
const [deletedItem] = await db.delete(cartItems)
|
||||
.where(and(
|
||||
eq(cartItems.id, itemId),
|
||||
eq(cartItems.userId, userId)
|
||||
))
|
||||
.returning();
|
||||
|
||||
if (!deletedItem) {
|
||||
throw new ApiError("Cart item not found", 404);
|
||||
}
|
||||
|
||||
// Return updated cart
|
||||
return await getCartData(userId);
|
||||
}),
|
||||
|
||||
clearCart: protectedProcedure
|
||||
.mutation(async ({ ctx }) => {
|
||||
const userId = ctx.user.userId;
|
||||
|
||||
await db.delete(cartItems).where(eq(cartItems.userId, userId));
|
||||
|
||||
return {
|
||||
items: [],
|
||||
totalItems: 0,
|
||||
totalAmount: 0,
|
||||
message: "Cart cleared successfully",
|
||||
};
|
||||
}),
|
||||
|
||||
// Original DB-based getCartSlots (commented out)
|
||||
// getCartSlots: publicProcedure
|
||||
// .input(z.object({
|
||||
// productIds: z.array(z.number().int().positive())
|
||||
// }))
|
||||
// .query(async ({ input }) => {
|
||||
// const { productIds } = input;
|
||||
//
|
||||
// if (productIds.length === 0) {
|
||||
// return {};
|
||||
// }
|
||||
//
|
||||
// // Get slots for these products where freeze time is after current time
|
||||
// const slotsData = await db
|
||||
// .select({
|
||||
// productId: productSlots.productId,
|
||||
// slotId: deliverySlotInfo.id,
|
||||
// deliveryTime: deliverySlotInfo.deliveryTime,
|
||||
// freezeTime: deliverySlotInfo.freezeTime,
|
||||
// isActive: deliverySlotInfo.isActive,
|
||||
// })
|
||||
// .from(productSlots)
|
||||
// .innerJoin(deliverySlotInfo, eq(productSlots.slotId, deliverySlotInfo.id))
|
||||
// .where(and(
|
||||
// inArray(productSlots.productId, productIds),
|
||||
// gt(deliverySlotInfo.freezeTime, sql`NOW()`),
|
||||
// eq(deliverySlotInfo.isActive, true)
|
||||
// ));
|
||||
//
|
||||
// // Group by productId
|
||||
// const result: Record<number, any[]> = {};
|
||||
// slotsData.forEach(slot => {
|
||||
// if (!result[slot.productId]) {
|
||||
// result[slot.productId] = [];
|
||||
// }
|
||||
// result[slot.productId].push({
|
||||
// id: slot.slotId,
|
||||
// deliveryTime: slot.deliveryTime,
|
||||
// freezeTime: slot.freezeTime,
|
||||
// });
|
||||
// });
|
||||
//
|
||||
// return result;
|
||||
// }),
|
||||
|
||||
// Cache-based getCartSlots
|
||||
getCartSlots: publicProcedure
|
||||
.input(z.object({
|
||||
productIds: z.array(z.number().int().positive())
|
||||
}))
|
||||
.query(async ({ input }) => {
|
||||
const { productIds } = input;
|
||||
|
||||
if (productIds.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return await getMultipleProductsSlots(productIds);
|
||||
}),
|
||||
});
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
import { router, protectedProcedure } from '../trpc-index';
|
||||
import { z } from 'zod';
|
||||
import { db } from '../../db/db_index';
|
||||
import { complaints } from '../../db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
export const complaintRouter = router({
|
||||
getAll: protectedProcedure
|
||||
.query(async ({ ctx }) => {
|
||||
const userId = ctx.user.userId;
|
||||
|
||||
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(complaints.createdAt);
|
||||
|
||||
return {
|
||||
complaints: userComplaints.map(c => ({
|
||||
id: c.id,
|
||||
complaintBody: c.complaintBody,
|
||||
response: c.response,
|
||||
isResolved: c.isResolved,
|
||||
createdAt: c.createdAt,
|
||||
orderId: c.orderId,
|
||||
})),
|
||||
};
|
||||
}),
|
||||
|
||||
raise: protectedProcedure
|
||||
.input(z.object({
|
||||
orderId: z.string().optional(),
|
||||
complaintBody: z.string().min(1, 'Complaint body is required'),
|
||||
}))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const userId = ctx.user.userId;
|
||||
const { orderId, complaintBody } = input;
|
||||
|
||||
let orderIdNum: number | null = null;
|
||||
|
||||
if (orderId) {
|
||||
const readableIdMatch = orderId.match(/^ORD(\d+)$/);
|
||||
if (readableIdMatch) {
|
||||
orderIdNum = parseInt(readableIdMatch[1]);
|
||||
}
|
||||
}
|
||||
|
||||
await db.insert(complaints).values({
|
||||
userId,
|
||||
orderId: orderIdNum,
|
||||
complaintBody: complaintBody.trim(),
|
||||
});
|
||||
|
||||
return { success: true, message: 'Complaint raised successfully' };
|
||||
}),
|
||||
});
|
||||
|
|
@ -1,296 +0,0 @@
|
|||
import { router, protectedProcedure } from '../trpc-index';
|
||||
import { z } from 'zod';
|
||||
import { db } from '../../db/db_index';
|
||||
import { coupons, couponUsage, couponApplicableUsers, reservedCoupons, couponApplicableProducts } from '../../db/schema';
|
||||
import { eq, and, or, gt, isNull, sql } from 'drizzle-orm';
|
||||
import { ApiError } from 'src/lib/api-error';
|
||||
|
||||
import { users } from '../../db/schema';
|
||||
|
||||
type CouponWithRelations = typeof coupons.$inferSelect & {
|
||||
applicableUsers: (typeof couponApplicableUsers.$inferSelect & { user: typeof users.$inferSelect })[];
|
||||
usages: typeof couponUsage.$inferSelect[];
|
||||
};
|
||||
|
||||
export interface EligibleCoupon {
|
||||
id: number;
|
||||
code: string;
|
||||
discountType: 'percentage' | 'flat';
|
||||
discountValue: number;
|
||||
maxValue?: number;
|
||||
minOrder?: number;
|
||||
description: string;
|
||||
exclusiveApply?: boolean;
|
||||
isEligible: boolean;
|
||||
ineligibilityReason?: string;
|
||||
}
|
||||
|
||||
const generateCouponDescription = (coupon: any): string => {
|
||||
let desc = '';
|
||||
|
||||
if (coupon.discountPercent) {
|
||||
desc += `${coupon.discountPercent}% off`;
|
||||
} else if (coupon.flatDiscount) {
|
||||
desc += `₹${coupon.flatDiscount} off`;
|
||||
}
|
||||
|
||||
if (coupon.minOrder) {
|
||||
desc += ` on orders above ₹${coupon.minOrder}`;
|
||||
}
|
||||
|
||||
if (coupon.maxValue) {
|
||||
desc += ` (max discount ₹${coupon.maxValue})`;
|
||||
}
|
||||
|
||||
return desc;
|
||||
};
|
||||
|
||||
export interface CouponDisplay {
|
||||
id: number;
|
||||
code: string;
|
||||
discountType: 'percentage' | 'flat';
|
||||
discountValue: number;
|
||||
maxValue?: number;
|
||||
minOrder?: number;
|
||||
description: string;
|
||||
validTill?: Date;
|
||||
usageCount: number;
|
||||
maxLimitForUser?: number;
|
||||
isExpired: boolean;
|
||||
isUsedUp: boolean;
|
||||
}
|
||||
|
||||
export const userCouponRouter = router({
|
||||
getEligible: protectedProcedure
|
||||
.query(async ({ ctx }) => {
|
||||
try {
|
||||
|
||||
const userId = ctx.user.userId;
|
||||
|
||||
// Get all active, non-expired coupons
|
||||
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: {
|
||||
with: {
|
||||
user: true
|
||||
}
|
||||
},
|
||||
applicableProducts: {
|
||||
with: {
|
||||
product: true
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
// Filter to only coupons applicable to current user
|
||||
const applicableCoupons = allCoupons.filter(coupon => {
|
||||
if(!coupon.isUserBased) return true;
|
||||
const applicableUsers = coupon.applicableUsers || [];
|
||||
return applicableUsers.some(au => au.userId === userId);
|
||||
});
|
||||
|
||||
return { success: true, data: applicableCoupons };
|
||||
}
|
||||
catch(e) {
|
||||
console.log(e)
|
||||
throw new ApiError("Unable to get coupons")
|
||||
}
|
||||
}),
|
||||
|
||||
getProductCoupons: protectedProcedure
|
||||
.input(z.object({ productId: z.number().int().positive() }))
|
||||
.query(async ({ input, ctx }) => {
|
||||
const userId = ctx.user.userId;
|
||||
const { productId } = input;
|
||||
|
||||
// Get all active, non-expired coupons
|
||||
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: {
|
||||
with: {
|
||||
user: true
|
||||
}
|
||||
},
|
||||
applicableProducts: {
|
||||
with: {
|
||||
product: true
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
// Filter to only coupons applicable to current user and product
|
||||
const applicableCoupons = allCoupons.filter(coupon => {
|
||||
const applicableUsers = coupon.applicableUsers || [];
|
||||
const userApplicable = !coupon.isUserBased || applicableUsers.some(au => au.userId === userId);
|
||||
|
||||
const applicableProducts = coupon.applicableProducts || [];
|
||||
const productApplicable = applicableProducts.length === 0 || applicableProducts.some(ap => ap.productId === productId);
|
||||
|
||||
return userApplicable && productApplicable;
|
||||
});
|
||||
|
||||
return { success: true, data: applicableCoupons };
|
||||
}),
|
||||
|
||||
getMyCoupons: protectedProcedure
|
||||
.query(async ({ ctx }) => {
|
||||
const userId = ctx.user.userId;
|
||||
|
||||
// Get all coupons
|
||||
const allCoupons = await db.query.coupons.findMany({
|
||||
with: {
|
||||
usages: {
|
||||
where: eq(couponUsage.userId, userId)
|
||||
},
|
||||
applicableUsers: {
|
||||
with: {
|
||||
user: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Filter coupons in JS: not invalidated, applicable to user, and not expired
|
||||
const applicableCoupons = (allCoupons as CouponWithRelations[]).filter(coupon => {
|
||||
const isNotInvalidated = !coupon.isInvalidated;
|
||||
const applicableUsers = coupon.applicableUsers || [];
|
||||
const isApplicable = coupon.isApplyForAll || applicableUsers.some(au => au.userId === userId);
|
||||
const isNotExpired = !coupon.validTill || new Date(coupon.validTill) > new Date();
|
||||
return isNotInvalidated && isApplicable && isNotExpired;
|
||||
});
|
||||
|
||||
// Categorize coupons
|
||||
const personalCoupons: CouponDisplay[] = [];
|
||||
const generalCoupons: CouponDisplay[] = [];
|
||||
|
||||
applicableCoupons.forEach(coupon => {
|
||||
const usageCount = coupon.usages.length;
|
||||
const isExpired = false; // Already filtered out expired coupons
|
||||
const isUsedUp = Boolean(coupon.maxLimitForUser && usageCount >= coupon.maxLimitForUser);
|
||||
|
||||
const couponDisplay: CouponDisplay = {
|
||||
id: coupon.id,
|
||||
code: coupon.couponCode,
|
||||
discountType: coupon.discountPercent ? 'percentage' : 'flat',
|
||||
discountValue: parseFloat(coupon.discountPercent || coupon.flatDiscount || '0'),
|
||||
maxValue: coupon.maxValue ? parseFloat(coupon.maxValue) : undefined,
|
||||
minOrder: coupon.minOrder ? parseFloat(coupon.minOrder) : undefined,
|
||||
description: generateCouponDescription(coupon),
|
||||
validTill: coupon.validTill ? new Date(coupon.validTill) : undefined,
|
||||
usageCount,
|
||||
maxLimitForUser: coupon.maxLimitForUser ? parseInt(coupon.maxLimitForUser.toString()) : undefined,
|
||||
isExpired,
|
||||
isUsedUp,
|
||||
};
|
||||
|
||||
if ((coupon.applicableUsers || []).some(au => au.userId === userId) && !coupon.isApplyForAll) {
|
||||
// Personal coupon
|
||||
personalCoupons.push(couponDisplay);
|
||||
} else if (coupon.isApplyForAll) {
|
||||
// General coupon
|
||||
generalCoupons.push(couponDisplay);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
personal: personalCoupons,
|
||||
general: generalCoupons,
|
||||
}
|
||||
};
|
||||
}),
|
||||
|
||||
redeemReservedCoupon: protectedProcedure
|
||||
.input(z.object({ secretCode: z.string() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const userId = ctx.user.userId;
|
||||
const { secretCode } = input;
|
||||
|
||||
// Find the reserved coupon
|
||||
const reservedCoupon = await db.query.reservedCoupons.findFirst({
|
||||
where: and(
|
||||
eq(reservedCoupons.secretCode, secretCode.toUpperCase()),
|
||||
eq(reservedCoupons.isRedeemed, false)
|
||||
),
|
||||
});
|
||||
|
||||
if (!reservedCoupon) {
|
||||
throw new ApiError("Invalid or already redeemed coupon code", 400);
|
||||
}
|
||||
|
||||
// Check if already redeemed by this user (in case of multiple attempts)
|
||||
if (reservedCoupon.redeemedBy === userId) {
|
||||
throw new ApiError("You have already redeemed this coupon", 400);
|
||||
}
|
||||
|
||||
// Create the coupon in the main table
|
||||
const couponResult = await db.transaction(async (tx) => {
|
||||
// Insert into coupons
|
||||
const couponInsert = 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();
|
||||
|
||||
const coupon = couponInsert[0];
|
||||
|
||||
// Insert into couponApplicableUsers
|
||||
await tx.insert(couponApplicableUsers).values({
|
||||
couponId: coupon.id,
|
||||
userId,
|
||||
});
|
||||
|
||||
// Copy applicable products
|
||||
if (reservedCoupon.productIds && Array.isArray(reservedCoupon.productIds) && reservedCoupon.productIds.length > 0) {
|
||||
// Assuming productIds are the IDs, but wait, in schema, productIds is jsonb, but in relations, couponApplicableProducts has productId
|
||||
// For simplicity, since reservedCoupons has productIds as jsonb, but to match, perhaps insert into couponApplicableProducts if needed
|
||||
// But in createReservedCoupon, I inserted applicableProducts into couponApplicableProducts
|
||||
// So for reserved, perhaps do the same, but since it's jsonb, maybe not.
|
||||
// For now, skip, as the coupon will have productIds in coupons table.
|
||||
}
|
||||
|
||||
// Update reserved coupon as redeemed
|
||||
await tx.update(reservedCoupons).set({
|
||||
isRedeemed: true,
|
||||
redeemedBy: userId,
|
||||
redeemedAt: new Date(),
|
||||
}).where(eq(reservedCoupons.id, reservedCoupon.id));
|
||||
|
||||
return coupon;
|
||||
});
|
||||
|
||||
return { success: true, coupon: couponResult };
|
||||
}),
|
||||
});
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
import { router, protectedProcedure } from '../trpc-index';
|
||||
import { z } from 'zod';
|
||||
import { generateUploadUrl } from '../../lib/s3-client';
|
||||
import { ApiError } from '../../lib/api-error';
|
||||
|
||||
export const fileUploadRouter = router({
|
||||
generateUploadUrls: protectedProcedure
|
||||
.input(z.object({
|
||||
contextString: z.enum(['review', 'product_info', 'notification']),
|
||||
mimeTypes: z.array(z.string()),
|
||||
}))
|
||||
.mutation(async ({ input }): Promise<{ uploadUrls: string[] }> => {
|
||||
const { contextString, mimeTypes } = input;
|
||||
|
||||
const uploadUrls: string[] = [];
|
||||
const keys: string[] = [];
|
||||
|
||||
for (const mimeType of mimeTypes) {
|
||||
// Generate key based on context and mime type
|
||||
let folder: string;
|
||||
if (contextString === 'review') {
|
||||
folder = 'review-images';
|
||||
} else if(contextString === 'product_info') {
|
||||
folder = 'product-images';
|
||||
}
|
||||
// else if(contextString === 'review_response') {
|
||||
// folder = 'review-response-images'
|
||||
// }
|
||||
else if(contextString === 'notification') {
|
||||
folder = 'notification-images'
|
||||
} else {
|
||||
folder = '';
|
||||
}
|
||||
|
||||
const extension = mimeType === 'image/jpeg' ? '.jpg' :
|
||||
mimeType === 'image/png' ? '.png' :
|
||||
mimeType === 'image/gif' ? '.gif' : '.jpg';
|
||||
const key = `${folder}/${Date.now()}${extension}`;
|
||||
|
||||
try {
|
||||
const uploadUrl = await generateUploadUrl(key, mimeType);
|
||||
uploadUrls.push(uploadUrl);
|
||||
keys.push(key);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error generating upload URL:', error);
|
||||
throw new ApiError('Failed to generate upload URL', 500);
|
||||
}
|
||||
}
|
||||
|
||||
return { uploadUrls };
|
||||
}),
|
||||
});
|
||||
|
||||
export type FileUploadRouter = typeof fileUploadRouter;
|
||||
|
|
@ -1,989 +0,0 @@
|
|||
import { router, protectedProcedure } from "../trpc-index";
|
||||
import { z } from "zod";
|
||||
import { db } from "../../db/db_index";
|
||||
import {
|
||||
orders,
|
||||
orderItems,
|
||||
orderStatus,
|
||||
addresses,
|
||||
productInfo,
|
||||
paymentInfoTable,
|
||||
coupons,
|
||||
couponUsage,
|
||||
payments,
|
||||
cartItems,
|
||||
refunds,
|
||||
units,
|
||||
userDetails,
|
||||
} from "../../db/schema";
|
||||
import { eq, and, inArray, desc, gte, lte } from "drizzle-orm";
|
||||
import { scaffoldAssetUrl } from "../../lib/s3-client";
|
||||
import { ApiError } from "../../lib/api-error";
|
||||
import {
|
||||
sendOrderPlacedNotification,
|
||||
sendOrderCancelledNotification,
|
||||
} from "../../lib/notif-job";
|
||||
import { RazorpayPaymentService } from "../../lib/payments-utils";
|
||||
import { getNextDeliveryDate } from "../common-apis/common";
|
||||
import { CONST_KEYS, getConstant, getConstants } from "../../lib/const-store";
|
||||
import { publishFormattedOrder, publishCancellation } from "../../lib/post-order-handler";
|
||||
import { getSlotById } from "../../stores/slot-store";
|
||||
|
||||
|
||||
const validateAndGetCoupon = async (
|
||||
couponId: number | undefined,
|
||||
userId: number,
|
||||
totalAmount: number
|
||||
) => {
|
||||
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 ApiError("Invalid coupon", 400);
|
||||
if (coupon.isInvalidated)
|
||||
throw new ApiError("Coupon is no longer valid", 400);
|
||||
if (coupon.validTill && new Date(coupon.validTill) < new Date())
|
||||
throw new ApiError("Coupon has expired", 400);
|
||||
if (
|
||||
coupon.maxLimitForUser &&
|
||||
coupon.usages.length >= coupon.maxLimitForUser
|
||||
)
|
||||
throw new ApiError("Coupon usage limit exceeded", 400);
|
||||
if (
|
||||
coupon.minOrder &&
|
||||
parseFloat(coupon.minOrder.toString()) > totalAmount
|
||||
)
|
||||
throw new ApiError(
|
||||
"Order amount does not meet coupon minimum requirement",
|
||||
400
|
||||
);
|
||||
|
||||
return coupon;
|
||||
};
|
||||
|
||||
const applyDiscountToOrder = (
|
||||
orderTotal: number,
|
||||
appliedCoupon: typeof coupons.$inferSelect | null,
|
||||
proportion: number
|
||||
) => {
|
||||
let finalOrderTotal = orderTotal;
|
||||
// const proportion = totalAmount / 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;
|
||||
}
|
||||
}
|
||||
|
||||
// let orderDeliveryCharge = 0;
|
||||
// if (isFirstOrder && finalOrderTotal < minOrderValue) {
|
||||
// orderDeliveryCharge = deliveryCharge;
|
||||
// finalOrderTotal += deliveryCharge;
|
||||
// }
|
||||
|
||||
|
||||
return { finalOrderTotal, orderGroupProportion: proportion };
|
||||
};
|
||||
|
||||
const placeOrderUtil = async (params: {
|
||||
userId: number;
|
||||
selectedItems: Array<{
|
||||
productId: number;
|
||||
quantity: number;
|
||||
slotId: number | null;
|
||||
}>;
|
||||
addressId: number;
|
||||
paymentMethod: "online" | "cod";
|
||||
couponId?: number;
|
||||
userNotes?: string;
|
||||
isFlash?: boolean;
|
||||
}) => {
|
||||
const {
|
||||
userId,
|
||||
selectedItems,
|
||||
addressId,
|
||||
paymentMethod,
|
||||
couponId,
|
||||
userNotes,
|
||||
} = params;
|
||||
|
||||
const constants = await getConstants<number>([
|
||||
CONST_KEYS.minRegularOrderValue,
|
||||
CONST_KEYS.deliveryCharge,
|
||||
CONST_KEYS.flashFreeDeliveryThreshold,
|
||||
CONST_KEYS.flashDeliveryCharge,
|
||||
]);
|
||||
|
||||
const isFlashDelivery = params.isFlash;
|
||||
const minOrderValue = (isFlashDelivery ? constants[CONST_KEYS.flashFreeDeliveryThreshold] : constants[CONST_KEYS.minRegularOrderValue]) || 0;
|
||||
const deliveryCharge = (isFlashDelivery ? constants[CONST_KEYS.flashDeliveryCharge] : constants[CONST_KEYS.deliveryCharge]) || 0;
|
||||
|
||||
const orderGroupId = `${Date.now()}-${userId}`;
|
||||
|
||||
const address = await db.query.addresses.findFirst({
|
||||
where: and(eq(addresses.userId, userId), eq(addresses.id, addressId)),
|
||||
});
|
||||
if (!address) {
|
||||
throw new ApiError("Invalid address", 400);
|
||||
}
|
||||
|
||||
const ordersBySlot = new Map<
|
||||
number | null,
|
||||
Array<{
|
||||
productId: number;
|
||||
quantity: number;
|
||||
slotId: number | null;
|
||||
product: any;
|
||||
}>
|
||||
>();
|
||||
|
||||
for (const item of selectedItems) {
|
||||
const product = await db.query.productInfo.findFirst({
|
||||
where: eq(productInfo.id, item.productId),
|
||||
});
|
||||
if (!product) {
|
||||
throw new ApiError(`Product ${item.productId} not found`, 400);
|
||||
}
|
||||
|
||||
if (!ordersBySlot.has(item.slotId)) {
|
||||
ordersBySlot.set(item.slotId, []);
|
||||
}
|
||||
ordersBySlot.get(item.slotId)!.push({ ...item, product });
|
||||
}
|
||||
|
||||
if (params.isFlash) {
|
||||
for (const item of selectedItems) {
|
||||
const product = await db.query.productInfo.findFirst({
|
||||
where: eq(productInfo.id, item.productId),
|
||||
});
|
||||
if (!product?.isFlashAvailable) {
|
||||
throw new ApiError(`Product ${item.productId} is not available for flash delivery`, 400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let totalAmount = 0;
|
||||
for (const [slotId, items] of ordersBySlot) {
|
||||
const orderTotal = items.reduce(
|
||||
(sum, item) => {
|
||||
const itemPrice = params.isFlash
|
||||
? parseFloat((item.product.flashPrice || item.product.price).toString())
|
||||
: parseFloat(item.product.price.toString());
|
||||
return sum + itemPrice * item.quantity;
|
||||
},
|
||||
0
|
||||
);
|
||||
totalAmount += orderTotal;
|
||||
}
|
||||
|
||||
const appliedCoupon = await validateAndGetCoupon(couponId, userId, totalAmount);
|
||||
|
||||
const expectedDeliveryCharge =
|
||||
totalAmount < minOrderValue ? deliveryCharge : 0;
|
||||
|
||||
const totalWithDelivery = totalAmount + expectedDeliveryCharge;
|
||||
|
||||
type OrderData = {
|
||||
order: Omit<typeof orders.$inferInsert, "id">;
|
||||
orderItems: Omit<typeof orderItems.$inferInsert, "id">[];
|
||||
orderStatus: Omit<typeof orderStatus.$inferInsert, "id">;
|
||||
};
|
||||
|
||||
const ordersData: OrderData[] = [];
|
||||
let isFirstOrder = true;
|
||||
|
||||
for (const [slotId, items] of ordersBySlot) {
|
||||
const subOrderTotal = items.reduce(
|
||||
(sum, item) => {
|
||||
const itemPrice = params.isFlash
|
||||
? parseFloat((item.product.flashPrice || item.product.price).toString())
|
||||
: parseFloat(item.product.price.toString());
|
||||
return sum + itemPrice * item.quantity;
|
||||
},
|
||||
0
|
||||
);
|
||||
const subOrderTotalWithDelivery = subOrderTotal + expectedDeliveryCharge;
|
||||
|
||||
const orderGroupProportion = subOrderTotal / totalAmount;
|
||||
const orderTotalAmount = isFirstOrder ? subOrderTotalWithDelivery : subOrderTotal;
|
||||
|
||||
const { finalOrderTotal: finalOrderAmount } = applyDiscountToOrder(
|
||||
orderTotalAmount,
|
||||
appliedCoupon,
|
||||
orderGroupProportion
|
||||
);
|
||||
|
||||
const order: Omit<typeof orders.$inferInsert, "id"> = {
|
||||
userId,
|
||||
addressId,
|
||||
slotId: params.isFlash ? null : slotId,
|
||||
isCod: paymentMethod === "cod",
|
||||
isOnlinePayment: paymentMethod === "online",
|
||||
paymentInfoId: null,
|
||||
totalAmount: finalOrderAmount.toString(),
|
||||
deliveryCharge: isFirstOrder ? expectedDeliveryCharge.toString() : "0",
|
||||
readableId: -1,
|
||||
userNotes: userNotes || null,
|
||||
orderGroupId,
|
||||
orderGroupProportion: orderGroupProportion.toString(),
|
||||
isFlashDelivery: params.isFlash,
|
||||
};
|
||||
|
||||
const orderItemsData: Omit<typeof orderItems.$inferInsert, "id">[] = items.map(
|
||||
(item) => ({
|
||||
orderId: 0,
|
||||
productId: item.productId,
|
||||
quantity: item.quantity.toString(),
|
||||
price: params.isFlash
|
||||
? item.product.flashPrice || item.product.price
|
||||
: item.product.price,
|
||||
discountedPrice: (
|
||||
params.isFlash
|
||||
? item.product.flashPrice || item.product.price
|
||||
: item.product.price
|
||||
).toString(),
|
||||
})
|
||||
);
|
||||
|
||||
const orderStatusData: Omit<typeof orderStatus.$inferInsert, "id"> = {
|
||||
userId,
|
||||
orderId: 0,
|
||||
paymentStatus: paymentMethod === "cod" ? "cod" : "pending",
|
||||
};
|
||||
|
||||
ordersData.push({ order, orderItems: orderItemsData, orderStatus: orderStatusData });
|
||||
isFirstOrder = false;
|
||||
}
|
||||
|
||||
const createdOrders = await 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 as number });
|
||||
});
|
||||
allOrderStatuses.push({
|
||||
...od.orderStatus,
|
||||
orderId: order.id as number,
|
||||
});
|
||||
});
|
||||
|
||||
await tx.insert(orderItems).values(allOrderItems);
|
||||
await tx.insert(orderStatus).values(allOrderStatuses);
|
||||
|
||||
if (paymentMethod === "online" && sharedPaymentInfoId) {
|
||||
const razorpayOrder = await RazorpayPaymentService.createOrder(
|
||||
sharedPaymentInfoId,
|
||||
totalWithDelivery.toString()
|
||||
);
|
||||
await RazorpayPaymentService.insertPaymentRecord(
|
||||
sharedPaymentInfoId,
|
||||
razorpayOrder,
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
return insertedOrders;
|
||||
});
|
||||
|
||||
await db.delete(cartItems).where(
|
||||
and(
|
||||
eq(cartItems.userId, userId),
|
||||
inArray(
|
||||
cartItems.productId,
|
||||
selectedItems.map((item) => item.productId)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
if (appliedCoupon && createdOrders.length > 0) {
|
||||
await db.insert(couponUsage).values({
|
||||
userId,
|
||||
couponId: appliedCoupon.id,
|
||||
orderId: createdOrders[0].id as number,
|
||||
orderItemId: null,
|
||||
usedAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
for (const order of createdOrders) {
|
||||
sendOrderPlacedNotification(userId, order.id.toString());
|
||||
}
|
||||
|
||||
await publishFormattedOrder(createdOrders, ordersBySlot);
|
||||
|
||||
return { success: true, data: createdOrders };
|
||||
};
|
||||
|
||||
export const orderRouter = router({
|
||||
placeOrder: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
selectedItems: z.array(
|
||||
z.object({
|
||||
productId: z.number().int().positive(),
|
||||
quantity: z.number().int().positive(),
|
||||
slotId: z.union([z.number().int(), z.null()]),
|
||||
})
|
||||
),
|
||||
addressId: z.number().int().positive(),
|
||||
paymentMethod: z.enum(["online", "cod"]),
|
||||
couponId: z.number().int().positive().optional(),
|
||||
userNotes: z.string().optional(),
|
||||
isFlashDelivery: z.boolean().optional().default(false),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const userId = ctx.user.userId;
|
||||
|
||||
// Check if user is suspended from placing orders
|
||||
const userDetail = await db.query.userDetails.findFirst({
|
||||
where: eq(userDetails.userId, userId),
|
||||
});
|
||||
|
||||
if (userDetail?.isSuspended) {
|
||||
throw new ApiError("Unable to place order", 403);
|
||||
}
|
||||
|
||||
const {
|
||||
selectedItems,
|
||||
addressId,
|
||||
paymentMethod,
|
||||
couponId,
|
||||
userNotes,
|
||||
isFlashDelivery,
|
||||
} = input;
|
||||
|
||||
// Check if flash delivery is enabled when placing a flash delivery order
|
||||
if (isFlashDelivery) {
|
||||
const isFlashDeliveryEnabled = await getConstant<boolean>(CONST_KEYS.isFlashDeliveryEnabled);
|
||||
if (!isFlashDeliveryEnabled) {
|
||||
throw new ApiError("Flash delivery is currently unavailable. Please opt for scheduled delivery.", 403);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any selected slot is at full capacity (only for regular delivery)
|
||||
if (!isFlashDelivery) {
|
||||
const slotIds = [...new Set(selectedItems.filter(i => i.slotId !== null).map(i => i.slotId as number))];
|
||||
for (const slotId of slotIds) {
|
||||
const slot = await getSlotById(slotId);
|
||||
if (slot?.isCapacityFull) {
|
||||
throw new ApiError("Selected delivery slot is at full capacity. Please choose another slot.", 403);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let processedItems = selectedItems;
|
||||
|
||||
// Handle flash delivery slot resolution
|
||||
if (isFlashDelivery) {
|
||||
// For flash delivery, set slotId to null (no specific slot assigned)
|
||||
processedItems = selectedItems.map(item => ({
|
||||
...item,
|
||||
slotId: null as any, // Type override for flash delivery
|
||||
}));
|
||||
}
|
||||
|
||||
return await placeOrderUtil({
|
||||
userId,
|
||||
selectedItems: processedItems,
|
||||
addressId,
|
||||
paymentMethod,
|
||||
couponId,
|
||||
userNotes,
|
||||
isFlash: isFlashDelivery,
|
||||
});
|
||||
}),
|
||||
|
||||
getOrders: protectedProcedure
|
||||
.input(
|
||||
z
|
||||
.object({
|
||||
page: z.number().min(1).default(1),
|
||||
pageSize: z.number().min(1).max(50).default(10),
|
||||
})
|
||||
.optional()
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { page = 1, pageSize = 10 } = input || {};
|
||||
const userId = ctx.user.userId;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
// Get total count for pagination
|
||||
const totalCountResult = await db.$count(
|
||||
orders,
|
||||
eq(orders.userId, userId)
|
||||
);
|
||||
const totalCount = totalCountResult;
|
||||
|
||||
const userOrders = await db.query.orders.findMany({
|
||||
where: eq(orders.userId, userId),
|
||||
with: {
|
||||
orderItems: {
|
||||
with: {
|
||||
product: true,
|
||||
},
|
||||
},
|
||||
slot: true,
|
||||
paymentInfo: true,
|
||||
orderStatus: true,
|
||||
refunds: true,
|
||||
},
|
||||
orderBy: (orders, { desc }) => [desc(orders.createdAt)],
|
||||
limit: pageSize,
|
||||
offset: offset,
|
||||
});
|
||||
|
||||
const mappedOrders = await Promise.all(
|
||||
userOrders.map(async (order) => {
|
||||
const status = order.orderStatus[0];
|
||||
const refund = order.refunds[0];
|
||||
|
||||
type DeliveryStatus = "cancelled" | "success" | "pending" | "packaged";
|
||||
type OrderStatus = "cancelled" | "success";
|
||||
|
||||
let deliveryStatus: DeliveryStatus;
|
||||
let orderStatus: OrderStatus;
|
||||
|
||||
const allItemsPackaged = order.orderItems.every(
|
||||
(item) => item.is_packaged
|
||||
);
|
||||
|
||||
if (status?.isCancelled) {
|
||||
deliveryStatus = "cancelled";
|
||||
orderStatus = "cancelled";
|
||||
} else if (status?.isDelivered) {
|
||||
deliveryStatus = "success";
|
||||
orderStatus = "success";
|
||||
} else if (allItemsPackaged) {
|
||||
deliveryStatus = "packaged";
|
||||
orderStatus = "success";
|
||||
} else {
|
||||
deliveryStatus = "pending";
|
||||
orderStatus = "success";
|
||||
}
|
||||
|
||||
const paymentMode = order.isCod ? "CoD" : "Online";
|
||||
const paymentStatus = status?.paymentStatus || "pending";
|
||||
const refundStatus = refund?.refundStatus || "none";
|
||||
const refundAmount = refund?.refundAmount
|
||||
? parseFloat(refund.refundAmount.toString())
|
||||
: null;
|
||||
|
||||
const items = await Promise.all(
|
||||
order.orderItems.map(async (item) => {
|
||||
|
||||
const signedImages = item.product.images
|
||||
? scaffoldAssetUrl(
|
||||
item.product.images as string[]
|
||||
)
|
||||
: [];
|
||||
return {
|
||||
productName: item.product.name,
|
||||
quantity: parseFloat(item.quantity),
|
||||
price: parseFloat(item.price.toString()),
|
||||
discountedPrice: parseFloat(
|
||||
item.discountedPrice?.toString() || item.price.toString()
|
||||
),
|
||||
amount:
|
||||
parseFloat(item.price.toString()) * parseFloat(item.quantity),
|
||||
image: signedImages[0] || null,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
id: order.id,
|
||||
orderId: `ORD${order.id}`,
|
||||
orderDate: order.createdAt.toISOString(),
|
||||
deliveryStatus,
|
||||
deliveryDate: order.slot?.deliveryTime.toISOString(),
|
||||
orderStatus,
|
||||
cancelReason: status?.cancelReason || null,
|
||||
paymentMode,
|
||||
totalAmount: Number(order.totalAmount),
|
||||
deliveryCharge: Number(order.deliveryCharge),
|
||||
paymentStatus,
|
||||
refundStatus,
|
||||
refundAmount,
|
||||
userNotes: order.userNotes || null,
|
||||
items,
|
||||
isFlashDelivery: order.isFlashDelivery,
|
||||
createdAt: order.createdAt.toISOString(),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: mappedOrders,
|
||||
pagination: {
|
||||
page,
|
||||
pageSize,
|
||||
totalCount,
|
||||
totalPages: Math.ceil(totalCount / pageSize),
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
getOrderById: protectedProcedure
|
||||
.input(z.object({ orderId: z.string() }))
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { orderId } = input;
|
||||
const userId = ctx.user.userId;
|
||||
|
||||
const order = await db.query.orders.findFirst({
|
||||
where: and(eq(orders.id, parseInt(orderId)), eq(orders.userId, userId)),
|
||||
with: {
|
||||
orderItems: {
|
||||
with: {
|
||||
product: true,
|
||||
},
|
||||
},
|
||||
slot: true,
|
||||
paymentInfo: true,
|
||||
orderStatus: {
|
||||
with: {
|
||||
refundCoupon: true,
|
||||
},
|
||||
},
|
||||
refunds: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!order) {
|
||||
throw new Error("Order not found");
|
||||
}
|
||||
|
||||
// Get coupon usage for this specific order using new orderId field
|
||||
const couponUsageData = await db.query.couponUsage.findMany({
|
||||
where: eq(couponUsage.orderId, order.id), // Use new orderId field
|
||||
with: {
|
||||
coupon: true,
|
||||
},
|
||||
});
|
||||
|
||||
let couponData = null;
|
||||
if (couponUsageData.length > 0) {
|
||||
// Calculate total discount from multiple coupons
|
||||
let totalDiscountAmount = 0;
|
||||
const orderTotal = parseFloat(order.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());
|
||||
}
|
||||
|
||||
// Apply max value limit if set
|
||||
if (
|
||||
usage.coupon.maxValue &&
|
||||
discountAmount > parseFloat(usage.coupon.maxValue.toString())
|
||||
) {
|
||||
discountAmount = parseFloat(usage.coupon.maxValue.toString());
|
||||
}
|
||||
|
||||
totalDiscountAmount += discountAmount;
|
||||
}
|
||||
|
||||
couponData = {
|
||||
couponCode: couponUsageData
|
||||
.map((u) => u.coupon.couponCode)
|
||||
.join(", "),
|
||||
couponDescription: `${couponUsageData.length} coupons applied`,
|
||||
discountAmount: totalDiscountAmount,
|
||||
};
|
||||
}
|
||||
|
||||
const status = order.orderStatus[0];
|
||||
const refund = order.refunds[0];
|
||||
|
||||
type DeliveryStatus = "cancelled" | "success" | "pending" | "packaged";
|
||||
type OrderStatus = "cancelled" | "success";
|
||||
|
||||
let deliveryStatus: DeliveryStatus;
|
||||
let orderStatus: OrderStatus;
|
||||
|
||||
const allItemsPackaged = order.orderItems.every(
|
||||
(item) => item.is_packaged
|
||||
);
|
||||
|
||||
if (status?.isCancelled) {
|
||||
deliveryStatus = "cancelled";
|
||||
orderStatus = "cancelled";
|
||||
} else if (status?.isDelivered) {
|
||||
deliveryStatus = "success";
|
||||
orderStatus = "success";
|
||||
} else if (allItemsPackaged) {
|
||||
deliveryStatus = "packaged";
|
||||
orderStatus = "success";
|
||||
} else {
|
||||
deliveryStatus = "pending";
|
||||
orderStatus = "success";
|
||||
}
|
||||
|
||||
const paymentMode = order.isCod ? "CoD" : "Online";
|
||||
const paymentStatus = status?.paymentStatus || "pending";
|
||||
const refundStatus = refund?.refundStatus || "none";
|
||||
const refundAmount = refund?.refundAmount
|
||||
? parseFloat(refund.refundAmount.toString())
|
||||
: null;
|
||||
|
||||
const items = await Promise.all(
|
||||
order.orderItems.map(async (item) => {
|
||||
const signedImages = item.product.images
|
||||
? scaffoldAssetUrl(
|
||||
item.product.images as string[]
|
||||
)
|
||||
: [];
|
||||
return {
|
||||
productName: item.product.name,
|
||||
quantity: parseFloat(item.quantity),
|
||||
price: parseFloat(item.price.toString()),
|
||||
discountedPrice: parseFloat(
|
||||
item.discountedPrice?.toString() || item.price.toString()
|
||||
),
|
||||
amount:
|
||||
parseFloat(item.price.toString()) * parseFloat(item.quantity),
|
||||
image: signedImages[0] || null,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
id: order.id,
|
||||
orderId: `ORD${order.id}`,
|
||||
orderDate: order.createdAt.toISOString(),
|
||||
deliveryStatus,
|
||||
deliveryDate: order.slot?.deliveryTime.toISOString(),
|
||||
orderStatus: order.orderStatus,
|
||||
cancellationStatus: orderStatus,
|
||||
cancelReason: status?.cancelReason || null,
|
||||
paymentMode,
|
||||
paymentStatus,
|
||||
refundStatus,
|
||||
refundAmount,
|
||||
userNotes: order.userNotes || null,
|
||||
items,
|
||||
couponCode: couponData?.couponCode || null,
|
||||
couponDescription: couponData?.couponDescription || null,
|
||||
discountAmount: couponData?.discountAmount || null,
|
||||
orderAmount: parseFloat(order.totalAmount.toString()),
|
||||
isFlashDelivery: order.isFlashDelivery,
|
||||
createdAt: order.createdAt.toISOString(),
|
||||
};
|
||||
}),
|
||||
|
||||
cancelOrder: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
// id: z.string().regex(/^ORD\d+$/, "Invalid order ID format"),
|
||||
id: z.number(),
|
||||
reason: z.string().min(1, "Cancellation reason is required"),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const userId = ctx.user.userId;
|
||||
const { id, reason } = input;
|
||||
|
||||
// Check if order exists and belongs to user
|
||||
const order = await db.query.orders.findFirst({
|
||||
where: eq(orders.id, Number(id)),
|
||||
with: {
|
||||
orderStatus: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!order) {
|
||||
console.error("Order not found:", id);
|
||||
throw new ApiError("Order not found", 404);
|
||||
}
|
||||
|
||||
if (order.userId !== userId) {
|
||||
console.error("Order does not belong to user:", {
|
||||
orderId: id,
|
||||
orderUserId: order.userId,
|
||||
requestUserId: userId,
|
||||
});
|
||||
|
||||
throw new ApiError("Order not found", 404);
|
||||
}
|
||||
|
||||
const status = order.orderStatus[0];
|
||||
if (!status) {
|
||||
console.error("Order status not found for order:", id);
|
||||
throw new ApiError("Order status not found", 400);
|
||||
}
|
||||
|
||||
if (status.isCancelled) {
|
||||
console.error("Order is already cancelled:", id);
|
||||
throw new ApiError("Order is already cancelled", 400);
|
||||
}
|
||||
|
||||
if (status.isDelivered) {
|
||||
console.error("Cannot cancel delivered order:", id);
|
||||
throw new ApiError("Cannot cancel delivered order", 400);
|
||||
}
|
||||
|
||||
// Perform database operations in transaction
|
||||
const result = await db.transaction(async (tx) => {
|
||||
// Update order status
|
||||
await tx
|
||||
.update(orderStatus)
|
||||
.set({
|
||||
isCancelled: true,
|
||||
cancelReason: reason,
|
||||
cancellationUserNotes: reason,
|
||||
cancellationReviewed: false,
|
||||
})
|
||||
.where(eq(orderStatus.id, status.id));
|
||||
|
||||
// Determine refund status based on payment method
|
||||
const refundStatus = order.isCod ? "na" : "pending";
|
||||
|
||||
// Insert refund record
|
||||
await tx.insert(refunds).values({
|
||||
orderId: order.id,
|
||||
refundStatus,
|
||||
});
|
||||
|
||||
return { orderId: order.id, userId };
|
||||
});
|
||||
|
||||
// Send notification outside transaction (idempotent operation)
|
||||
await sendOrderCancelledNotification(
|
||||
result.userId,
|
||||
result.orderId.toString()
|
||||
);
|
||||
|
||||
// Publish to Redis for Telegram notification
|
||||
await publishCancellation(result.orderId, 'user', reason);
|
||||
|
||||
return { success: true, message: "Order cancelled successfully" };
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
throw new ApiError("failed to cancel order");
|
||||
}
|
||||
}),
|
||||
|
||||
updateUserNotes: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
userNotes: z.string(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const userId = ctx.user.userId;
|
||||
const { id, userNotes } = input;
|
||||
|
||||
// Extract readable ID from orderId (e.g., ORD001 -> 1)
|
||||
// const readableIdMatch = id.match(/^ORD(\d+)$/);
|
||||
// if (!readableIdMatch) {
|
||||
// console.error("Invalid order ID format:", id);
|
||||
// throw new ApiError("Invalid order ID format", 400);
|
||||
// }
|
||||
// const readableId = parseInt(readableIdMatch[1]);
|
||||
|
||||
// Check if order exists and belongs to user
|
||||
const order = await db.query.orders.findFirst({
|
||||
where: eq(orders.id, Number(id)),
|
||||
with: {
|
||||
orderStatus: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!order) {
|
||||
console.error("Order not found:", id);
|
||||
throw new ApiError("Order not found", 404);
|
||||
}
|
||||
|
||||
if (order.userId !== userId) {
|
||||
console.error("Order does not belong to user:", {
|
||||
orderId: id,
|
||||
orderUserId: order.userId,
|
||||
requestUserId: userId,
|
||||
});
|
||||
throw new ApiError("Order not found", 404);
|
||||
}
|
||||
|
||||
const status = order.orderStatus[0];
|
||||
if (!status) {
|
||||
console.error("Order status not found for order:", id);
|
||||
throw new ApiError("Order status not found", 400);
|
||||
}
|
||||
|
||||
// Only allow updating notes for orders that are not delivered or cancelled
|
||||
if (status.isDelivered) {
|
||||
console.error("Cannot update notes for delivered order:", id);
|
||||
throw new ApiError("Cannot update notes for delivered order", 400);
|
||||
}
|
||||
|
||||
if (status.isCancelled) {
|
||||
console.error("Cannot update notes for cancelled order:", id);
|
||||
throw new ApiError("Cannot update notes for cancelled order", 400);
|
||||
}
|
||||
|
||||
// Update user notes
|
||||
await db
|
||||
.update(orders)
|
||||
.set({
|
||||
userNotes: userNotes || null,
|
||||
})
|
||||
.where(eq(orders.id, order.id));
|
||||
|
||||
return { success: true, message: "Notes updated successfully" };
|
||||
}),
|
||||
|
||||
getRecentlyOrderedProducts: protectedProcedure
|
||||
.input(
|
||||
z
|
||||
.object({
|
||||
limit: z.number().min(1).max(50).default(20),
|
||||
})
|
||||
.optional()
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { limit = 20 } = input || {};
|
||||
const userId = ctx.user.userId;
|
||||
|
||||
// Get user's recent delivered orders (last 30 days)
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||
|
||||
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, thirtyDaysAgo)
|
||||
)
|
||||
)
|
||||
.orderBy(desc(orders.createdAt))
|
||||
.limit(10); // Get last 10 orders
|
||||
|
||||
if (recentOrders.length === 0) {
|
||||
return { success: true, products: [] };
|
||||
}
|
||||
|
||||
const orderIds = recentOrders.map((order) => order.id);
|
||||
|
||||
// Get unique product IDs from recent orders
|
||||
const orderItemsResult = await db
|
||||
.select({ productId: orderItems.productId })
|
||||
.from(orderItems)
|
||||
.where(inArray(orderItems.orderId, orderIds));
|
||||
|
||||
const productIds = [
|
||||
...new Set(orderItemsResult.map((item) => item.productId)),
|
||||
];
|
||||
|
||||
if (productIds.length === 0) {
|
||||
return { success: true, products: [] };
|
||||
}
|
||||
|
||||
// Get product details
|
||||
const productsWithUnits = 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);
|
||||
|
||||
// Generate signed URLs for product images
|
||||
const formattedProducts = await Promise.all(
|
||||
productsWithUnits.map(async (product) => {
|
||||
const nextDeliveryDate = await getNextDeliveryDate(product.id);
|
||||
return {
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
shortDescription: product.shortDescription,
|
||||
price: product.price,
|
||||
unit: product.unitShortNotation,
|
||||
incrementStep: product.incrementStep,
|
||||
isOutOfStock: product.isOutOfStock,
|
||||
nextDeliveryDate: nextDeliveryDate
|
||||
? nextDeliveryDate.toISOString()
|
||||
: null,
|
||||
images: scaffoldAssetUrl(
|
||||
(product.images as string[]) || []
|
||||
),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
products: formattedProducts,
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
|
@ -1,159 +0,0 @@
|
|||
|
||||
import { router, protectedProcedure } from '../trpc-index';
|
||||
import { z } from 'zod';
|
||||
import { db } from '../../db/db_index';
|
||||
import { orders, payments, orderStatus } from '../../db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { ApiError } from '../../lib/api-error';
|
||||
import crypto from 'crypto';
|
||||
import { razorpayId, razorpaySecret } from "../../lib/env-exporter";
|
||||
import { DiskPersistedSet } from "src/lib/disk-persisted-set";
|
||||
import { RazorpayPaymentService } from "../../lib/payments-utils";
|
||||
|
||||
|
||||
|
||||
|
||||
export const paymentRouter = router({
|
||||
createRazorpayOrder: protectedProcedure //either create a new payment order or return the existing one
|
||||
.input(z.object({
|
||||
orderId: z.string(),
|
||||
}))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const userId = ctx.user.userId;
|
||||
const { orderId } = input;
|
||||
|
||||
// Validate order exists and belongs to user
|
||||
const order = await db.query.orders.findFirst({
|
||||
where: eq(orders.id, parseInt(orderId)),
|
||||
});
|
||||
|
||||
if (!order) {
|
||||
throw new ApiError("Order not found", 404);
|
||||
}
|
||||
|
||||
if (order.userId !== userId) {
|
||||
throw new ApiError("Order does not belong to user", 403);
|
||||
}
|
||||
|
||||
// Check for existing pending payment
|
||||
const existingPayment = await db.query.payments.findFirst({
|
||||
where: eq(payments.orderId, parseInt(orderId)),
|
||||
});
|
||||
|
||||
if (existingPayment && existingPayment.status === 'pending') {
|
||||
return {
|
||||
razorpayOrderId: existingPayment.merchantOrderId,
|
||||
key: razorpayId,
|
||||
};
|
||||
}
|
||||
|
||||
// Create Razorpay order and insert payment record
|
||||
const razorpayOrder = await RazorpayPaymentService.createOrder(parseInt(orderId), order.totalAmount);
|
||||
await RazorpayPaymentService.insertPaymentRecord(parseInt(orderId), razorpayOrder);
|
||||
|
||||
return {
|
||||
razorpayOrderId: razorpayOrder.id,
|
||||
key: razorpayId,
|
||||
};
|
||||
}),
|
||||
|
||||
|
||||
|
||||
verifyPayment: protectedProcedure
|
||||
.input(z.object({
|
||||
razorpay_payment_id: z.string(),
|
||||
razorpay_order_id: z.string(),
|
||||
razorpay_signature: z.string(),
|
||||
}))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { razorpay_payment_id, razorpay_order_id, razorpay_signature } = input;
|
||||
|
||||
// Verify signature
|
||||
const expectedSignature = crypto
|
||||
.createHmac('sha256', razorpaySecret)
|
||||
.update(razorpay_order_id + '|' + razorpay_payment_id)
|
||||
.digest('hex');
|
||||
|
||||
if (expectedSignature !== razorpay_signature) {
|
||||
throw new ApiError("Invalid payment signature", 400);
|
||||
}
|
||||
|
||||
// Get current payment record
|
||||
const currentPayment = await db.query.payments.findFirst({
|
||||
where: eq(payments.merchantOrderId, razorpay_order_id),
|
||||
});
|
||||
|
||||
if (!currentPayment) {
|
||||
throw new ApiError("Payment record not found", 404);
|
||||
}
|
||||
|
||||
// Update payment status and payload
|
||||
const updatedPayload = {
|
||||
...((currentPayment.payload as any) || {}),
|
||||
payment_id: razorpay_payment_id,
|
||||
signature: razorpay_signature,
|
||||
};
|
||||
|
||||
const [updatedPayment] = await db
|
||||
.update(payments)
|
||||
.set({
|
||||
status: 'success',
|
||||
payload: updatedPayload,
|
||||
})
|
||||
.where(eq(payments.merchantOrderId, razorpay_order_id))
|
||||
.returning();
|
||||
|
||||
// Update order status to mark payment as processed
|
||||
await db
|
||||
.update(orderStatus)
|
||||
.set({
|
||||
paymentStatus: 'success',
|
||||
})
|
||||
.where(eq(orderStatus.orderId, updatedPayment.orderId));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Payment verified successfully",
|
||||
};
|
||||
}),
|
||||
|
||||
markPaymentFailed: protectedProcedure
|
||||
.input(z.object({
|
||||
merchantOrderId: z.string(),
|
||||
}))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const userId = ctx.user.userId;
|
||||
const { merchantOrderId } = input;
|
||||
|
||||
// Find payment by merchantOrderId
|
||||
const payment = await db.query.payments.findFirst({
|
||||
where: eq(payments.merchantOrderId, merchantOrderId),
|
||||
});
|
||||
|
||||
if (!payment) {
|
||||
throw new ApiError("Payment not found", 404);
|
||||
}
|
||||
|
||||
// Check if payment belongs to user's order
|
||||
const order = await db.query.orders.findFirst({
|
||||
where: eq(orders.id, payment.orderId),
|
||||
});
|
||||
|
||||
if (!order || order.userId !== userId) {
|
||||
throw new ApiError("Payment does not belong to user", 403);
|
||||
}
|
||||
|
||||
// Update payment status to failed
|
||||
await db
|
||||
.update(payments)
|
||||
.set({ status: 'failed' })
|
||||
.where(eq(payments.id, payment.id));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Payment marked as failed",
|
||||
};
|
||||
}),
|
||||
|
||||
});
|
||||
|
||||
|
|
@ -1,265 +0,0 @@
|
|||
import { router, publicProcedure, protectedProcedure } from '../trpc-index';
|
||||
import { z } from 'zod';
|
||||
import { db } from '../../db/db_index';
|
||||
import { productInfo, units, productSlots, deliverySlotInfo, specialDeals, storeInfo, productTagInfo, productTags, productReviews, users } from '../../db/schema';
|
||||
import { claimUploadUrl, extractKeyFromPresignedUrl, scaffoldAssetUrl } from '../../lib/s3-client';
|
||||
import { ApiError } from '../../lib/api-error';
|
||||
import { eq, and, gt, sql, inArray, desc } from 'drizzle-orm';
|
||||
import { getProductById as getProductByIdFromCache, getAllProducts as getAllProductsFromCache } from '../../stores/product-store';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
// Uniform Product Type
|
||||
interface Product {
|
||||
id: number;
|
||||
name: string;
|
||||
shortDescription: string | null;
|
||||
longDescription: string | null;
|
||||
price: string;
|
||||
marketPrice: string | null;
|
||||
unitNotation: string;
|
||||
images: string[];
|
||||
isOutOfStock: boolean;
|
||||
store: { id: number; name: string; description: string | null } | null;
|
||||
incrementStep: number;
|
||||
productQuantity: number;
|
||||
isFlashAvailable: boolean;
|
||||
flashPrice: string | null;
|
||||
deliverySlots: Array<{ id: number; deliveryTime: Date; freezeTime: Date }>;
|
||||
specialDeals: Array<{ quantity: string; price: string; validTill: Date }>;
|
||||
}
|
||||
|
||||
export const productRouter = router({
|
||||
getProductDetails: publicProcedure
|
||||
.input(z.object({
|
||||
id: z.string().regex(/^\d+$/, 'Invalid product ID'),
|
||||
}))
|
||||
.query(async ({ input }): Promise<Product> => {
|
||||
const { id } = input;
|
||||
const productId = parseInt(id);
|
||||
|
||||
if (isNaN(productId)) {
|
||||
throw new Error('Invalid product ID');
|
||||
}
|
||||
|
||||
console.log('from the api to get product details')
|
||||
|
||||
// First, try to get the product from Redis cache
|
||||
const cachedProduct = await getProductByIdFromCache(productId);
|
||||
|
||||
if (cachedProduct) {
|
||||
// Filter delivery slots to only include those with future freeze times and not at full capacity
|
||||
const currentTime = new Date();
|
||||
const filteredSlots = cachedProduct.deliverySlots.filter(slot =>
|
||||
dayjs(slot.freezeTime).isAfter(currentTime) && !slot.isCapacityFull
|
||||
);
|
||||
|
||||
return {
|
||||
...cachedProduct,
|
||||
deliverySlots: filteredSlots
|
||||
};
|
||||
}
|
||||
|
||||
// If not in cache, fetch from database (fallback)
|
||||
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) {
|
||||
throw new Error('Product not found');
|
||||
}
|
||||
|
||||
const product = productData[0];
|
||||
|
||||
// Fetch store info for this product
|
||||
const storeData = product.storeId ? await db.query.storeInfo.findFirst({
|
||||
where: eq(storeInfo.id, product.storeId),
|
||||
columns: { id: true, name: true, description: true },
|
||||
}) : null;
|
||||
|
||||
// Fetch delivery slots for this product
|
||||
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`NOW()`),
|
||||
gt(deliverySlotInfo.freezeTime, sql`NOW()`)
|
||||
)
|
||||
)
|
||||
.orderBy(deliverySlotInfo.deliveryTime);
|
||||
|
||||
// Fetch special deals for this product
|
||||
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`NOW()`)
|
||||
)
|
||||
)
|
||||
.orderBy(specialDeals.quantity);
|
||||
|
||||
// Generate signed URLs for images
|
||||
const signedImages = scaffoldAssetUrl((product.images as string[]) || []);
|
||||
|
||||
const response: Product = {
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
shortDescription: product.shortDescription,
|
||||
longDescription: product.longDescription,
|
||||
price: product.price.toString(),
|
||||
marketPrice: product.marketPrice?.toString() || null,
|
||||
unitNotation: product.unitShortNotation,
|
||||
images: signedImages,
|
||||
isOutOfStock: product.isOutOfStock,
|
||||
store: storeData ? {
|
||||
id: storeData.id,
|
||||
name: storeData.name,
|
||||
description: storeData.description,
|
||||
} : null,
|
||||
incrementStep: product.incrementStep,
|
||||
productQuantity: product.productQuantity,
|
||||
isFlashAvailable: product.isFlashAvailable,
|
||||
flashPrice: product.flashPrice?.toString() || null,
|
||||
deliverySlots: deliverySlotsData,
|
||||
specialDeals: specialDealsData.map(d => ({ quantity: d.quantity.toString(), price: d.price.toString(), validTill: d.validTill })),
|
||||
};
|
||||
|
||||
return response;
|
||||
}),
|
||||
|
||||
getProductReviews: publicProcedure
|
||||
.input(z.object({
|
||||
productId: z.number().int().positive(),
|
||||
limit: z.number().int().min(1).max(50).optional().default(10),
|
||||
offset: z.number().int().min(0).optional().default(0),
|
||||
}))
|
||||
.query(async ({ input }) => {
|
||||
const { productId, limit, offset } = input;
|
||||
|
||||
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);
|
||||
|
||||
// Generate signed URLs for images
|
||||
const reviewsWithSignedUrls = await Promise.all(
|
||||
reviews.map(async (review) => ({
|
||||
...review,
|
||||
signedImageUrls: scaffoldAssetUrl((review.imageUrls as string[]) || []),
|
||||
}))
|
||||
);
|
||||
|
||||
// Check if more reviews exist
|
||||
const totalCountResult = await db
|
||||
.select({ count: sql`count(*)` })
|
||||
.from(productReviews)
|
||||
.where(eq(productReviews.productId, productId));
|
||||
|
||||
const totalCount = Number(totalCountResult[0].count);
|
||||
const hasMore = offset + limit < totalCount;
|
||||
|
||||
return { reviews: reviewsWithSignedUrls, hasMore };
|
||||
}),
|
||||
|
||||
createReview: protectedProcedure
|
||||
.input(z.object({
|
||||
productId: z.number().int().positive(),
|
||||
reviewBody: z.string().min(1, 'Review body is required'),
|
||||
ratings: z.number().int().min(1).max(5),
|
||||
imageUrls: z.array(z.string()).optional().default([]),
|
||||
uploadUrls: z.array(z.string()).optional().default([]),
|
||||
}))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { productId, reviewBody, ratings, imageUrls, uploadUrls } = input;
|
||||
const userId = ctx.user.userId;
|
||||
|
||||
// Optional: Check if product exists
|
||||
const product = await db.query.productInfo.findFirst({
|
||||
where: eq(productInfo.id, productId),
|
||||
});
|
||||
if (!product) {
|
||||
throw new ApiError('Product not found', 404);
|
||||
}
|
||||
|
||||
// Insert review
|
||||
const [newReview] = await db.insert(productReviews).values({
|
||||
userId,
|
||||
productId,
|
||||
reviewBody,
|
||||
ratings,
|
||||
imageUrls: uploadUrls.map(item => extractKeyFromPresignedUrl(item)),
|
||||
}).returning();
|
||||
|
||||
// Claim upload URLs
|
||||
if (uploadUrls && uploadUrls.length > 0) {
|
||||
try {
|
||||
await Promise.all(uploadUrls.map(url => claimUploadUrl(url)));
|
||||
} catch (error) {
|
||||
console.error('Error claiming upload URLs:', error);
|
||||
// Don't fail the review creation
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, review: newReview };
|
||||
}),
|
||||
|
||||
getAllProductsSummary: publicProcedure
|
||||
.query(async (): Promise<Product[]> => {
|
||||
// Get all products from cache
|
||||
const allCachedProducts = await getAllProductsFromCache();
|
||||
|
||||
// Transform the cached products to match the expected summary format
|
||||
// (with empty deliverySlots and specialDeals arrays for summary view)
|
||||
const transformedProducts = allCachedProducts.map(product => ({
|
||||
...product,
|
||||
deliverySlots: [], // Empty for summary view
|
||||
specialDeals: [], // Empty for summary view
|
||||
}));
|
||||
|
||||
return transformedProducts;
|
||||
}),
|
||||
|
||||
});
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
import { router, publicProcedure } from "../trpc-index";
|
||||
import { z } from "zod";
|
||||
import { db } from "../../db/db_index";
|
||||
import {
|
||||
deliverySlotInfo,
|
||||
productSlots,
|
||||
productInfo,
|
||||
units,
|
||||
} from "../../db/schema";
|
||||
import { eq, and, gt, asc } from "drizzle-orm";
|
||||
import { getAllSlots as getAllSlotsFromCache, getSlotById as getSlotByIdFromCache } from "../../stores/slot-store";
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
// Helper method to get formatted slot data by ID
|
||||
async function getSlotData(slotId: number) {
|
||||
const slot = await getSlotByIdFromCache(slotId);
|
||||
|
||||
if (!slot) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentTime = new Date();
|
||||
if (dayjs(slot.freezeTime).isBefore(currentTime)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
deliveryTime: slot.deliveryTime,
|
||||
freezeTime: slot.freezeTime,
|
||||
slotId: slot.id,
|
||||
products: slot.products.filter((product) => !product.isOutOfStock),
|
||||
};
|
||||
}
|
||||
|
||||
export const slotsRouter = router({
|
||||
getSlots: publicProcedure.query(async () => {
|
||||
const slots = await db.query.deliverySlotInfo.findMany({
|
||||
where: eq(deliverySlotInfo.isActive, true),
|
||||
});
|
||||
return {
|
||||
slots,
|
||||
count: slots.length,
|
||||
};
|
||||
}),
|
||||
|
||||
getSlotsWithProducts: publicProcedure.query(async () => {
|
||||
const allSlots = await getAllSlotsFromCache();
|
||||
const currentTime = new Date();
|
||||
const validSlots = allSlots
|
||||
.filter((slot) => {
|
||||
return dayjs(slot.freezeTime).isAfter(currentTime) &&
|
||||
dayjs(slot.deliveryTime).isAfter(currentTime) &&
|
||||
!slot.isCapacityFull;
|
||||
})
|
||||
.sort((a, b) => dayjs(a.deliveryTime).valueOf() - dayjs(b.deliveryTime).valueOf());
|
||||
|
||||
return {
|
||||
slots: validSlots,
|
||||
count: validSlots.length,
|
||||
};
|
||||
}),
|
||||
|
||||
nextMajorDelivery: publicProcedure.query(async () => {
|
||||
const now = new Date();
|
||||
|
||||
// Find the next upcoming active delivery slot ID
|
||||
const nextSlot = await db.query.deliverySlotInfo.findFirst({
|
||||
where: and(
|
||||
eq(deliverySlotInfo.isActive, true),
|
||||
gt(deliverySlotInfo.deliveryTime, now),
|
||||
),
|
||||
orderBy: asc(deliverySlotInfo.deliveryTime),
|
||||
});
|
||||
|
||||
if (!nextSlot) {
|
||||
return null; // No upcoming delivery slots
|
||||
}
|
||||
|
||||
// Get formatted data using helper method
|
||||
return await getSlotData(nextSlot.id);
|
||||
}),
|
||||
|
||||
getSlotById: publicProcedure
|
||||
.input(z.object({ slotId: z.number() }))
|
||||
.query(async ({ input }) => {
|
||||
return await getSlotData(input.slotId);
|
||||
}),
|
||||
});
|
||||
|
|
@ -1,143 +0,0 @@
|
|||
import { router, publicProcedure } from '../trpc-index';
|
||||
import { z } from 'zod';
|
||||
import { db } from '../../db/db_index';
|
||||
import { storeInfo, productInfo, units } from '../../db/schema';
|
||||
import { eq, and, sql } from 'drizzle-orm';
|
||||
import { scaffoldAssetUrl } from '../../lib/s3-client';
|
||||
import { ApiError } from '../../lib/api-error';
|
||||
|
||||
export const storesRouter = router({
|
||||
getStores: publicProcedure
|
||||
.query(async () => {
|
||||
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);
|
||||
|
||||
// Generate signed URLs for store images and fetch sample products
|
||||
const storesWithDetails = await Promise.all(
|
||||
storesData.map(async (store) => {
|
||||
const signedImageUrl = store.imageUrl ? scaffoldAssetUrl(store.imageUrl) : null;
|
||||
|
||||
// Fetch up to 3 products for this 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);
|
||||
|
||||
// Generate signed URLs for product images
|
||||
const productsWithSignedUrls = await Promise.all(
|
||||
sampleProducts.map(async (product) => {
|
||||
const images = product.images as string[];
|
||||
return {
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
signedImageUrl: (images && images.length > 0) ? scaffoldAssetUrl(images[0]) : null,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
id: store.id,
|
||||
name: store.name,
|
||||
description: store.description,
|
||||
signedImageUrl,
|
||||
productCount: store.productCount,
|
||||
sampleProducts: productsWithSignedUrls,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
stores: storesWithDetails,
|
||||
};
|
||||
}),
|
||||
|
||||
getStoreWithProducts: publicProcedure
|
||||
.input(z.object({
|
||||
storeId: z.number(),
|
||||
}))
|
||||
.query(async ({ input }) => {
|
||||
const { storeId } = input;
|
||||
|
||||
// Fetch store info
|
||||
const storeData = await db.query.storeInfo.findFirst({
|
||||
where: eq(storeInfo.id, storeId),
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
description: true,
|
||||
imageUrl: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!storeData) {
|
||||
throw new ApiError('Store not found', 404);
|
||||
}
|
||||
|
||||
// Generate signed URL for store image
|
||||
const signedImageUrl = storeData.imageUrl ? scaffoldAssetUrl(storeData.imageUrl) : null;
|
||||
|
||||
// Fetch products for this store
|
||||
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,
|
||||
unitNotation: units.shortNotation,
|
||||
productQuantity: productInfo.productQuantity,
|
||||
})
|
||||
.from(productInfo)
|
||||
.innerJoin(units, eq(productInfo.unitId, units.id))
|
||||
.where(and(eq(productInfo.storeId, storeId), eq(productInfo.isSuspended, false)));
|
||||
|
||||
|
||||
// Generate signed URLs for product images
|
||||
const productsWithSignedUrls = await Promise.all(
|
||||
productsData.map(async (product) => ({
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
shortDescription: product.shortDescription,
|
||||
price: product.price,
|
||||
marketPrice: product.marketPrice,
|
||||
incrementStep: product.incrementStep,
|
||||
unit: product.unitShortNotation,
|
||||
unitNotation: product.unitNotation,
|
||||
images: scaffoldAssetUrl((product.images as string[]) || []),
|
||||
isOutOfStock: product.isOutOfStock,
|
||||
productQuantity: product.productQuantity
|
||||
}))
|
||||
);
|
||||
|
||||
return {
|
||||
store: {
|
||||
id: storeData.id,
|
||||
name: storeData.name,
|
||||
description: storeData.description,
|
||||
signedImageUrl,
|
||||
},
|
||||
products: productsWithSignedUrls,
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import { router, publicProcedure } from '../trpc-index';
|
||||
import { z } from 'zod';
|
||||
import { getTagsByStoreId } from '../../stores/product-tag-store';
|
||||
import { ApiError } from '../../lib/api-error';
|
||||
|
||||
export const tagsRouter = router({
|
||||
getTagsByStore: publicProcedure
|
||||
.input(z.object({
|
||||
storeId: z.number(),
|
||||
}))
|
||||
.query(async ({ input }) => {
|
||||
const { storeId } = input;
|
||||
|
||||
// Get tags from cache that are related to this store
|
||||
const tags = await getTagsByStoreId(storeId);
|
||||
|
||||
|
||||
return {
|
||||
tags: tags.map(tag => ({
|
||||
id: tag.id,
|
||||
tagName: tag.tagName,
|
||||
tagDescription: tag.tagDescription,
|
||||
imageUrl: tag.imageUrl,
|
||||
productIds: tag.productIds,
|
||||
})),
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
import { router } from '../trpc-index';
|
||||
import { addressRouter } from './address';
|
||||
import { authRouter } from './auth';
|
||||
import { bannerRouter } from './banners';
|
||||
import { cartRouter } from './cart';
|
||||
import { complaintRouter } from './complaint';
|
||||
import { orderRouter } from './order';
|
||||
import { productRouter } from './product';
|
||||
import { slotsRouter } from './slots';
|
||||
import { userRouter as userDataRouter } from './user';
|
||||
import { userCouponRouter } from './coupon';
|
||||
import { paymentRouter } from './payments';
|
||||
import { storesRouter } from './stores';
|
||||
import { fileUploadRouter } from './file-upload';
|
||||
import { tagsRouter } from './tags';
|
||||
|
||||
export const userRouter = router({
|
||||
address: addressRouter,
|
||||
auth: authRouter,
|
||||
banner: bannerRouter,
|
||||
cart: cartRouter,
|
||||
complaint: complaintRouter,
|
||||
order: orderRouter,
|
||||
product: productRouter,
|
||||
slots: slotsRouter,
|
||||
user: userDataRouter,
|
||||
coupon: userCouponRouter,
|
||||
payment: paymentRouter,
|
||||
stores: storesRouter,
|
||||
fileUpload: fileUploadRouter,
|
||||
tags: tagsRouter,
|
||||
});
|
||||
|
||||
export type UserRouter = typeof userRouter;
|
||||
|
|
@ -1,170 +0,0 @@
|
|||
import { router, protectedProcedure, publicProcedure } from '../trpc-index';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
import { z } from 'zod';
|
||||
import { db } from '../../db/db_index';
|
||||
import { users, userDetails, userCreds, notifCreds, unloggedUserTokens } from '../../db/schema';
|
||||
import { ApiError } from '../../lib/api-error';
|
||||
import { jwtSecret } from 'src/lib/env-exporter';
|
||||
import { generateSignedUrlFromS3Url } from '../../lib/s3-client';
|
||||
|
||||
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 userRouter = router({
|
||||
getSelfData: protectedProcedure
|
||||
.query(async ({ ctx }) => {
|
||||
const userId = ctx.user.userId;
|
||||
|
||||
if (!userId) {
|
||||
throw new ApiError('User not authenticated', 401);
|
||||
}
|
||||
|
||||
const [user] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.id, userId))
|
||||
.limit(1);
|
||||
|
||||
if (!user) {
|
||||
throw new ApiError('User not found', 404);
|
||||
}
|
||||
|
||||
// Get user details for profile image
|
||||
const [userDetail] = await db
|
||||
.select()
|
||||
.from(userDetails)
|
||||
.where(eq(userDetails.userId, userId))
|
||||
.limit(1);
|
||||
|
||||
// Generate signed URL for profile image if it exists
|
||||
const profileImageSignedUrl = userDetail?.profileImage
|
||||
? await generateSignedUrlFromS3Url(userDetail.profileImage)
|
||||
: null;
|
||||
|
||||
const response: Omit<AuthResponse, 'token'> = {
|
||||
user: {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
mobile: user.mobile,
|
||||
profileImage: profileImageSignedUrl,
|
||||
bio: userDetail?.bio || null,
|
||||
dateOfBirth: userDetail?.dateOfBirth || null,
|
||||
gender: userDetail?.gender || null,
|
||||
occupation: userDetail?.occupation || null,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: response,
|
||||
};
|
||||
}),
|
||||
|
||||
checkProfileComplete: protectedProcedure
|
||||
.query(async ({ ctx }) => {
|
||||
const userId = ctx.user.userId;
|
||||
|
||||
if (!userId) {
|
||||
throw new ApiError('User not authenticated', 401);
|
||||
}
|
||||
|
||||
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) {
|
||||
throw new ApiError('User not found', 404);
|
||||
}
|
||||
|
||||
const { users: user, user_creds: creds } = result[0];
|
||||
|
||||
return {
|
||||
isComplete: !!(user.name && user.email && creds),
|
||||
};
|
||||
}),
|
||||
|
||||
savePushToken: publicProcedure
|
||||
.input(z.object({ token: z.string() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { token } = input;
|
||||
const userId = ctx.user?.userId;
|
||||
|
||||
if (userId) {
|
||||
// AUTHENTICATED USER
|
||||
// Check if token exists in notif_creds for this user
|
||||
const existing = await db.query.notifCreds.findFirst({
|
||||
where: and(
|
||||
eq(notifCreds.userId, userId),
|
||||
eq(notifCreds.token, token)
|
||||
),
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
// Update lastVerified timestamp
|
||||
await db
|
||||
.update(notifCreds)
|
||||
.set({ lastVerified: new Date() })
|
||||
.where(eq(notifCreds.id, existing.id));
|
||||
} else {
|
||||
// Insert new token into notif_creds
|
||||
await db.insert(notifCreds).values({
|
||||
userId,
|
||||
token,
|
||||
lastVerified: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
// Remove from unlogged_user_tokens if it exists
|
||||
await db
|
||||
.delete(unloggedUserTokens)
|
||||
.where(eq(unloggedUserTokens.token, token));
|
||||
|
||||
} else {
|
||||
// UNAUTHENTICATED USER
|
||||
// Save/update in unlogged_user_tokens
|
||||
const existing = await db.query.unloggedUserTokens.findFirst({
|
||||
where: eq(unloggedUserTokens.token, token),
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
await db
|
||||
.update(unloggedUserTokens)
|
||||
.set({ lastVerified: new Date() })
|
||||
.where(eq(unloggedUserTokens.id, existing.id));
|
||||
} else {
|
||||
await db.insert(unloggedUserTokens).values({
|
||||
token,
|
||||
lastVerified: new Date(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}),
|
||||
});
|
||||
|
|
@ -2,13 +2,13 @@ import { Request, Response, NextFunction } from 'express';
|
|||
import bcrypt from 'bcryptjs';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { db } from '../db/db_index';
|
||||
import { users, userCreds, userDetails } from '../db/schema';
|
||||
import { ApiError } from '../lib/api-error';
|
||||
import catchAsync from '../lib/catch-async';
|
||||
import { jwtSecret } from 'src/lib/env-exporter';
|
||||
import uploadHandler from '../lib/upload-handler';
|
||||
import { imageUploadS3, generateSignedUrlFromS3Url } from '../lib/s3-client';
|
||||
import { db } from '@/src/db/db_index'
|
||||
import { users, userCreds, userDetails } from '@/src/db/schema'
|
||||
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;
|
||||
|
|
@ -318,4 +318,4 @@ export const updateProfile = catchAsync(async (req: Request, res: Response, next
|
|||
success: true,
|
||||
data: response,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue