merge test #1

Merged
shafi merged 8 commits from test into main 2026-03-09 15:50:27 +00:00
106 changed files with 309 additions and 3634 deletions
Showing only changes of commit 8fc603db0a - Show all commits

View file

@ -1,8 +1,6 @@
# Agent Instructions for Meat Farmer Monorepo # Agent Instructions for Meat Farmer Monorepo
## Important instructions ## 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 ## 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` - Apps: `user-ui`, `admin-ui`, `inspiration-ui`, `inspiration-backend`
- Database: Drizzle ORM with PostgreSQL - 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

View file

@ -5,19 +5,19 @@ import cors from "cors";
import multer from "multer"; import multer from "multer";
import path from "path"; import path from "path";
import fs from "fs"; import fs from "fs";
import { db } from './src/db/db_index'; import { db } from '@/src/db/db_index';
import { staffUsers, userDetails } from './src/db/schema'; import { staffUsers, userDetails } from '@/src/db/schema';
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import mainRouter from './src/main-router'; import mainRouter from '@/src/main-router';
import initFunc from './src/lib/init'; import initFunc from '@/src/lib/init';
import { createExpressMiddleware } from '@trpc/server/adapters/express'; 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 { TRPCError } from '@trpc/server';
import jwt from 'jsonwebtoken' import jwt from 'jsonwebtoken'
import signedUrlCache from 'src/lib/signed-url-cache'; import signedUrlCache from '@/src/lib/signed-url-cache';
import { seed } from 'src/db/seed'; import { seed } from '@/src/db/seed';
import './src/jobs/jobs-index'; import '@/src/jobs/jobs-index';
import { startAutomatedJobs } from './src/lib/automatedJobs'; import { startAutomatedJobs } from '@/src/lib/automatedJobs';
seed() seed()
initFunc() initFunc()

View file

@ -1,7 +1,7 @@
import { Router } from "express"; import { Router } from "express";
import { authenticateStaff } from "../middleware/staff-auth"; import { authenticateStaff } from "@/src/middleware/staff-auth";
import productRouter from "./product.router"; import productRouter from "@/src/apis/admin-apis/apis/product.router"
import tagRouter from "./tag.router"; import tagRouter from "@/src/apis/admin-apis/apis/tag.router"
const router = Router(); const router = Router();

View file

@ -1,11 +1,11 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import { db } from "../db/db_index"; import { db } from "@/src/db/db_index";
import { productTagInfo } from "../db/schema"; import { productTagInfo } from "@/src/db/schema";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { ApiError } from "../lib/api-error"; import { ApiError } from "@/src/lib/api-error";
import { imageUploadS3, generateSignedUrlFromS3Url } from "../lib/s3-client"; import { imageUploadS3, generateSignedUrlFromS3Url } from "@/src/lib/s3-client";
import { deleteS3Image } from "../lib/delete-image"; import { deleteS3Image } from "@/src/lib/delete-image";
import { initializeAllStores } from '../stores/store-initializer'; import { initializeAllStores } from '@/src/stores/store-initializer';
/** /**
* Create a new product tag * Create a new product tag

View file

@ -1,12 +1,12 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import { db } from "../db/db_index"; import { db } from "@/src/db/db_index";
import { productInfo, units, specialDeals, productTags } from "../db/schema"; import { productInfo, units, specialDeals, productTags } from "@/src/db/schema";
import { eq, inArray } from "drizzle-orm"; import { eq, inArray } from "drizzle-orm";
import { ApiError } from "../lib/api-error"; import { ApiError } from "@/src/lib/api-error";
import { imageUploadS3, getOriginalUrlFromSignedUrl } from "../lib/s3-client"; import { imageUploadS3, getOriginalUrlFromSignedUrl } from "@/src/lib/s3-client";
import { deleteS3Image } from "../lib/delete-image"; import { deleteS3Image } from "@/src/lib/delete-image";
import type { SpecialDeal } from "../db/types"; import type { SpecialDeal } from "@/src/db/types";
import { initializeAllStores } from '../stores/store-initializer'; import { initializeAllStores } from '@/src/stores/store-initializer';
type CreateDeal = { type CreateDeal = {
quantity: number; quantity: number;

View file

@ -1,6 +1,6 @@
import { Router } from "express"; import { Router } from "express";
import { createProduct, updateProduct } from "./product.controller"; import { createProduct, updateProduct } from "@/src/apis/admin-apis/apis/product.controller"
import uploadHandler from '../lib/upload-handler'; import uploadHandler from '@/src/lib/upload-handler';
const router = Router(); const router = Router();

View file

@ -1,6 +1,6 @@
import { Router } from "express"; import { Router } from "express";
import { createTag, getAllTags, getTagById, updateTag, deleteTag } from "./product-tags.controller"; import { createTag, getAllTags, getTagById, updateTag, deleteTag } from "@/src/apis/admin-apis/apis/product-tags.controller"
import uploadHandler from '../lib/upload-handler'; import uploadHandler from '@/src/lib/upload-handler';
const router = Router(); const router = Router();

View file

@ -1,8 +1,8 @@
import { eq, gt, and, sql, inArray } from "drizzle-orm"; import { eq, gt, and, sql, inArray } from "drizzle-orm";
import { Request, Response } from "express"; import { Request, Response } from "express";
import { db } from "../db/db_index"; import { db } from "@/src/db/db_index"
import { productInfo, units, productSlots, deliverySlotInfo, productTags } from "../db/schema"; import { productInfo, units, productSlots, deliverySlotInfo, productTags } from "@/src/db/schema"
import { scaffoldAssetUrl } from "../lib/s3-client"; import { scaffoldAssetUrl } from "@/src/lib/s3-client"
/** /**
* Get next delivery date for a product * Get next delivery date for a product

View file

@ -1,5 +1,5 @@
import { Router } from "express"; import { Router } from "express";
import { getAllProductsSummary } from "./common-product.controller"; import { getAllProductsSummary } from "@/src/apis/common-apis/apis/common-product.controller"
const router = Router(); const router = Router();

View file

@ -1,5 +1,5 @@
import { Router } from "express"; import { Router } from "express";
import commonProductsRouter from "./common-product.router"; import commonProductsRouter from "@/src/apis/common-apis/apis/common-product.router"
const router = Router(); const router = Router();

View file

@ -1,7 +1,7 @@
import { drizzle } from "drizzle-orm/node-postgres" import { drizzle } from "drizzle-orm/node-postgres"
import { migrate } from "drizzle-orm/node-postgres/migrator" import { migrate } from "drizzle-orm/node-postgres/migrator"
import path from "path" 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({ connection: process.env.DATABASE_URL!, casing: "snake_case", schema: schema })
// const db = drizzle('postgresql://postgres:postgres@localhost:2345/pooler'); // const db = drizzle('postgresql://postgres:postgres@localhost:2345/pooler');

View file

@ -2,13 +2,13 @@
* This was a one time script to change the composition of the signed urls * 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 { import {
userDetails, userDetails,
productInfo, productInfo,
productTagInfo, productTagInfo,
complaints complaints
} from './schema'; } from '@/src/db/schema';
import { eq, not, isNull } from 'drizzle-orm'; import { eq, not, isNull } from 'drizzle-orm';
const S3_DOMAIN = 'https://s3.sgp.io.cloud.ovh.net'; const S3_DOMAIN = 'https://s3.sgp.io.cloud.ovh.net';
@ -122,4 +122,4 @@ runMigration()
.catch((error) => { .catch((error) => {
console.error('Process failed:', error); console.error('Process failed:', error);
process.exit(1); process.exit(1);
}); });

View file

@ -1,8 +1,8 @@
import { db } from "./db_index"; import { db } from "@/src/db/db_index"
import { units, productInfo, deliverySlotInfo, productSlots, keyValStore, staffRoles, staffPermissions, staffRolePermissions } from "./schema"; import { units, productInfo, deliverySlotInfo, productSlots, keyValStore, staffRoles, staffPermissions, staffRolePermissions } from "@/src/db/schema"
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { minOrderValue, deliveryCharge } from '../lib/env-exporter'; import { minOrderValue, deliveryCharge } from '@/src/lib/env-exporter'
import { CONST_KEYS } from '../lib/const-keys'; import { CONST_KEYS } from '@/src/lib/const-keys'
export async function seed() { export async function seed() {
console.log("Seeding database..."); console.log("Seeding database...");

View file

@ -14,7 +14,7 @@ import type {
productCategories, productCategories,
cartItems, cartItems,
coupons, coupons,
} from "./schema"; } from "@/src/db/schema";
export type User = InferSelectModel<typeof users>; export type User = InferSelectModel<typeof users>;
export type Address = InferSelectModel<typeof addresses>; export type Address = InferSelectModel<typeof addresses>;
@ -44,4 +44,4 @@ export type OrderWithItems = Order & {
export type CartItemWithProduct = CartItem & { export type CartItemWithProduct = CartItem & {
product: ProductInfo; product: ProductInfo;
}; };

View file

@ -1,5 +1,5 @@
import * as cron from 'node-cron'; 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 runCombinedJob = async () => {
const start = Date.now(); const start = Date.now();

View file

@ -1,8 +1,8 @@
import * as cron from 'node-cron'; import * as cron from 'node-cron';
import { db } from '../db/db_index'; import { db } from '@/src/db/db_index'
import { payments, orders, deliverySlotInfo, refunds } from '../db/schema'; import { payments, orders, deliverySlotInfo, refunds } from '@/src/db/schema'
import { eq, and, gt, isNotNull } from 'drizzle-orm'; import { eq, and, gt, isNotNull } from 'drizzle-orm';
import { RazorpayPaymentService } from '../lib/payments-utils'; import { RazorpayPaymentService } from '@/src/lib/payments-utils'
interface PendingPaymentRecord { interface PendingPaymentRecord {
payment: typeof payments.$inferSelect; payment: typeof payments.$inferSelect;

View file

@ -1,9 +1,9 @@
import * as cron from 'node-cron'; import * as cron from 'node-cron';
import { db } from '../db/db_index'; import { db } from '@/src/db/db_index'
import { productInfo, keyValStore } from '../db/schema'; import { productInfo, keyValStore } from '@/src/db/schema'
import { inArray, eq } from 'drizzle-orm'; import { inArray, eq } from 'drizzle-orm';
import { CONST_KEYS } from '../lib/const-keys'; import { CONST_KEYS } from '@/src/lib/const-keys'
import { computeConstants } from '../lib/const-store'; import { computeConstants } from '@/src/lib/const-store'
const MUTTON_ITEMS = [ const MUTTON_ITEMS = [

View file

@ -1,5 +1,5 @@
import axiosParent from "axios"; import axiosParent from "axios";
import { phonePeBaseUrl } from "./env-exporter"; import { phonePeBaseUrl } from "@/src/lib/env-exporter"
export const phonepeAxios = axiosParent.create({ export const phonepeAxios = axiosParent.create({
baseURL: phonePeBaseUrl, baseURL: phonePeBaseUrl,

View file

@ -1,7 +1,7 @@
import { db } from '../db/db_index'; import { db } from '@/src/db/db_index'
import { keyValStore } from '../db/schema'; import { keyValStore } from '@/src/db/schema'
import redisClient from './redis-client'; import redisClient from '@/src/lib/redis-client'
import { CONST_KEYS, CONST_KEYS_ARRAY, type ConstKey } from './const-keys'; import { CONST_KEYS, CONST_KEYS_ARRAY, type ConstKey } from '@/src/lib/const-keys'
const CONST_REDIS_PREFIX = 'const:'; const CONST_REDIS_PREFIX = 'const:';

View file

@ -1,7 +1,7 @@
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { db } from "../db/db_index"; import { db } from "@/src/db/db_index"
import { deleteImageUtil, getOriginalUrlFromSignedUrl } from "./s3-client"; import { deleteImageUtil, getOriginalUrlFromSignedUrl } from "@/src/lib/s3-client"
import { s3Url } from "./env-exporter"; import { s3Url } from "@/src/lib/env-exporter"
function extractS3Key(url: string): string | null { function extractS3Key(url: string): string | null {
try { try {

View file

@ -1,5 +1,5 @@
import { db } from '../db/db_index'; import { db } from '@/src/db/db_index'
import { orders, orderItems, orderStatus, payments, refunds, couponUsage, complaints } from '../db/schema'; import { orders, orderItems, orderStatus, payments, refunds, couponUsage, complaints } from '@/src/db/schema'
import { eq, inArray } from 'drizzle-orm'; import { eq, inArray } from 'drizzle-orm';
/** /**

View file

@ -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> { export async function enqueue(queueName: string, eventData: any): Promise<boolean> {
try { try {

View file

@ -1,6 +1,6 @@
import { Expo } from "expo-server-sdk"; import { Expo } from "expo-server-sdk";
import { title } from "process"; import { title } from "process";
import { expoAccessToken } from "./env-exporter"; import { expoAccessToken } from "@/src/lib/env-exporter"
const expo = new Expo({ const expo = new Expo({
accessToken: expoAccessToken, accessToken: expoAccessToken,

View file

@ -1,8 +1,8 @@
import './notif-job'; import '@/src/lib/notif-job'
import { initializeAllStores } from '../stores/store-initializer'; import { initializeAllStores } from '@/src/stores/store-initializer'
import { initializeUserNegativityStore } from '../stores/user-negativity-store'; import { initializeUserNegativityStore } from '@/src/stores/user-negativity-store'
import { startOrderHandler, startCancellationHandler, publishOrder } from './post-order-handler'; import { startOrderHandler, startCancellationHandler, publishOrder } from '@/src/lib/post-order-handler'
import { deleteOrders } from './delete-orders'; import { deleteOrders } from '@/src/lib/delete-orders'
/** /**
* Initialize all application services * Initialize all application services

View file

@ -1,8 +1,8 @@
import { Queue, Worker } from 'bullmq'; import { Queue, Worker } from 'bullmq';
import { Expo } from 'expo-server-sdk'; import { Expo } from 'expo-server-sdk';
import { redisUrl } from './env-exporter'; import { redisUrl } from '@/src/lib/env-exporter'
import { db } from '../db/db_index'; import { db } from '@/src/db/db_index'
import { generateSignedUrlFromS3Url } from './s3-client'; import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client'
import { import {
NOTIFS_QUEUE, NOTIFS_QUEUE,
ORDER_PLACED_MESSAGE, ORDER_PLACED_MESSAGE,
@ -12,7 +12,7 @@ import {
ORDER_DELIVERED_MESSAGE, ORDER_DELIVERED_MESSAGE,
ORDER_CANCELLED_MESSAGE, ORDER_CANCELLED_MESSAGE,
REFUND_INITIATED_MESSAGE REFUND_INITIATED_MESSAGE
} from './const-strings'; } from '@/src/lib/const-strings';
export const notificationQueue = new Queue(NOTIFS_QUEUE, { export const notificationQueue = new Queue(NOTIFS_QUEUE, {
connection: { url: redisUrl }, connection: { url: redisUrl },

View file

@ -1,6 +1,6 @@
import { db } from "../db/db_index"; import { db } from "@/src/db/db_index"
import { sendPushNotificationsMany } from "./expo-service"; import { sendPushNotificationsMany } from "@/src/lib/expo-service"
// import { usersTable, notifCredsTable, notificationTable } from "../db/schema"; // import { usersTable, notifCredsTable, notificationTable } from "@/src/db/schema";
import { eq, inArray } from "drizzle-orm"; import { eq, inArray } from "drizzle-orm";
// Core notification dispatch methods (renamed for clarity) // Core notification dispatch methods (renamed for clarity)
@ -244,4 +244,4 @@ export const sendNotifToSingleUser = dispatchUserNotification;
/** /**
* @deprecated Use notifyNewOffer() or other purpose-specific methods instead * @deprecated Use notifyNewOffer() or other purpose-specific methods instead
*/ */
export const sendNotifToManyUsers = dispatchBulkNotification; export const sendNotifToManyUsers = dispatchBulkNotification;

View file

@ -1,5 +1,5 @@
import { ApiError } from './api-error'; import { ApiError } from '@/src/lib/api-error'
import { otpSenderAuthToken } from './env-exporter'; import { otpSenderAuthToken } from '@/src/lib/env-exporter'
const otpStore = new Map<string, string>(); const otpStore = new Map<string, string>();

View file

@ -1,7 +1,7 @@
import Razorpay from "razorpay"; import Razorpay from "razorpay";
import { razorpayId, razorpaySecret } from "./env-exporter"; import { razorpayId, razorpaySecret } from "@/src/lib/env-exporter"
import { db } from "../db/db_index"; import { db } from "@/src/db/db_index"
import { payments } from "../db/schema"; import { payments } from "@/src/db/schema"
type Tx = Parameters<Parameters<typeof db.transaction>[0]>[0]; type Tx = Parameters<Parameters<typeof db.transaction>[0]>[0];

View file

@ -1,7 +1,7 @@
import { db } from '../db/db_index'; import { db } from '@/src/db/db_index'
import { orders, orderStatus } from '../db/schema'; import { orders, orderStatus } from '@/src/db/schema'
import redisClient from './redis-client'; import redisClient from '@/src/lib/redis-client'
import { sendTelegramMessage } from './telegram-service'; import { sendTelegramMessage } from '@/src/lib/telegram-service'
import { inArray, eq } from 'drizzle-orm'; import { inArray, eq } from 'drizzle-orm';
const ORDER_CHANNEL = 'orders:placed'; const ORDER_CHANNEL = 'orders:placed';

View file

@ -1,5 +1,5 @@
import { createClient, RedisClientType } from 'redis'; import { createClient, RedisClientType } from 'redis';
import { redisUrl } from './env-exporter'; import { redisUrl } from '@/src/lib/env-exporter'
class RedisClient { class RedisClient {
private client: RedisClientType; private client: RedisClientType;

View file

@ -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 * Constants for role names to avoid hardcoding and typos

View file

@ -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 { DeleteObjectCommand, DeleteObjectsCommand, PutObjectCommand, S3Client, GetObjectCommand } from "@aws-sdk/client-s3"
import { getSignedUrl } from "@aws-sdk/s3-request-presigner" import { getSignedUrl } from "@aws-sdk/s3-request-presigner"
import signedUrlCache from "./signed-url-cache" import signedUrlCache from "@/src/lib/signed-url-cache"
import { s3AccessKeyId, s3Region, s3Url, s3SecretAccessKey, s3BucketName, assetsDomain } from "./env-exporter"; import { s3AccessKeyId, s3Region, s3Url, s3SecretAccessKey, s3BucketName, assetsDomain } from "@/src/lib/env-exporter"
import { db } from "../db/db_index"; // Adjust path if needed import { db } from "@/src/db/db_index"; // Adjust path if needed
import { uploadUrlStatus } from "../db/schema"; import { uploadUrlStatus } from "@/src/db/schema"
import { and, eq } from 'drizzle-orm'; import { and, eq } from 'drizzle-orm';
const s3Client = new S3Client({ const s3Client = new S3Client({

View file

@ -1,5 +1,5 @@
import axios from 'axios'; 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 BOT_TOKEN = telegramBotToken;
const CHAT_IDS = telegramChatIds; const CHAT_IDS = telegramChatIds;

View file

@ -1,11 +1,11 @@
import { Router, Request, Response, NextFunction } from "express"; import { Router, Request, Response, NextFunction } from "express";
import avRouter from "./admin-apis/av-router"; import avRouter from "@/src/apis/admin-apis/apis/av-router"
import { ApiError } from "./lib/api-error"; import { ApiError } from "@/src/lib/api-error"
import v1Router from "./v1-router"; import v1Router from "@/src/v1-router"
import testController from "./test-controller"; import testController from "@/src/test-controller"
import { authenticateUser } from "./middleware/auth.middleware"; import { authenticateUser } from "@/src/middleware/auth.middleware"
import { raiseComplaint } from "./uv-apis/user-rest.controller"; import { raiseComplaint } from "@/src/uv-apis/user-rest.controller"
import uploadHandler from "./lib/upload-handler"; import uploadHandler from "@/src/lib/upload-handler"
const router = Router(); const router = Router();

View file

@ -1,9 +1,9 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { db } from '../db/db_index'; import { db } from '@/src/db/db_index'
import { staffUsers, userDetails } from '../db/schema'; import { staffUsers, userDetails } from '@/src/db/schema'
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import { ApiError } from '../lib/api-error'; import { ApiError } from '@/src/lib/api-error'
interface AuthenticatedRequest extends Request { interface AuthenticatedRequest extends Request {
user?: { user?: {

View file

@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken'; 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 // Extend the Request interface to include user property
declare global { declare global {

View file

@ -1,9 +1,9 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { db } from '../db/db_index'; import { db } from '@/src/db/db_index'
import { staffUsers } from '../db/schema'; import { staffUsers } from '@/src/db/schema'
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import { ApiError } from '../lib/api-error'; import { ApiError } from '@/src/lib/api-error'
// Extend Request interface to include staffUser // Extend Request interface to include staffUser
declare global { declare global {

View file

@ -1,4 +1,4 @@
import { db } from '../../db/db_index' import { db } from '@/src/db/db_index'
import { import {
orders, orders,
orderItems, orderItems,
@ -13,7 +13,7 @@ import {
refunds, refunds,
units, units,
userDetails, userDetails,
} from '../../db/schema' } from '@/src/db/schema'
import { eq, and, inArray, desc, gte } from 'drizzle-orm' import { eq, and, inArray, desc, gte } from 'drizzle-orm'
// ============ User/Auth Queries ============ // ============ User/Auth Queries ============

View file

@ -1,5 +1,5 @@
import { db } from '../../db/db_index' import { db } from '@/src/db/db_index'
import { productInfo, units, productSlots, deliverySlotInfo, specialDeals, storeInfo, productReviews, users } from '../../db/schema' import { productInfo, units, productSlots, deliverySlotInfo, specialDeals, storeInfo, productReviews, users } from '@/src/db/schema'
import { eq, and, gt, sql, desc } from 'drizzle-orm' import { eq, and, gt, sql, desc } from 'drizzle-orm'
/** /**

View file

@ -1,9 +1,13 @@
// import redisClient from './redis-client'; // import redisClient from '@/src/stores/redis-client';
import redisClient from 'src/lib/redis-client'; import redisClient from '@/src/lib/redis-client';
import { db } from '../db/db_index'; import { db } from '@/src/db/db_index'
import { homeBanners } from '../db/schema'; import { homeBanners } from '@/src/db/schema'
import { isNotNull, asc } from 'drizzle-orm'; 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) // Banner Type (matches getBanners return)
interface Banner { interface Banner {

View file

@ -1,9 +1,9 @@
// import redisClient from './redis-client'; // import redisClient from '@/src/stores/redis-client';
import redisClient from 'src/lib/redis-client'; import redisClient from '@/src/lib/redis-client';
import { db } from '../db/db_index'; import { db } from '@/src/db/db_index'
import { productInfo, units, productSlots, deliverySlotInfo, specialDeals, storeInfo, productTags, productTagInfo } from '../db/schema'; import { productInfo, units, productSlots, deliverySlotInfo, specialDeals, storeInfo, productTags, productTagInfo } from '@/src/db/schema'
import { eq, and, gt, sql } from 'drizzle-orm'; 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) // Uniform Product Type (matches getProductDetails return)
interface Product { interface Product {

View file

@ -1,9 +1,9 @@
// import redisClient from './redis-client'; // import redisClient from '@/src/stores/redis-client';
import redisClient from 'src/lib/redis-client'; import redisClient from '@/src/lib/redis-client';
import { db } from '../db/db_index'; import { db } from '@/src/db/db_index'
import { productTagInfo, productTags } from '../db/schema'; import { productTagInfo, productTags } from '@/src/db/schema'
import { eq, inArray } from 'drizzle-orm'; 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) // Tag Type (matches getDashboardTags return)
interface Tag { interface Tag {
@ -168,4 +168,4 @@ export async function getTagsByStoreId(storeId: number): Promise<Tag[]> {
console.error(`Error getting tags for store ${storeId}:`, error); console.error(`Error getting tags for store ${storeId}:`, error);
return []; return [];
} }
} }

View file

@ -1,8 +1,8 @@
import redisClient from 'src/lib/redis-client'; import redisClient from '@/src/lib/redis-client';
import { db } from '../db/db_index'; import { db } from '@/src/db/db_index'
import { deliverySlotInfo, productSlots, productInfo, units } from '../db/schema'; import { deliverySlotInfo, productSlots, productInfo, units } from '@/src/db/schema'
import { eq, and, gt, asc } from 'drizzle-orm'; 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'; import dayjs from 'dayjs';
// Define the structure for slot with products // Define the structure for slot with products

View file

@ -1,9 +1,9 @@
import roleManager from '../lib/roles-manager'; import roleManager from '@/src/lib/roles-manager'
import { computeConstants } from '../lib/const-store'; import { computeConstants } from '@/src/lib/const-store'
import { initializeProducts } from './product-store'; import { initializeProducts } from '@/src/stores/product-store'
import { initializeProductTagStore } from './product-tag-store'; import { initializeProductTagStore } from '@/src/stores/product-tag-store'
import { initializeSlotStore } from './slot-store'; import { initializeSlotStore } from '@/src/stores/slot-store'
import { initializeBannerStore } from './banner-store'; import { initializeBannerStore } from '@/src/stores/banner-store'
/** /**
* Initialize all application stores * Initialize all application stores

View file

@ -1,6 +1,6 @@
import redisClient from 'src/lib/redis-client'; import redisClient from '@/src/lib/redis-client';
import { db } from '../db/db_index'; import { db } from '@/src/db/db_index'
import { userIncidents } from '../db/schema'; import { userIncidents } from '@/src/db/schema'
import { eq, sum } from 'drizzle-orm'; import { eq, sum } from 'drizzle-orm';
export async function initializeUserNegativityStore(): Promise<void> { export async function initializeUserNegativityStore(): Promise<void> {

View file

@ -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;

View file

@ -1,8 +1,8 @@
import { z } from 'zod'; import { z } from 'zod';
import { addressZones, addressAreas } from '../../db/schema'; import { addressZones, addressAreas } from '@/src/db/schema'
import { eq, desc } from 'drizzle-orm'; import { eq, desc } from 'drizzle-orm';
import { db } from '../../db/db_index'; import { db } from '@/src/db/db_index'
import { router,protectedProcedure } from '../trpc-index'; import { router,protectedProcedure } from '@/src/trpc/trpc-index'
const addressRouter = router({ const addressRouter = router({
getZones: protectedProcedure.query(async () => { getZones: protectedProcedure.query(async () => {

View file

@ -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;

View file

@ -1,11 +1,11 @@
import { z } from 'zod'; import { z } from 'zod';
import { db } from '../../db/db_index'; import { db } from '@/src/db/db_index'
import { homeBanners } from '../../db/schema'; import { homeBanners } from '@/src/db/schema'
import { eq, and, desc, sql } from 'drizzle-orm'; import { eq, and, desc, sql } from 'drizzle-orm';
import { protectedProcedure, router } from '../trpc-index'; import { protectedProcedure, router } from '@/src/trpc/trpc-index'
import { extractKeyFromPresignedUrl, generateSignedUrlFromS3Url } from '../../lib/s3-client'; import { extractKeyFromPresignedUrl, generateSignedUrlFromS3Url } from '@/src/lib/s3-client'
import { ApiError } from 'src/lib/api-error'; import { ApiError } from '@/src/lib/api-error';
import { initializeAllStores } from '../../stores/store-initializer'; import { initializeAllStores } from '@/src/stores/store-initializer'
export const bannerRouter = router({ export const bannerRouter = router({
// Get all banners // Get all banners
@ -171,4 +171,4 @@ export const bannerRouter = router({
return { success: true }; return { success: true };
}), }),
}); });

View file

@ -1,7 +1,7 @@
import { router, protectedProcedure } from '../trpc-index'; import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'; import { z } from 'zod';
import { db } from '../../db/db_index'; import { db } from '@/src/db/db_index'
import { orders, orderStatus, users, addresses, orderItems, productInfo, units, refunds } from '../../db/schema'; import { orders, orderStatus, users, addresses, orderItems, productInfo, units, refunds } from '@/src/db/schema'
import { eq, desc } from 'drizzle-orm'; import { eq, desc } from 'drizzle-orm';
const updateCancellationReviewSchema = z.object({ const updateCancellationReviewSchema = z.object({

View file

@ -1,9 +1,9 @@
import { router, protectedProcedure } from '../trpc-index'; import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'; import { z } from 'zod';
import { db } from '../../db/db_index'; import { db } from '@/src/db/db_index'
import { complaints, users } from '../../db/schema'; import { complaints, users } from '@/src/db/schema'
import { eq, desc, lt, and } from 'drizzle-orm'; 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({ export const complaintRouter = router({
getAll: protectedProcedure getAll: protectedProcedure

View file

@ -1,9 +1,9 @@
import { router, protectedProcedure } from '../trpc-index'; import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'; import { z } from 'zod';
import { db } from '../../db/db_index'; import { db } from '@/src/db/db_index'
import { keyValStore } from '../../db/schema'; import { keyValStore } from '@/src/db/schema'
import { computeConstants } from '../../lib/const-store'; import { computeConstants } from '@/src/lib/const-store'
import { CONST_KEYS } from '../../lib/const-keys'; import { CONST_KEYS } from '@/src/lib/const-keys'
export const constRouter = router({ export const constRouter = router({
getConstants: protectedProcedure getConstants: protectedProcedure

View file

@ -1,7 +1,7 @@
import { router, protectedProcedure } from '../trpc-index'; import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'; import { z } from 'zod';
import { db } from '../../db/db_index'; import { db } from '@/src/db/db_index'
import { coupons, users, staffUsers, orders, couponApplicableUsers, couponApplicableProducts, orderStatus, reservedCoupons } from '../../db/schema'; import { coupons, users, staffUsers, orders, couponApplicableUsers, couponApplicableProducts, orderStatus, reservedCoupons } from '@/src/db/schema'
import { eq, and, like, or, inArray, lt } from 'drizzle-orm'; import { eq, and, like, or, inArray, lt } from 'drizzle-orm';
import dayjs from 'dayjs'; import dayjs from 'dayjs';

View file

@ -1,6 +1,6 @@
import { router, protectedProcedure } from "../trpc-index"; import { router, protectedProcedure } from "@/src/trpc/trpc-index"
import { z } from "zod"; import { z } from "zod";
import { db } from "../../db/db_index"; import { db } from "@/src/db/db_index"
import { import {
orders, orders,
orderItems, orderItems,
@ -10,17 +10,17 @@ import {
refunds, refunds,
coupons, coupons,
couponUsage, couponUsage,
} from "../../db/schema"; } from "@/src/db/schema";
import { eq, and, gte, lt, desc, SQL, inArray } from "drizzle-orm"; import { eq, and, gte, lt, desc, SQL, inArray } from "drizzle-orm";
import dayjs from "dayjs"; import dayjs from "dayjs";
import utc from "dayjs/plugin/utc"; import utc from "dayjs/plugin/utc";
import { ApiError } from "../../lib/api-error"; import { ApiError } from "@/src/lib/api-error"
import { import {
sendOrderPackagedNotification, sendOrderPackagedNotification,
sendOrderDeliveredNotification, sendOrderDeliveredNotification,
} from "../../lib/notif-job"; } from "@/src/lib/notif-job";
import { publishCancellation } from "../../lib/post-order-handler"; import { publishCancellation } from "@/src/lib/post-order-handler"
import { getMultipleUserNegativityScores } from "../../stores/user-negativity-store"; import { getMultipleUserNegativityScores } from "@/src/stores/user-negativity-store"
const updateOrderNotesSchema = z.object({ const updateOrderNotesSchema = z.object({
orderId: z.number(), orderId: z.number(),

View file

@ -1,15 +1,15 @@
import { router, protectedProcedure } from "../trpc-index"; import { router, protectedProcedure } from "@/src/trpc/trpc-index"
import { z } from "zod"; import { z } from "zod";
import { db } from "../../db/db_index"; import { db } from "@/src/db/db_index"
import { import {
orders, orders,
orderStatus, orderStatus,
payments, payments,
refunds, refunds,
} from "../../db/schema"; } from "@/src/db/schema";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { ApiError } from "../../lib/api-error"; import { ApiError } from "@/src/lib/api-error"
import { RazorpayPaymentService } from "../../lib/payments-utils"; import { RazorpayPaymentService } from "@/src/lib/payments-utils"
const initiateRefundSchema = z const initiateRefundSchema = z
.object({ .object({

View file

@ -1,13 +1,13 @@
import { router, protectedProcedure } from '../trpc-index'; import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'; import { z } from 'zod';
import { db } from '../../db/db_index'; import { db } from '@/src/db/db_index'
import { productInfo, units, specialDeals, productSlots, productTags, productReviews, users, productGroupInfo, productGroupMembership } from '../../db/schema'; import { productInfo, units, specialDeals, productSlots, productTags, productReviews, users, productGroupInfo, productGroupMembership } from '@/src/db/schema'
import { eq, and, inArray, desc, sql } from 'drizzle-orm'; import { eq, and, inArray, desc, sql } from 'drizzle-orm';
import { ApiError } from '../../lib/api-error'; import { ApiError } from '@/src/lib/api-error'
import { imageUploadS3, generateSignedUrlsFromS3Urls, getOriginalUrlFromSignedUrl, claimUploadUrl } from '../../lib/s3-client'; import { imageUploadS3, generateSignedUrlsFromS3Urls, getOriginalUrlFromSignedUrl, claimUploadUrl } from '@/src/lib/s3-client'
import { deleteS3Image } from '../../lib/delete-image'; import { deleteS3Image } from '@/src/lib/delete-image'
import type { SpecialDeal } from '../../db/types'; import type { SpecialDeal } from '@/src/db/types'
import { initializeAllStores } from '../../stores/store-initializer'; import { initializeAllStores } from '@/src/stores/store-initializer'
type CreateDeal = { type CreateDeal = {
quantity: number; quantity: number;
@ -335,7 +335,7 @@ export const productRouter = router({
// Claim upload URLs // Claim upload URLs
if (uploadUrls && uploadUrls.length > 0) { 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))); await Promise.all(uploadUrls.map(url => claimUploadUrl(url)));
} }
@ -531,4 +531,4 @@ export const productRouter = router({
updatedCount: updates.length, updatedCount: updates.length,
}; };
}), }),
}); });

View file

@ -1,14 +1,14 @@
import { router, protectedProcedure } from "../trpc-index"; import { router, protectedProcedure } from "@/src/trpc/trpc-index"
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod";
import { db } from "../../db/db_index"; import { db } from "@/src/db/db_index"
import { deliverySlotInfo, productSlots, productInfo, vendorSnippets, productGroupInfo } from "../../db/schema"; import { deliverySlotInfo, productSlots, productInfo, vendorSnippets, productGroupInfo } from "@/src/db/schema"
import { eq, inArray, and, desc } from "drizzle-orm"; import { eq, inArray, and, desc } from "drizzle-orm";
import { ApiError } from "../../lib/api-error"; import { ApiError } from "@/src/lib/api-error"
import { appUrl } from "../../lib/env-exporter"; import { appUrl } from "@/src/lib/env-exporter"
import redisClient from "../../lib/redis-client"; import redisClient from "@/src/lib/redis-client"
import { getSlotSequenceKey } from "../../lib/redisKeyGetters"; import { getSlotSequenceKey } from "@/src/lib/redisKeyGetters"
import { initializeAllStores } from '../../stores/store-initializer'; import { initializeAllStores } from '@/src/stores/store-initializer'
interface CachedDeliverySequence { interface CachedDeliverySequence {
[userId: string]: number[]; [userId: string]: number[];

View file

@ -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 { z } from 'zod';
import { db } from '../../db/db_index'; import { db } from '@/src/db/db_index'
import { staffUsers, staffRoles, users, userDetails, orders } from '../../db/schema'; import { staffUsers, staffRoles, users, userDetails, orders } from '@/src/db/schema'
import { eq, or, ilike, and, lt, desc } from 'drizzle-orm'; import { eq, or, ilike, and, lt, desc } from 'drizzle-orm';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { ApiError } from '../../lib/api-error'; import { ApiError } from '@/src/lib/api-error'
export const staffUserRouter = router({ export const staffUserRouter = router({
login: publicProcedure login: publicProcedure

View file

@ -1,12 +1,12 @@
import { router, protectedProcedure } from '../trpc-index'; import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'; import { z } from 'zod';
import { db } from '../../db/db_index'; import { db } from '@/src/db/db_index'
import { storeInfo, productInfo } from '../../db/schema'; import { storeInfo, productInfo } from '@/src/db/schema'
import { eq, inArray } from 'drizzle-orm'; import { eq, inArray } from 'drizzle-orm';
import { ApiError } from '../../lib/api-error'; import { ApiError } from '@/src/lib/api-error'
import { extractKeyFromPresignedUrl, deleteImageUtil, generateSignedUrlFromS3Url } from '../../lib/s3-client'; import { extractKeyFromPresignedUrl, deleteImageUtil, generateSignedUrlFromS3Url } from '@/src/lib/s3-client'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; 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({ export const storeRouter = router({
getStores: protectedProcedure getStores: protectedProcedure

View file

@ -1,11 +1,11 @@
import { protectedProcedure } from '../trpc-index'; import { protectedProcedure } from '@/src/trpc/trpc-index';
import { z } from 'zod'; import { z } from 'zod';
import { db } from '../../db/db_index'; import { db } from '@/src/db/db_index';
import { users, complaints, orders, orderItems, notifCreds, unloggedUserTokens, userDetails, userIncidents } from '../../db/schema'; 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 { eq, sql, desc, asc, count, max, inArray } from 'drizzle-orm';
import { ApiError } from '../../lib/api-error'; import { ApiError } from '@/src/lib/api-error';
import { notificationQueue } from '../../lib/notif-job'; import { notificationQueue } from '@/src/lib/notif-job';
import { recomputeUserNegativityScore } from '../../stores/user-negativity-store'; import { recomputeUserNegativityScore } from '@/src/stores/user-negativity-store';
async function createUserByMobile(mobile: string): Promise<typeof users.$inferSelect> { async function createUserByMobile(mobile: string): Promise<typeof users.$inferSelect> {
// Clean mobile number (remove non-digits) // Clean mobile number (remove non-digits)
@ -212,7 +212,7 @@ export const userRouter = {
let orderStatuses: { orderId: number; isDelivered: boolean; isCancelled: boolean }[] = []; let orderStatuses: { orderId: number; isDelivered: boolean; isCancelled: boolean }[] = [];
if (orderIds.length > 0) { if (orderIds.length > 0) {
const { orderStatus } = await import('../../db/schema'); const { orderStatus } = await import('@/src/db/schema');
orderStatuses = await db orderStatuses = await db
.select({ .select({
orderId: orderStatus.orderId, orderId: orderStatus.orderId,

View file

@ -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 { z } from 'zod';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { db } from '../../db/db_index'; import { db } from '@/src/db/db_index'
import { vendorSnippets, deliverySlotInfo, productInfo, orders, orderItems, users, orderStatus } from '../../db/schema'; 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 { 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({ const createSnippetSchema = z.object({
snippetCode: z.string().min(1, "Snippet code is required"), snippetCode: z.string().min(1, "Snippet code is required"),

View file

@ -1,14 +1,14 @@
import { router, publicProcedure, protectedProcedure } from '../trpc-index'; import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index'
import { commonRouter } from './common'; import { commonRouter } from '@/src/trpc/apis/common-apis/common'
import { db } from '../../db/db_index'; import { db } from '@/src/db/db_index'
import { keyValStore, productInfo, storeInfo } from '../../db/schema'; import { keyValStore, productInfo, storeInfo } from '@/src/db/schema'
import * as turf from '@turf/turf'; import * as turf from '@turf/turf';
import { z } from 'zod'; import { z } from 'zod';
import { mbnrGeoJson } from '../../lib/mbnr-geojson'; import { mbnrGeoJson } from '@/src/lib/mbnr-geojson'
import { generateUploadUrl } from '../../lib/s3-client'; import { generateUploadUrl } from '@/src/lib/s3-client'
import { ApiError } from '../../lib/api-error'; import { ApiError } from '@/src/lib/api-error'
import { getAllConstValues } from '../../lib/const-store'; import { getAllConstValues } from '@/src/lib/const-store'
import { CONST_KEYS } from '../../lib/const-keys'; import { CONST_KEYS } from '@/src/lib/const-keys'
const polygon = turf.polygon(mbnrGeoJson.features[0].geometry.coordinates); const polygon = turf.polygon(mbnrGeoJson.features[0].geometry.coordinates);

View file

@ -1,11 +1,11 @@
import { router, publicProcedure } from '../trpc-index'; import { router, publicProcedure } from '@/src/trpc/trpc-index'
import { db } from '../../db/db_index'; import { db } from '@/src/db/db_index'
import { productInfo, units, productSlots, deliverySlotInfo, storeInfo, productTags, productTagInfo } from '../../db/schema'; import { productInfo, units, productSlots, deliverySlotInfo, storeInfo, productTags, productTagInfo } from '@/src/db/schema'
import { eq, gt, and, sql, inArray } from 'drizzle-orm'; 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 { z } from 'zod';
import { getAllProducts as getAllProductsFromCache } from '../../stores/product-store'; import { getAllProducts as getAllProductsFromCache } from '@/src/stores/product-store'
import { getDashboardTags as getDashboardTagsFromCache } from '../../stores/product-tag-store'; import { getDashboardTags as getDashboardTagsFromCache } from '@/src/stores/product-tag-store'
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
export const getNextDeliveryDate = async (productId: number): Promise<Date | null> => { export const getNextDeliveryDate = async (productId: number): Promise<Date | null> => {

View file

@ -0,0 +1,5 @@
import { router } from '@/src/trpc/trpc-index'
export const userRouter = router({})
export type UserRouter = typeof userRouter

View file

@ -1,8 +1,8 @@
import { router, publicProcedure } from './trpc-index'; import { router, publicProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'; import { z } from 'zod';
import { adminRouter } from './admin-apis/admin-trpc-index'; import { adminRouter } from '@/src/trpc/apis/admin-apis/apis/admin-trpc-index'
import { userRouter } from './user-apis/user-trpc-index'; import { userRouter } from '@/src/trpc/apis/user-apis/apis/user-trpc-index'
import { commonApiRouter } from './common-apis/common-trpc-index'; import { commonApiRouter } from '@/src/trpc/apis/common-apis/common-trpc-index'
// Create the main app router // Create the main app router
export const appRouter = router({ export const appRouter = router({

View file

@ -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' };
}),
});

View file

@ -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' };
}),
});

View file

@ -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,
};
}),
});

View file

@ -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);
}),
});

View file

@ -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' };
}),
});

View file

@ -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 };
}),
});

View file

@ -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;

View file

@ -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,
};
}),
});

View file

@ -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",
};
}),
});

View file

@ -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;
}),
});

View file

@ -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);
}),
});

View file

@ -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,
};
}),
});

View file

@ -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,
})),
};
}),
});

View file

@ -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;

View file

@ -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 };
}),
});

View file

@ -2,13 +2,13 @@ import { Request, Response, NextFunction } from 'express';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import { db } from '../db/db_index'; import { db } from '@/src/db/db_index'
import { users, userCreds, userDetails } from '../db/schema'; import { users, userCreds, userDetails } from '@/src/db/schema'
import { ApiError } from '../lib/api-error'; import { ApiError } from '@/src/lib/api-error'
import catchAsync from '../lib/catch-async'; import catchAsync from '@/src/lib/catch-async'
import { jwtSecret } from 'src/lib/env-exporter'; import { jwtSecret } from '@/src/lib/env-exporter';
import uploadHandler from '../lib/upload-handler'; import uploadHandler from '@/src/lib/upload-handler'
import { imageUploadS3, generateSignedUrlFromS3Url } from '../lib/s3-client'; import { imageUploadS3, generateSignedUrlFromS3Url } from '@/src/lib/s3-client'
interface RegisterRequest { interface RegisterRequest {
name: string; name: string;
@ -318,4 +318,4 @@ export const updateProfile = catchAsync(async (req: Request, res: Response, next
success: true, success: true,
data: response, data: response,
}); });
}); });

Some files were not shown because too many files have changed in this diff Show more