This commit is contained in:
shafi54 2026-09-12 11:45:07 +05:30
parent ee60799a8b
commit 8aaaa12ca0
50 changed files with 185 additions and 2149 deletions

View file

@ -114,6 +114,8 @@ er-ui/app/(drawer)/(tabs)/home/index.tsx, I want similar here") and expects the
- Wants the floating cart bar HIDDEN on routes where it is redundant — the cart page (including the flash cart alias) and the entire "me" section — and expects the condition to cover ALL child routes via a path-prefix check (`pathname.startsWith('/me')`) rather than matching only the parent route. Confidence: 0.6
- Uses Bun as his package manager / package runner ("I use bun"), so whenever giving or documenting commands he expects `bun run <script>` / `bunx <tool>` rather than `npm run` / `npx` (including inside subfolders, e.g. `bun run --cwd .. <script>`). Confidence: 0.9
- Expects generated test automation to be actually EXECUTED and passing, not just written/typechecking — he asks the agent to run the suite and report how many tests succeed ("try to run the script and see how many tests succeed"), treating a suite that was never run against a live app as unverified. Confidence: 0.65
- Treats reports/audits (e.g., DEAD_CODE_REPORT.md) as claims to be independently verified against actual code usage, not trusted at face value: he asks the agent to read a report and confirm "if the pointed code is actually dead", expecting per-claim caller searches and explicit flags of false positives. Confidence: 0.75
- After performing removals himself, expects the agent to re-scan the codebase and independently confirm each item is actually gone (and that the removals didn't break anything) — "I've removed them. Check and tell me if all are gone" — treating his own "done" statement as a claim to verify rather than assuming completion. Confidence: 0.6
e changes. Confidence: 0.95
er-ui/app/(drawer)/(tabs)/home/index.tsx, I want similar here") and expects the agent to mirror that pattern exactly, including details like item counts and container styling. Confidence: 0.9
als) rather than introducing new brand palettes; approval of a redesign's layout/composition does not imply approval of color or theme changes. Confidence: 0.95

162
DEAD_CODE_REPORT.md Normal file
View file

@ -0,0 +1,162 @@
# Dead Code Report — branch `DEAD_CODE_CLEAN`
> **Status: EXECUTED.** The dead code listed below has been removed (see "Removal status" at the bottom). The one exception is the Drizzle relation exports, which were kept after verification.
Static-analysis sweep for reachable-code / caller-less code across the monorepo.
**Method:** ripgrep-based caller search + manual verification.
- **tRPC endpoints:** every procedure in `admin`/`user`/`common` routers was enumerated, then searched for callers (`trpc.<path>` and `trpcClient.<path>`) in `apps/admin-ui`, `apps/user-ui`, `apps/web-ui`, `apps/admin-web`, `apps/fallback-ui`, `apps/info-site`.
- **Methods/functions:** exported + local `function`/`const` definitions in `apps/backend/src` and `packages/db_helper_sqlite/src` were searched repo-wide; classified as *truly uncalled* only when they appear nowhere except their definition (and pure re-export files).
- **Files:** inbound-reference scan by module name.
- Excludes `node_modules`, `dist`, `.wrangler`, `.output`, `.expo`, `.turbo`, `.tanstack`.
> Caveats at the bottom — some of these may be intentional (manual tooling, external API consumers, planned features). Verify before deleting.
---
## 1. Dead tRPC endpoints (15)
No caller in any client app (native or web). Each is still registered on the router, so it is reachable over HTTP but unused by this repo.
| Endpoint | File | Line | Note |
|---|---|---|---|
| `trpc.admin.coupon.validate` | `apps/backend/src/trpc/apis/admin-apis/apis/coupon.ts` | 265 | Admin coupon validation |
| `trpc.admin.vendorSnippets.getById` | `.../admin-apis/apis/vendor-snippets.ts` | 184 | Single snippet fetch |
| `trpc.admin.vendorSnippets.getVendorOrders` | `.../admin-apis/apis/vendor-snippets.ts` | 464 | Vendor orders |
| `trpc.admin.slots.getSlotsProductIds` | `.../admin-apis/apis/slots.ts` | 138 | Dup of product.* variant |
| `trpc.admin.slots.updateSlotProducts` | `.../admin-apis/apis/slots.ts` | 194 | Dup of product.* variant |
| `trpc.admin.slots.getSlots` | `.../admin-apis/apis/slots.ts` | 387 | Only `trpc.user.slots.getSlots` is used |
| `trpc.admin.slots.deleteSlot` | `.../admin-apis/apis/slots.ts` | 583 | No delete-slot UI |
| `trpc.admin.product.updateSlotProducts` | `.../admin-apis/apis/product.ts` | 392 | Duplicate router entry |
| `trpc.admin.product.getSlotsProductIds` | `.../admin-apis/apis/product.ts` | 462 | Duplicate router entry |
| `trpc.admin.staffUser.getUsers` | `.../admin-apis/apis/staff-user.ts` | 79 | `admin.user.getAllUsers` is the used one |
| `trpc.admin.staffUser.getUserDetails` | `.../admin-apis/apis/staff-user.ts` | 104 | `admin.user.getUserDetails` is used |
| `trpc.admin.staffUser.updateUserSuspension` | `.../admin-apis/apis/staff-user.ts` | 128 | `admin.user.updateUserSuspension` is used |
| `trpc.admin.user.createUserByMobile` | `.../admin-apis/apis/user.ts` | 29 | No caller |
| `trpc.common.product.getDashboardTags` | `.../common-apis/common.ts` | 144 | Home reads tags from `products.json`, not this |
| `trpc.hello` | `apps/backend/src/trpc/router.ts` | 14 | Scaffold sample endpoint |
**Notable:** the `slots``product` routers have **four duplicated** procedures (`getSlotsProductIds`, `updateSlotProducts`) — both copies are dead. The `staffUser` router duplicates three user-management procedures that are only ever called via the `admin.user` router.
---
## 2. Dead backend functions / constants (truly uncalled)
Defined/exported but never referenced anywhere else in the repo.
### Env / config
| Symbol | File |
|---|---|
| `getJwtSecret` | `apps/backend/src/lib/env-exporter.ts` |
| `getRedisUrl` | `apps/backend/src/lib/env-exporter.ts` |
| `getPhonePeBaseUrl` | `apps/backend/src/lib/env-exporter.ts` |
| `getPhonePeClientId` | `apps/backend/src/lib/env-exporter.ts` |
| `getPhonePeClientVersion` | `apps/backend/src/lib/env-exporter.ts` |
| `getPhonePeClientSecret` | `apps/backend/src/lib/env-exporter.ts` |
| `getPhonePeMerchantId` | `apps/backend/src/lib/env-exporter.ts` |
| `READABLE_ORDER_ID_KEY` | `apps/backend/src/lib/const-strings.ts` |
| `WELCOME_MESSAGE` | `apps/backend/src/lib/const-strings.ts` |
| `defaultRole` | `apps/backend/src/lib/roles-manager.ts` |
### Notifications / jobs
| Symbol | File | Note |
|---|---|---|
| `sendPushNotificationsMany` | `apps/backend/src/lib/expo-service.ts` | Whole file has no importers (see §4) |
| `sendOrderPlacedNotification` | `apps/backend/src/lib/notif-job.ts` | Only an unused import + a commented call in `user-apis/order.ts` |
| `sendOrderCancelledNotification` | `apps/backend/src/lib/notif-job.ts` | Same — imported, call commented |
| `sendPaymentFailedNotification` | `apps/backend/src/lib/notif-job.ts` | No reference |
| `sendOrderOutForDeliveryNotification` | `apps/backend/src/lib/notif-job.ts` | No reference |
| `sendRefundInitiatedNotification` | `apps/backend/src/lib/notif-job.ts` | No reference |
*(Alive: `sendAdminNotification` via `queue-consumer.ts``worker.ts`; `sendOrderPackagedNotification` and `sendOrderDeliveredNotification` via admin order api; `scheduleNotification` via those.)*
### Stores / cache / misc
| Symbol | File |
|---|---|
| `getOrderDetailsWrapper` | `apps/backend/src/dbService.ts` |
| `clearAllCache` | `apps/backend/src/lib/cloud_cache.ts` |
| `getAllBanners` | `apps/backend/src/stores/banner-store.ts` |
| `getTagById` | `apps/backend/src/stores/product-tag-store.ts` |
| `getAllTags` | `apps/backend/src/stores/product-tag-store.ts` |
| `getProductSlots` | `apps/backend/src/stores/slot-store.ts` |
| `getAllProductsSlots` | `apps/backend/src/stores/slot-store.ts` |
| `getUserNegativity` | `apps/backend/src/stores/user-negativity-store.ts` |
| `createTRPCRouter` | `apps/backend/src/trpc/trpc-index.ts` (alias re-export, unused) |
---
## 3. Dead exports in `db_helper_sqlite`
| Symbol | File | Note |
|---|---|---|
| `mergeDuplicateProducts` | `packages/db_helper_sqlite/src/admin-apis/merge-duplicate-products.ts` | Exported via index + `sqliteImporter`, but **no code calls it** (function had a "how to use manually" doc comment → manual utility). **Removed.** |
`productSkusRelations`, `productMarketStatsRelations`, `skuFeaturesRelations`, `productCombosRelations` (`src/db/schema.ts`) looked unreferenced but are **NOT dead** — Drizzle registers relation exports via `import * as schema` in `db_index.ts`, and `db.query.*.findMany({ with: … })` depends on them. **Kept.**
Used only inside their own file (helpers of live code — **not dead**): `parseDuplicateProductsMd` (deleted with the merge utility), `splitQuantityFeature`, `cleanFeatureValue`, `productTypeEnum`.
---
## 4. Dead / unreachable files & endpoints
| Item | File | Note |
|---|---|---|
| Expo push service | `apps/backend/src/lib/expo-service.ts` | No module imports it; its only export is uncalled |
| `av-router` | `apps/backend/src/apis/admin-apis/apis/av-router.ts` | Mounted at `/api/v1/av` but registers **zero routes** (staff-auth middleware only) |
| REST product summary | `apps/backend/src/apis/common-apis/apis/common-product.controller.ts` (`getAllProductsSummary`) | Reachable at `/api/v1/cm/products/summary`, but **no client calls it** (apps use tRPC) |
`/api/test` (`test-controller.ts`) is referenced only by `apps/fallback-ui/src/routes/demo.tsx` (a demo page).
---
## 5. Unused imports that point at dead code
| File | Unused import | Reality |
|---|---|---|
| `apps/backend/src/trpc/apis/user-apis/apis/order.ts` | `sendOrderPlacedNotification` (line 29) | Call is commented out (line 235) |
| `apps/backend/src/trpc/apis/user-apis/apis/order.ts` | `sendOrderCancelledNotification` (line 30) | Call is commented out (line 598) |
---
## 6. Commented-out / unreachable code blocks
Not "callers-less" but unreachable. Many "Old implementation" blocks were kept across the backend; the most notable registered-looking ones:
- `apps/backend/src/trpc/apis/common-apis/common.ts:160-176``getStoresSummary` and `healthCheck` procedures inside a `/* … */` block (NOT registered; the live ones are in `common-trpc-index.ts`).
- `apps/backend/src/main-router.ts``// router.route('/av', avRouter)` (route disabled).
- Numerous `// Old implementation - direct DB queries:` blocks in `admin-apis/*`, `user-apis/*`, and db helpers (historical only).
---
## 7. Checked and NOT dead (for reference)
- `apps/admin-ui` / `user-ui` / `web-ui` exports: **0 truly-unused**. 18 shared exports (`AdminOrderItemCore`, `SkuFlagCore`, `UserStoreSummaryCore`, …) are referenced only within their own file to compose other types — that's normal, not dead.
- Backend functions used only inside their own file (e.g. `cloud_cache` internals `createProductsFileInternal`, `constructCacheUrl`, `clearUrlCache`; `slot-store` `transformSlotToStoreSlot`/`extractSlotInfo`/`fetchAllTransformedSlots`; `trpc-index` `createCallerFactory`) — all reachable via their parent functions, **not dead**.
- `queue-consumer.ts` — although it imports only-in-backend, it IS used by `worker.ts`, so alive.
---
## Caveats
1. **Static analysis only.** A tRPC procedure can still be called by an external/undisclosed client (mobile build, partner integration). Confirm before removing public procedures.
2. **`mergeDuplicateProducts`** and **`expo-service`** look intentionally kept for manual/one-off use — decide whether to delete or document.
3. The `admin-web` tree is present on disk but **untracked** on this branch; it was included in the caller search.
4. Some "dead" management procedures (`staffUser.*`, duplicated `slots`/`product` entries) may be leftovers from older UI screens; safe to remove once confirmed.
## Removal status (executed)
**Removed — 15 tRPC procedures:** `admin.coupon.validate`; `admin.vendorSnippets.getById`, `getVendorOrders`; `admin.slots.getSlotsProductIds`, `updateSlotProducts`, `getSlots`, `deleteSlot`; `admin.product.updateSlotProducts`, `getSlotsProductIds`; `admin.staffUser.getUsers`, `getUserDetails`, `updateUserSuspension`; `admin.user.createUserByMobile`; `common.product.getDashboardTags`; `trpc.hello`.
**Removed — backend functions/constants:** `getJwtSecret`, `getRedisUrl`, `getExpoAccessToken`, `getPhonePeBaseUrl/ClientId/ClientVersion/ClientSecret/MerchantId`, `READABLE_ORDER_ID_KEY`, `WELCOME_MESSAGE`, `defaultRole`, `getOrderDetailsWrapper`, `clearAllCache`, `getAllBanners`, `getTagById`, `getAllTags`, `getDashboardTags`, `getProductSlots`, `getAllProductsSlots`, `getUserNegativity`, `createTRPCRouter`, `createCallerFactory`, and the notification senders `sendOrderPlacedNotification`, `sendPaymentFailedNotification`, `sendOrderOutForDeliveryNotification`, `sendOrderCancelledNotification`, `sendRefundInitiatedNotification` + their message constants.
**Removed — DB helper functions orphaned by the procedures above (from BOTH `db_helper_sqlite` and `db_helper_postgres`, plus their barrel exports and `sqliteImporter` re-exports):**
`createUserByMobile`, `deleteSlotById`, `getVendorOrders`, `updateSlotProducts`, `getSlotsProductIds` (the five behind the deleted endpoints) **and** `getActiveSlots`, `getAllUsers`, `getUserWithDetails`, `validateCoupon`, `checkUnitExists`, `getProductImagesById`, `replaceProductTags` (same class, missed by the first scan because their names also exist in the parallel package).
**Removed — files:** `apps/backend/src/lib/expo-service.ts`, `apps/backend/src/middleware/staff-auth.ts` (only used by the empty `av-router`), `apps/backend/src/apis/admin-apis/apis/av-router.ts` (empty router), `packages/db_helper_sqlite/src/admin-apis/merge-duplicate-products.ts`.
**Removed — wiring:** `avRouter` import/route from `main-router.ts` + `v1-router.ts`; `mergeDuplicateProducts` exports from `db_helper_sqlite/index.ts` and `sqliteImporter.ts`; unused `sendOrder*Notification` import + commented calls in `user-apis/order.ts`; orphaned imports in all touched files.
**Kept (false positives / intentional):** the four Drizzle relation exports (implicitly registered); `mergeDuplicateProducts` was deleted per §3.
**Verification:** `tsc` clean for `apps/backend`, `packages/db_helper_sqlite`, `apps/web-ui`, `apps/admin-ui`, `apps/user-ui`, `apps/admin-web`. (`apps/fallback-ui` has one pre-existing `vite.config.ts` error unrelated to this change.)

View file

@ -1,11 +0,0 @@
import { Hono } from 'hono';
import { authenticateStaff } from "@/src/middleware/staff-auth";
const router = new Hono();
// Apply staff authentication to all admin routes
router.use('*', authenticateStaff);
const avRouter = router;
export default avRouter;

View file

@ -1,9 +1,7 @@
// Database Service - Central export for all database-related imports
// This file re-exports everything from postgresImporter to provide a clean abstraction layer
import type { AdminOrderDetails } from '@packages/shared'
// import { getOrderDetails } from '@/src/postgresImporter'
import { getOrderDetails, initDb } from '@/src/sqliteImporter'
import { initDb } from '@/src/sqliteImporter'
// Re-export everything from postgresImporter
// export * from '@/src/postgresImporter'
@ -13,10 +11,6 @@ export * from '@/src/sqliteImporter'
export { initDb }
// Re-export getOrderDetails with the correct signature
export async function getOrderDetailsWrapper(orderId: number): Promise<AdminOrderDetails | null> {
return getOrderDetails(orderId)
}
// Re-export all types from shared package
export type {
// Admin types

View file

@ -248,41 +248,3 @@ export async function clearUrlCache(urls: string[]): Promise<{ success: boolean;
return { success: false, errors: [errorMessage] }
}
}
export async function clearAllCache(): Promise<{ success: boolean; errors?: string[] }> {
const cloudflareApiToken = getCloudflareApiToken()
const cloudflareZoneId = getCloudflareZoneId()
if (!cloudflareApiToken || !cloudflareZoneId) {
console.warn('Cloudflare credentials not configured, skipping cache clear')
return { success: false, errors: ['Cloudflare credentials not configured'] }
}
try {
const resp = await fetch(
`https://api.cloudflare.com/client/v4/zones/${cloudflareZoneId}/purge_cache`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${cloudflareApiToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ purge_everything: true }),
}
)
const result = await resp.json() as { success: boolean; errors?: { message: string }[] }
if (!result.success) {
const errorMessages = result.errors?.map(e => e.message) || ['Unknown error']
console.error('Cloudflare cache purge failed:', errorMessages)
return { success: false, errors: errorMessages }
}
console.log('Successfully purged all cache from Cloudflare')
return { success: true }
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
console.error('Error clearing Cloudflare cache:', errorMessage)
return { success: false, errors: [errorMessage] }
}
}

View file

@ -4,21 +4,14 @@
*/
// User role and designation constants
export const READABLE_ORDER_ID_KEY = 'readableOrderId';
// Queue constants
export const NOTIFS_QUEUE = 'notifications';
export const OTP_COMMENT_NAME='otp-comment'
// Notification message constants
export const ORDER_PLACED_MESSAGE = 'Your order has been placed successfully!';
export const PAYMENT_FAILED_MESSAGE = 'Payment failed. Please try again.';
export const ORDER_PACKAGED_MESSAGE = 'Your order has been packaged and is ready for delivery.';
export const ORDER_OUT_FOR_DELIVERY_MESSAGE = 'Your order is out for delivery.';
export const ORDER_DELIVERED_MESSAGE = 'Your order has been delivered.';
export const ORDER_CANCELLED_MESSAGE = 'Your order has been cancelled.';
export const REFUND_INITIATED_MESSAGE = 'Refund has been initiated for your order.';
export const WELCOME_MESSAGE = 'Welcome to Farm2Door! Thank you for joining us.';
export const REFUND_STATUS = {
PENDING: 'none',

View file

@ -63,7 +63,6 @@ const getRuntimeEnv = () => (globalThis as any).ENV || (globalThis as any).proce
export const getAppUrl = () => getRuntimeEnv().APP_URL as string
export const getJwtSecret = () => getRuntimeEnv().JWT_SECRET as string
export const defaultRoleName = 'gen_user';
@ -88,22 +87,8 @@ export const getApiCacheKey = () => getRuntimeEnv().API_CACHE_KEY as string
export const getCloudflareApiToken = () => getRuntimeEnv().CLOUDFLARE_API_TOKEN as string
export const getCloudflareZoneId = () => getRuntimeEnv().CLOUDFLARE_ZONE_ID as string
export const getS3Url = () => getRuntimeEnv().S3_URL as string
export const getRedisUrl = () => getRuntimeEnv().REDIS_URL as string
export const getExpoAccessToken = () => getRuntimeEnv().EXPO_ACCESS_TOKEN as string
export const getPhonePeBaseUrl = () => getRuntimeEnv().PHONE_PE_BASE_URL as string
export const getPhonePeClientId = () => getRuntimeEnv().PHONE_PE_CLIENT_ID as string
export const getPhonePeClientVersion = () => Number(getRuntimeEnv().PHONE_PE_CLIENT_VERSION as string)
export const getPhonePeClientSecret = () => getRuntimeEnv().PHONE_PE_CLIENT_SECRET as string
export const getPhonePeMerchantId = () => getRuntimeEnv().PHONE_PE_MERCHANT_ID as string
export const getRazorpayId = () => getRuntimeEnv().RAZORPAY_KEY as string

View file

@ -1,40 +0,0 @@
import { Expo } from "expo-server-sdk";
import { getExpoAccessToken } from "@/src/lib/env-exporter"
const expo = new Expo({
accessToken: getExpoAccessToken(),
useFcmV1: true,
});
type NotificationArgs = {
pushToken: string;
title?: string;
body?: string;
data: Record<string, unknown> | undefined;
// data?: object;
};
export const sendPushNotificationsMany = async (args: NotificationArgs[]) => {
// const { pushToken, title, body, data } = args;
const notifPayloads = args.map((arg) => ({
to: arg.pushToken,
title: arg.title,
body: arg.body,
data: arg.data,
sound: "default",
priority: "high" as any,
}));
const chunks = expo.chunkPushNotifications(notifPayloads);
let tickets = [];
(async () => {
for (let chunk of chunks) {
try {
let ticketChunk = await expo.sendPushNotificationsAsync(chunk);
tickets.push(...ticketChunk);
} catch (error) {
console.error(error);
}
}
})();
};

View file

@ -3,14 +3,8 @@
import { scaffoldAssetUrl } from '@/src/lib/s3-client'
import { queueDataPusher } from '@/src/lib/queue-data-pusher'
import {
NOTIFS_QUEUE,
ORDER_PLACED_MESSAGE,
PAYMENT_FAILED_MESSAGE,
ORDER_PACKAGED_MESSAGE,
ORDER_OUT_FOR_DELIVERY_MESSAGE,
ORDER_DELIVERED_MESSAGE,
ORDER_CANCELLED_MESSAGE,
REFUND_INITIATED_MESSAGE
ORDER_DELIVERED_MESSAGE
} from '@/src/lib/const-strings';
@ -100,24 +94,6 @@ export async function scheduleNotification(userId: number, payload: any, options
}
// Utility methods for specific notification events
export async function sendOrderPlacedNotification(userId: number, orderId?: string) {
await scheduleNotification(userId, {
title: 'Order Placed',
body: ORDER_PLACED_MESSAGE,
type: 'order',
orderId
});
}
export async function sendPaymentFailedNotification(userId: number, orderId?: string) {
await scheduleNotification(userId, {
title: 'Payment Failed',
body: PAYMENT_FAILED_MESSAGE,
type: 'payment',
orderId
});
}
export async function sendOrderPackagedNotification(userId: number, orderId?: string) {
await scheduleNotification(userId, {
title: 'Order Packaged',
@ -127,15 +103,6 @@ export async function sendOrderPackagedNotification(userId: number, orderId?: st
});
}
export async function sendOrderOutForDeliveryNotification(userId: number, orderId?: string) {
await scheduleNotification(userId, {
title: 'Out for Delivery',
body: ORDER_OUT_FOR_DELIVERY_MESSAGE,
type: 'order',
orderId
});
}
export async function sendOrderDeliveredNotification(userId: number, orderId?: string) {
await scheduleNotification(userId, {
title: 'Order Delivered',
@ -145,23 +112,6 @@ export async function sendOrderDeliveredNotification(userId: number, orderId?: s
});
}
export async function sendOrderCancelledNotification(userId: number, orderId?: string) {
await scheduleNotification(userId, {
title: 'Order Cancelled',
body: ORDER_CANCELLED_MESSAGE,
type: 'order',
orderId
});
}
export async function sendRefundInitiatedNotification(userId: number, orderId?: string) {
await scheduleNotification(userId, {
title: 'Refund Initiated',
body: REFUND_INITIATED_MESSAGE,
type: 'refund',
orderId
});
}
//
// process.on('SIGTERM', async () => {
// await notificationQueue.close();

View file

@ -9,7 +9,6 @@ export const ROLE_NAMES = {
RECEPTIONIST: 'receptionist'
};
export const defaultRole = ROLE_NAMES.GENERAL_USER;
/**
* RoleManager class to handle caching and retrieving role information

View file

@ -1,6 +1,4 @@
import { Hono } from 'hono';
import avRouter from "@/src/apis/admin-apis/apis/av-router"
import { ApiError } from "@/src/lib/api-error"
import v1Router from "@/src/v1-router"
import testController from "@/src/test-controller"
import { authenticateUser } from "@/src/middleware/auth.middleware"
@ -29,7 +27,6 @@ router.get('/seed', (c) => {
router.use('*', authenticateUser);
router.route('/v1', v1Router);
// router.route('/av', avRouter);
router.route('/test', testController);
const mainRouter = router;

View file

@ -1,73 +0,0 @@
import { Context, Next } from 'hono';
import { jwtVerify } from 'jose';
import { getStaffUserById } from '@/src/dbService';
import { ApiError } from '@/src/lib/api-error';
import { getEncodedJwtSecret } from '@/src/lib/env-exporter';
/**
* Verify JWT token and extract payload
*/
const verifyStaffToken = async (token: string) => {
try {
const { payload } = await jwtVerify(token, getEncodedJwtSecret());
return payload;
} catch (error) {
throw new ApiError('Access denied. Invalid auth credentials', 401);
}
};
/**
* Middleware to authenticate staff users and attach staffUser to context
*/
export const authenticateStaff = async (c: Context, next: Next) => {
try {
// Extract token from Authorization header
const authHeader = c.req.header('authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new ApiError('Staff authentication required', 401);
}
const token = authHeader.split(' ')[1];
if (!token) {
throw new ApiError('Staff authentication token missing', 401);
}
// Verify token and extract payload
const decoded = await verifyStaffToken(token) as any;
// Verify staffId exists in token
if (!decoded.staffId) {
throw new ApiError('Invalid staff token format', 401);
}
/*
// Old implementation - direct DB queries:
import { db } from '@/src/db/db_index'
import { staffUsers } from '@/src/db/schema'
import { eq } from 'drizzle-orm';
const staff = await db.query.staffUsers.findFirst({
where: eq(staffUsers.id, decoded.staffId),
});
*/
// Fetch staff user from database
const staff = await getStaffUserById(decoded.staffId);
if (!staff) {
throw new ApiError('Staff user not found', 401);
}
// Attach staff user to context
c.set('staffUser', {
id: staff.id,
name: staff.name,
});
await next();
} catch (error) {
throw error;
}
};

View file

@ -35,7 +35,6 @@ export {
getAllCoupons,
getCouponById,
invalidateCoupon,
validateCoupon,
getReservedCoupons,
getUsersForCoupon,
createCouponWithRelations,
@ -67,12 +66,7 @@ export {
createProduct,
updateProduct,
checkProductExistsByName,
checkUnitExists,
getProductImagesById,
replaceProductTags,
replaceTagProducts,
updateSlotProducts,
getSlotsProductIds,
getAllProductTags,
getAllProductTagInfos,
getProductTagInfoById,
@ -89,16 +83,12 @@ export {
deleteProductGroup,
updateProductPrices,
toggleProductOutOfStock,
// Merge duplicate products into multi-SKU products
mergeDuplicateProducts,
// Admin - Slots
getActiveSlotsWithProducts,
getActiveSlots,
getSlotsAfterDate,
getSlotByIdWithRelations,
createSlotWithRelations,
updateSlotWithRelations,
deleteSlotById,
updateSlotCapacity,
getSlotDeliverySequence,
updateSlotDeliverySequence,
@ -107,8 +97,6 @@ export {
getStaffUserByName,
getStaffUserById,
getAllStaff,
getAllUsers,
getUserWithDetails,
checkStaffUserExists,
checkStaffRoleExists,
createStaffUser,
@ -120,7 +108,6 @@ export {
updateStore,
deleteStore,
// Admin - User
createUserByMobile,
getUserByMobile,
getUnresolvedComplaintsCount,
getAllUsersWithFilters,
@ -151,7 +138,6 @@ export {
getVendorSlotById,
getVendorOrdersBySlotId,
updateVendorOrderItemPackaging,
getVendorOrders,
// User - Address
getUserDefaultAddress,
getUserAddresses,

View file

@ -2,7 +2,6 @@
import type { BannerData } from '@packages/shared'
import {
getAllBannersForCache,
getBanners,
getBannerById as getBannerByIdFromDb,
} from '@/src/dbService'
import { scaffoldAssetUrl } from '@/src/lib/s3-client'
@ -76,47 +75,3 @@ export async function getBannerById(id: number): Promise<BannerData | null> {
return null
}
}
export async function getAllBanners(): Promise<BannerData[]> {
try {
// Get all keys matching the pattern "banner:*"
// const keys = await redisClient.KEYS('banner:*')
//
// if (keys.length === 0) return []
//
// // Get all banners using MGET for better performance
// const bannersData = await redisClient.MGET(keys)
//
// const banners: Banner[] = []
// for (const bannerData of bannersData) {
// if (bannerData) {
// banners.push(JSON.parse(bannerData) as Banner)
// }
// }
//
// // Sort by serialNum to maintain the same order as the original query
// banners.sort((a, b) => (a.serialNum || 0) - (b.serialNum || 0))
//
// return banners
const banners = await getBanners()
return banners.map((banner) => {
const signedImageUrl = banner.imageUrl
? scaffoldAssetUrl(banner.imageUrl)
: banner.imageUrl
return {
id: banner.id,
name: banner.name,
imageUrl: signedImageUrl,
serialNum: banner.serialNum,
skuIds: banner.skuIds,
createdAt: banner.createdAt,
}
})
} catch (error) {
console.error('Error getting all banners:', error)
return []
}
}

View file

@ -3,9 +3,6 @@ import {
getAllTagsForCache,
getAllTagProductMappings,
getAllProductTags,
getProductTagById as getProductTagByIdFromDb,
type TagBasicData,
type TagProductMapping,
} from '@/src/dbService'
import { scaffoldAssetUrl } from '@/src/lib/s3-client'
import type { ProductTagCore } from '@packages/shared'
@ -120,96 +117,6 @@ export async function initializeProductTagStore(): Promise<void> {
}
}
export async function getTagById(id: number): Promise<Tag | null> {
try {
// const key = `tag:${id}`
// const data = await redisClient.get(key)
// if (!data) return null
// return JSON.parse(data) as Tag
const tag = await getProductTagByIdFromDb(id)
if (!tag) return null
return transformTagToStoreTag(tag)
} catch (error) {
console.error(`Error getting tag ${id}:`, error)
return null
}
}
export async function getAllTags(): Promise<Tag[]> {
try {
// Get all keys matching the pattern "tag:*"
// const keys = await redisClient.KEYS('tag:*')
//
// if (keys.length === 0) {
// return []
// }
//
// // Get all tags using MGET for better performance
// const tagsData = await redisClient.MGET(keys)
//
// const tags: Tag[] = []
// for (const tagData of tagsData) {
// if (tagData) {
// tags.push(JSON.parse(tagData) as Tag)
// }
// }
//
// return tags
const tags = await getAllProductTags()
const result: Tag[] = []
for (const tag of tags) {
result.push(await transformTagToStoreTag(tag))
}
return result
} catch (error) {
console.error('Error getting all tags:', error)
return []
}
}
export async function getDashboardTags(): Promise<Tag[]> {
try {
// Get all keys matching the pattern "tag:*"
// const keys = await redisClient.KEYS('tag:*')
//
// if (keys.length === 0) {
// return []
// }
//
// // Get all tags using MGET for better performance
// const tagsData = await redisClient.MGET(keys)
//
// const dashboardTags: Tag[] = []
// for (const tagData of tagsData) {
// if (tagData) {
// const tag = JSON.parse(tagData) as Tag
// if (tag.isDashboardTag) {
// dashboardTags.push(tag)
// }
// }
// }
//
// return dashboardTags
const tags = await getAllProductTags()
const result: Tag[] = []
for (const tag of tags) {
if (tag.isDashboardTag) {
result.push(await transformTagToStoreTag(tag))
}
}
return result
} catch (error) {
console.error('Error getting dashboard tags:', error)
return []
}
}
export async function getTagsByStoreId(storeId: number): Promise<Tag[]> {
try {
// Get all keys matching the pattern "tag:*"

View file

@ -5,7 +5,6 @@ import {
} from '@/src/dbService'
import type { UserProductDeliverySlot } from '@packages/shared'
import { scaffoldAssetUrl } from '@/src/lib/s3-client'
import dayjs from 'dayjs'
// Define the structure for slot with products — anchored to the shared slot core
interface SlotWithProducts extends UserProductDeliverySlot {
@ -213,76 +212,6 @@ export async function getAllSlots(): Promise<SlotWithProducts[]> {
}
}
export async function getProductSlots(productId: number): Promise<SlotInfo[]> {
try {
// const key = `product:${productId}:slots`
// const data = await redisClient.get(key)
// if (!data) return []
// return JSON.parse(data) as SlotInfo[]
const slots = await getAllSlotsWithProductsForCache()
const productSlots: SlotInfo[] = []
for (const slot of slots) {
const hasProduct = slot.products.some(p => p.id === productId)
if (hasProduct) {
productSlots.push(extractSlotInfo(slot))
}
}
return productSlots
} catch (error) {
console.error(`Error getting slots for product ${productId}:`, error)
return []
}
}
export async function getAllProductsSlots(): Promise<Record<number, SlotInfo[]>> {
try {
// Get all keys matching the pattern "product:*:slots"
// const keys = await redisClient.KEYS('product:*:slots')
//
// if (keys.length === 0) return {}
//
// // Get all product slots using MGET for better performance
// const productsData = await redisClient.MGET(keys)
//
// const result: Record<number, SlotInfo[]> = {}
// for (const key of keys) {
// // Extract productId from key "product:{id}:slots"
// const match = key.match(/product:(\d+):slots/)
// if (match) {
// const productId = parseInt(match[1], 10)
// const dataIndex = keys.indexOf(key)
// if (productsData[dataIndex]) {
// result[productId] = JSON.parse(productsData[dataIndex]) as SlotInfo[]
// }
// }
// }
//
// return result
const slots = await getAllSlotsWithProductsForCache()
const result: Record<number, SlotInfo[]> = {}
for (const slot of slots) {
const slotInfo = extractSlotInfo(slot)
for (const product of slot.products) {
const productId = product.id
if (!result[productId]) {
result[productId] = []
}
result[productId].push(slotInfo)
}
}
return result
} catch (error) {
console.error('Error getting all products slots:', error)
return {}
}
}
export async function getMultipleProductsSlots(
productIds: number[]
): Promise<Record<number, SlotInfo[]>> {

View file

@ -2,7 +2,6 @@
import {
getAllUserNegativityScores as getAllUserNegativityScoresFromDb,
getUserNegativityScore as getUserNegativityScoreFromDb,
type UserNegativityData,
} from '@/src/dbService'
export async function initializeUserNegativityStore(): Promise<void> {
@ -40,24 +39,6 @@ export async function initializeUserNegativityStore(): Promise<void> {
}
}
export async function getUserNegativity(userId: number): Promise<number> {
try {
// const key = `user:negativity:${userId}`
// const data = await redisClient.get(key)
//
// if (!data) {
// return 0
// }
//
// return parseInt(data, 10)
return await getUserNegativityScoreFromDb(userId)
} catch (error) {
console.error(`Error getting negativity score for user ${userId}:`, error)
return 0
}
}
export async function getAllUserNegativityScores(): Promise<Record<number, number>> {
try {
// const keys = await redisClient.KEYS('user:negativity:*')

View file

@ -6,7 +6,6 @@ import {
getAllCoupons as getAllCouponsFromDb,
getCouponById as getCouponByIdFromDb,
invalidateCoupon as invalidateCouponInDb,
validateCoupon as validateCouponInDb,
getReservedCoupons as getReservedCouponsFromDb,
getUsersForCoupon as getUsersForCouponFromDb,
// Batch 2 - Transaction methods
@ -20,7 +19,7 @@ import {
checkReservedCouponExists,
getOrderWithUser,
} from '@/src/dbService'
import type { Coupon, CouponFormInput, CouponValidationResult, UserMiniInfo } from '@packages/shared'
import type { Coupon, CouponFormInput, UserMiniInfo } from '@packages/shared'
const createCouponBodySchema = z.object({
couponCode: z.string().optional(),
@ -40,12 +39,6 @@ const createCouponBodySchema = z.object({
exclusiveApply: z.boolean().optional(),
});
const validateCouponBodySchema = z.object({
code: z.string(),
userId: z.number(),
orderAmount: z.number(),
});
export const couponRouter = router({
create: protectedProcedure
.input(createCouponBodySchema)
@ -262,20 +255,6 @@ export const couponRouter = router({
return { message: "Coupon invalidated successfully" };
}),
validate: protectedProcedure
.input(validateCouponBodySchema)
.query(async ({ input }): Promise<CouponValidationResult> => {
const { code, userId, orderAmount } = input;
if (!code || typeof code !== 'string') {
return { valid: false, message: "Invalid coupon code" };
}
const result = await validateCouponInDb(code, userId, orderAmount);
return result;
}),
generateCancellationCoupon: protectedProcedure
.input(
z.object({

View file

@ -8,8 +8,6 @@ import {
getAllProducts as getAllProductsInDb,
getProductById as getProductByIdInDb,
deleteProduct as deleteProductInDb,
updateSlotProducts as updateSlotProductsInDb,
getSlotsProductIds as getSlotsProductIdsInDb,
getProductReviews as getProductReviewsInDb,
respondToReview as respondToReviewInDb,
getAllProductGroups as getAllProductGroupsInDb,
@ -19,11 +17,8 @@ import {
updateProductPrices as updateProductPricesInDb,
toggleProductOutOfStock as toggleProductOutOfStockInDb,
checkProductExistsByName,
checkUnitExists,
createProduct as createProductInDb,
replaceProductTags,
replaceTagProducts,
getProductImagesById,
updateProduct as updateProductInDb,
checkProductTagExistsByName,
createProductTag as createProductTagInDb,
@ -34,9 +29,7 @@ import {
getProductTagById as getProductTagByIdInDb,
} from '@/src/dbService'
import type {
AdminProduct,
AdminProductWithRelations,
AdminSpecialDeal,
AdminProductGroupsResult,
AdminProductGroupResponse,
AdminProductReviewsResult,
@ -44,8 +37,6 @@ import type {
AdminProductListResponse,
AdminProductResponse,
AdminDeleteProductResult,
AdminUpdateSlotProductsResult,
AdminSlotsProductIdsResult,
AdminUpdateProductPricesResult,
AdminToggleOutOfStockResult,
} from '@packages/shared'
@ -193,8 +184,10 @@ export const productRouter = router({
throw new ApiError('Product not found', 404)
}
// Regenerate products/availability cache files so the user app reflects it.
await scheduleStoreInitialization()
// Only the availability cache needs regenerating for a stock flip.
await createAvailabilityCacheFile().catch((err) => {
console.error('Failed to regenerate availability cache after stock update:', err)
})
return {
productId: result.id,
@ -387,126 +380,6 @@ export const productRouter = router({
}
}),
updateSlotProducts: protectedProcedure
.input(z.object({
slotId: z.string(),
skuIds: z.array(z.string()),
}))
.mutation(async ({ input }): Promise<AdminUpdateSlotProductsResult> => {
const { slotId, skuIds } = input;
if (!Array.isArray(skuIds)) {
throw new ApiError("skuIds must be an array", 400);
}
const result = await updateSlotProductsInDb(slotId, skuIds)
/*
// Old implementation - direct DB queries:
// Get current associations
const currentAssociations = await db.query.productSlots.findMany({
where: eq(productSlots.slotId, parseInt(slotId)),
columns: {
productId: true,
},
});
const currentProductIds = currentAssociations.map(assoc => assoc.productId);
const newProductIds = productIds.map((id: string) => parseInt(id));
// Find products to add and remove
const productsToAdd = newProductIds.filter(id => !currentProductIds.includes(id));
const productsToRemove = currentProductIds.filter(id => !newProductIds.includes(id));
// Remove associations for products that are no longer selected
if (productsToRemove.length > 0) {
await db.delete(productSlots).where(
and(
eq(productSlots.slotId, parseInt(slotId)),
inArray(productSlots.productId, productsToRemove)
)
);
}
// Add associations for newly selected products
if (productsToAdd.length > 0) {
const newAssociations = productsToAdd.map(productId => ({
productId,
slotId: parseInt(slotId),
}));
await db.insert(productSlots).values(newAssociations);
}
// Reinitialize stores to reflect changes
await scheduleStoreInitialization()
return {
message: "Slot products updated successfully",
added: productsToAdd.length,
removed: productsToRemove.length,
};
*/
await scheduleStoreInitialization()
return {
message: 'Slot products updated successfully',
added: result.added,
removed: result.removed,
}
}),
getSlotsProductIds: protectedProcedure
.input(z.object({
slotIds: z.array(z.number()),
}))
.query(async ({ input }): Promise<AdminSlotsProductIdsResult> => {
const { slotIds } = input;
if (!Array.isArray(slotIds)) {
throw new ApiError("slotIds must be an array", 400);
}
const result = await getSlotsProductIdsInDb(slotIds)
/*
// Old implementation - direct DB queries:
if (slotIds.length === 0) {
return {};
}
// Fetch all associations for the requested slots
const associations = await db.query.productSlots.findMany({
where: inArray(productSlots.slotId, slotIds),
columns: {
slotId: true,
productId: true,
},
});
// Group by slotId
const result = associations.reduce((acc, assoc) => {
if (!acc[assoc.slotId]) {
acc[assoc.slotId] = [];
}
acc[assoc.slotId].push(assoc.productId);
return acc;
}, {} as Record<number, number[]>);
// Ensure all requested slots have entries (even if empty)
slotIds.forEach(slotId => {
if (!result[slotId]) {
result[slotId] = [];
}
});
return result;
*/
return result
}),
getProductReviews: protectedProcedure
.input(z.object({
productId: z.number().int().positive(),

View file

@ -5,35 +5,25 @@ import { ApiError } from "@/src/lib/api-error"
import { getAppUrl } from "@/src/lib/env-exporter"
// import redisClient from "@/src/lib/redis-client"
// import { getSlotSequenceKey } from "@/src/lib/redisKeyGetters"
import { scheduleStoreInitialization } from '@/src/stores/store-initializer'
import { createSlotsCacheFile } from '@/src/lib/cloud_cache'
import {
getActiveSlotsWithProducts as getActiveSlotsWithProductsInDb,
getActiveSlots as getActiveSlotsInDb,
getSlotsAfterDate as getSlotsAfterDateInDb,
getSlotByIdWithRelations as getSlotByIdWithRelationsInDb,
createSlotWithRelations as createSlotWithRelationsInDb,
updateSlotWithRelations as updateSlotWithRelationsInDb,
deleteSlotById as deleteSlotByIdInDb,
updateSlotCapacity as updateSlotCapacityInDb,
getSlotDeliverySequence as getSlotDeliverySequenceInDb,
updateSlotDeliverySequence as updateSlotDeliverySequenceInDb,
updateSlotProducts as updateSlotProductsInDb,
getSlotsProductIds as getSlotsProductIdsInDb,
staleSlotsCleanup,
} from '@/src/dbService'
import type {
AdminDeliverySequenceResult,
AdminSlotResult,
AdminSlotsResult,
AdminSlotsListResult,
AdminSlotCreateResult,
AdminSlotUpdateResult,
AdminSlotDeleteResult,
AdminUpdateDeliverySequenceResult,
AdminUpdateSlotCapacityResult,
AdminSlotsProductIdsResult,
AdminUpdateSlotProductsResult,
} from '@packages/shared'
@ -41,8 +31,6 @@ interface CachedDeliverySequence {
[userId: string]: number[];
}
const cachedSequenceSchema = z.record(z.string(), z.array(z.number()));
const createSlotSchema = z.object({
deliveryTime: z.string(),
freezeTime: z.string(),
@ -76,10 +64,6 @@ const updateSlotSchema = z.object({
groupIds: z.array(z.number()).optional(),
});
const deleteSlotSchema = z.object({
id: z.number(),
});
const getDeliverySequenceSchema = z.object({
id: z.string(),
});
@ -135,151 +119,7 @@ export const slotsRouter = router({
}),
// Exact replica of POST /av/products/slots/product-ids
getSlotsProductIds: protectedProcedure
.input(z.object({ slotIds: z.array(z.number()) }))
.query(async ({ input, ctx }): Promise<AdminSlotsProductIdsResult> => {
if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
}
const { slotIds } = input;
if (!Array.isArray(slotIds)) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "slotIds must be an array",
});
}
const result = await getSlotsProductIdsInDb(slotIds)
/*
// Old implementation - direct DB queries:
if (slotIds.length === 0) {
return {};
}
// Fetch all associations for the requested slots
const associations = await db.query.productSlots.findMany({
where: inArray(productSlots.slotId, slotIds),
columns: {
slotId: true,
productId: true,
},
});
// Group by slotId
const result = associations.reduce((acc, assoc) => {
if (!acc[assoc.slotId]) {
acc[assoc.slotId] = [];
}
acc[assoc.slotId].push(assoc.productId);
return acc;
}, {} as Record<number, number[]>);
// Ensure all requested slots have entries (even if empty)
slotIds.forEach((slotId) => {
if (!result[slotId]) {
result[slotId] = [];
}
});
return result;
*/
return result
}),
// Exact replica of PUT /av/products/slots/:slotId/products
updateSlotProducts: protectedProcedure
.input(
z.object({
slotId: z.number(),
skuIds: z.array(z.number()),
})
)
.mutation(async ({ input, ctx }): Promise<AdminUpdateSlotProductsResult> => {
if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
}
const { slotId, skuIds } = input;
if (!Array.isArray(skuIds)) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "skuIds must be an array",
});
}
const result = await updateSlotProductsInDb(String(slotId), skuIds.map(String))
/*
// Old implementation - direct DB queries:
// Get current associations
const currentAssociations = await db.query.productSlots.findMany({
where: eq(productSlots.slotId, slotId),
columns: {
productId: true,
},
});
const currentProductIds = currentAssociations.map(
(assoc) => assoc.productId
);
const newProductIds = productIds;
// Find products to add and remove
const productsToAdd = newProductIds.filter(
(id) => !currentProductIds.includes(id)
);
const productsToRemove = currentProductIds.filter(
(id) => !newProductIds.includes(id)
);
// Remove associations for products that are no longer selected
if (productsToRemove.length > 0) {
await db
.delete(productSlots)
.where(
and(
eq(productSlots.slotId, slotId),
inArray(productSlots.productId, productsToRemove)
)
);
}
// Add associations for newly selected products
if (productsToAdd.length > 0) {
const newAssociations = productsToAdd.map((productId) => ({
productId,
slotId,
}));
await db.insert(productSlots).values(newAssociations);
}
// Reinitialize stores to reflect changes
await scheduleStoreInitialization()
return {
message: "Slot products updated successfully",
added: productsToAdd.length,
removed: productsToRemove.length,
};
*/
await createSlotsCacheFile().catch((err) => {
console.error('Failed to regenerate slots cache after product update:', err)
})
return {
message: result.message,
added: result.added,
removed: result.removed,
}
}),
createSlot: protectedProcedure
.input(createSlotSchema)
.mutation(async ({ input, ctx }): Promise<AdminSlotCreateResult> => {
@ -384,26 +224,6 @@ export const slotsRouter = router({
return result
}),
getSlots: protectedProcedure.query(async ({ ctx }): Promise<AdminSlotsListResult> => {
if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
}
const slots = await getActiveSlotsInDb()
/*
// Old implementation - direct DB queries:
const slots = await db.query.deliverySlotInfo.findMany({
where: eq(deliverySlotInfo.isActive, true),
});
*/
return {
slots,
count: slots.length,
}
}),
getSlotById: protectedProcedure
.input(getSlotByIdSchema)
.query(async ({ input, ctx }): Promise<AdminSlotResult> => {
@ -580,44 +400,6 @@ export const slotsRouter = router({
}
}),
deleteSlot: protectedProcedure
.input(deleteSlotSchema)
.mutation(async ({ input, ctx }): Promise<AdminSlotDeleteResult> => {
if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
}
const { id } = input;
const deletedSlot = await deleteSlotByIdInDb(id)
/*
// Old implementation - direct DB queries:
const [deletedSlot] = await db
.update(deliverySlotInfo)
.set({ isActive: false })
.where(eq(deliverySlotInfo.id, id))
.returning();
if (!deletedSlot) {
throw new ApiError("Slot not found", 404);
}
*/
if (!deletedSlot) {
throw new ApiError('Slot not found', 404)
}
// Regenerate slots cache file (availability/products stay as-is)
await createSlotsCacheFile().catch((error) => {
console.error('Failed to regenerate slots cache after slot delete:', error)
})
return {
message: 'Slot deleted successfully',
}
}),
getDeliverySequence: protectedProcedure
.input(getDeliverySequenceSchema)
.query(async ({ input, ctx }): Promise<AdminDeliverySequenceResult> => {

View file

@ -7,15 +7,11 @@ import { ApiError } from '@/src/lib/api-error'
import {
getStaffUserByName,
getAllStaff,
getAllUsers,
getUserWithDetails,
upsertUserSuspension,
checkStaffUserExists,
checkStaffRoleExists,
createStaffUser,
getAllRoles,
} from '@/src/dbService'
import type { StaffUser, StaffRole } from '@packages/shared'
export const staffUserRouter = router({
login: publicProcedure
@ -76,65 +72,6 @@ export const staffUserRouter = router({
};
}),
getUsers: protectedProcedure
.input(z.object({
cursor: z.number().optional(),
limit: z.number().default(20),
search: z.string().optional(),
}))
.query(async ({ input }) => {
const { cursor, limit, search } = input;
const { users: usersToReturn, hasMore } = await getAllUsers(cursor, limit, search);
const formattedUsers = usersToReturn.map((user: any) => ({
id: user.id,
name: user.name,
email: user.email,
mobile: user.mobile,
image: user.userDetails?.profileImage || null,
}));
return {
users: formattedUsers,
nextCursor: hasMore ? usersToReturn[usersToReturn.length - 1].id : undefined,
};
}),
getUserDetails: protectedProcedure
.input(z.object({ userId: z.number() }))
.query(async ({ input }) => {
const { userId } = input;
const user = await getUserWithDetails(userId);
if (!user) {
throw new ApiError("User not found", 404);
}
const lastOrder = user.orders[0];
return {
id: user.id,
name: user.name,
email: user.email,
mobile: user.mobile,
addedOn: user.createdAt,
lastOrdered: lastOrder?.createdAt || null,
isSuspended: user.userDetails?.isSuspended || false,
};
}),
updateUserSuspension: protectedProcedure
.input(z.object({ userId: z.number(), isSuspended: z.boolean() }))
.mutation(async ({ input }) => {
const { userId, isSuspended } = input;
await upsertUserSuspension(userId, isSuspended);
return { success: true };
}),
createStaffUser: protectedProcedure
.input(z.object({
name: z.string().min(1, 'Name is required'),

View file

@ -3,8 +3,6 @@ import { z } from 'zod';
import { ApiError } from '@/src/lib/api-error';
import { recomputeUserNegativityScore } from '@/src/stores/user-negativity-store';
import {
createUserByMobile,
getUserByMobile,
getUnresolvedComplaintsCount,
getAllUsersWithFilters,
getOrderCountsByUserIds,
@ -26,34 +24,6 @@ import {
import { queueDataPusher } from '@/src/lib/queue-data-pusher'
export const userRouter = {
createUserByMobile: protectedProcedure
.input(z.object({
mobile: z.string().min(1, 'Mobile number is required'),
}))
.mutation(async ({ input }) => {
// Clean mobile number (remove non-digits)
const cleanMobile = input.mobile.replace(/\D/g, '');
// Validate: exactly 10 digits
if (cleanMobile.length !== 10) {
throw new ApiError('Mobile number must be exactly 10 digits', 400);
}
// Check if user already exists
const existingUser = await getUserByMobile(cleanMobile);
if (existingUser) {
throw new ApiError('User with this mobile number already exists', 409);
}
const newUser = await createUserByMobile(cleanMobile);
return {
success: true,
data: newUser,
};
}),
getEssentials: protectedProcedure
.query(async () => {
const count = await getUnresolvedComplaintsCount();

View file

@ -13,7 +13,6 @@ import {
getProductsByIds as getProductsByIdsInDb,
getVendorSlotById as getVendorSlotByIdInDb,
getVendorOrdersBySlotId as getVendorOrdersBySlotIdInDb,
getVendorOrders as getVendorOrdersInDb,
updateVendorOrderItemPackaging as updateVendorOrderItemPackagingInDb,
getSlotsAfterDate as getSlotsAfterDateInDb,
composeSkuName,
@ -22,11 +21,9 @@ import {
import type {
AdminVendorSnippet,
AdminVendorSnippetWithProducts,
AdminVendorSnippetWithSlot,
AdminVendorSnippetDeleteResult,
AdminVendorSnippetOrdersResult,
AdminVendorSnippetOrdersWithSlotResult,
AdminVendorOrderSummary,
AdminUpcomingSlotsResult,
AdminVendorUpdatePackagingResult,
} from '@packages/shared'
@ -181,36 +178,6 @@ export const vendorSnippetsRouter = router({
return []
}),
getById: protectedProcedure
.input(z.object({ id: z.number().int().positive() }))
.query(async ({ input }): Promise<AdminVendorSnippetWithSlot> => {
const { id } = input;
const result = await getVendorSnippetByIdInDb(id)
/*
// Old implementation - direct DB queries:
const result = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.id, id),
with: {
slot: true,
},
});
if (!result) {
throw new Error("Vendor snippet not found");
}
return result;
*/
if (!result) {
throw new Error('Vendor snippet not found')
}
return result
}),
update: protectedProcedure
.input(updateSnippetSchema)
.mutation(async ({ input }): Promise<AdminVendorSnippet> => {
@ -461,42 +428,6 @@ export const vendorSnippetsRouter = router({
}
}),
getVendorOrders: protectedProcedure
.query(async (): Promise<AdminVendorOrderSummary[]> => {
const vendorOrders = await getVendorOrdersInDb()
/*
// Old implementation - direct DB queries:
const vendorOrders = await db.query.orders.findMany({
with: {
user: true,
orderItems: {
with: {
product: {
with: {
unit: true,
},
},
},
},
},
orderBy: (orders, { desc }) => [desc(orders.createdAt)],
});
*/
return vendorOrders.map(order => ({
id: order.id,
status: 'pending',
orderDate: order.createdAt ? order.createdAt.toISOString() : new Date(0).toISOString(),
totalQuantity: order.orderItems.reduce((sum, item) => sum + parseFloat(item.quantity || '0'), 0),
products: order.orderItems.map(item => ({
name: composeSkuName(item.sku?.product?.name || 'Unknown', item.sku?.features || []),
quantity: parseFloat(item.quantity || '0'),
unit: composeUnitNotation(item.sku?.features || []) || 'unit',
})),
}))
}),
getUpcomingSlots: publicProcedure
.query(async (): Promise<AdminUpcomingSlotsResult> => {
const threeHoursAgo = dayjs().subtract(3, 'hour').toDate();

View file

@ -10,7 +10,6 @@ import {
} from '@/src/dbService'
import { generateSignedUrlsFromS3Urls, generateSignedUrlFromS3Url, scaffoldAssetUrl } from '@/src/lib/s3-client'
import { getAllProducts as getAllProductsFromCache } from '@/src/stores/product-store'
import { getDashboardTags as getDashboardTagsFromCache } from '@/src/stores/product-tag-store'
import { getConstant } from '@/src/lib/const-store'
import { CONST_KEYS } from '@/src/lib/const-keys'
@ -141,16 +140,6 @@ export async function scaffoldAvailability() {
}
export const commonRouter = router({
getDashboardTags: publicProcedure
.query(async () => {
// Get dashboard tags from cache
const tags = await getDashboardTagsFromCache();
return {
tags: tags,
};
}),
getAllProductsSummary: publicProcedure
.query(async () => {
const response = await scaffoldProducts();

View file

@ -25,10 +25,6 @@ import {
import { scaffoldAssetUrl } from "@/src/lib/s3-client";
import { ApiError } from "@/src/lib/api-error";
import type { DeliveryStatus, OrderStatus } from "@packages/shared";
import {
sendOrderPlacedNotification,
sendOrderCancelledNotification,
} from "@/src/lib/notif-job";
import { CONST_KEYS, getConstant, getConstants } from "@/src/lib/const-store";
import { publishFormattedOrder, publishCancellation } from "@/src/lib/post-order-handler";
import type {
@ -231,10 +227,6 @@ const placeOrderUtil = async (params: {
);
}
// for (const order of createdOrders) {
// sendOrderPlacedNotification(userId, order.id.toString());
// }
console.log('publishing the order')
await publishFormattedOrder(createdOrders, ordersBySlot);
@ -595,8 +587,6 @@ export const orderRouter = router({
await cancelUserOrderTransaction(id, status.id, reason, order.isCod);
// await sendOrderCancelledNotification(userId, id.toString());
await publishCancellation(id, 'user', reason);
return { success: true, message: "Order cancelled successfully" };

View file

@ -9,7 +9,6 @@ import {
getUserProductReviews as getUserProductReviewsInDb,
getUserProductByIdBasic as getUserProductByIdBasicInDb,
createUserProductReview as createUserProductReviewInDb,
getOffersAndCombos as getOffersAndCombosInDb,
} from '@/src/dbService'
import type {
UserProductDetail,
@ -165,21 +164,4 @@ export const productRouter = router({
return { success: true, review: newReview }
}),
getOffersPage: publicProcedure
.query(async () => {
const data = await getOffersAndCombosInDb();
const signImages = (products: typeof data.combos) =>
products.map((product) => ({
...product,
images: scaffoldAssetUrl((product.images as string[]) || []),
}));
return {
success: true,
combos: signImages(data.combos),
offers: signImages(data.offers),
};
}),
});

View file

@ -1,5 +1,4 @@
import { router, publicProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod';
import { router } from '@/src/trpc/trpc-index'
import { adminRouter } from '@/src/trpc/apis/admin-apis/apis/admin-trpc-index'
import { userRouter } from '@/src/trpc/apis/user-apis/apis/user-trpc-index'
import { commonApiRouter } from '@/src/trpc/apis/common-apis/common-trpc-index'
@ -11,11 +10,6 @@ import { scaffoldBanners } from './apis/user-apis/apis/banners';
// Create the main app router
export const appRouter = router({
hello: publicProcedure
.input(z.object({ name: z.string() }))
.query(({ input }) => {
return { greeting: `Hello ${input.name}!` };
}),
admin: adminRouter,
user: userRouter,
common: commonApiRouter,

View file

@ -69,6 +69,3 @@ export const protectedProcedure = t.procedure.use(errorLoggerMiddleware).use(
return next();
})
);
export const createCallerFactory = t.createCallerFactory;
export const createTRPCRouter = t.router;

View file

@ -1,10 +1,8 @@
import { Hono } from 'hono';
import avRouter from "@/src/apis/admin-apis/apis/av-router"
import commonRouter from "@/src/apis/common-apis/apis/common.router"
const router = new Hono();
router.route('/av', avRouter);
router.route('/cm', commonRouter);
const v1Router = router;

View file

@ -40,7 +40,6 @@ export default {
if (!app) {
app = createApp()
}
// await mergeDuplicateProducts()
return app.fetch(request, env, ctx)
},
async queue(

View file

@ -162,7 +162,10 @@ export function useAllProducts() {
})
const mergedProducts = useMemo(() => {
const rawProducts = productsQuery.data?.products || []
// Combo-only SKUs stay in products.json but are hidden from the user app.
const rawProducts = (productsQuery.data?.products || []).filter(
(p: any) => !p.isComboOnly
)
const availabilityById: Record<number, AvailabilityEntry> = {}
availabilityData?.availability?.forEach((entry: AvailabilityEntry) => {
availabilityById[entry.id] = entry

View file

@ -12,6 +12,9 @@ export function getQueryClient() {
staleTime: 30 * 1000,
retry: 2,
refetchOnWindowFocus: false,
// Refetch when a screen remounts (navigating back to it) even within
// staleTime — the web equivalent of native useMarkDataFetchers.
refetchOnMount: 'always',
},
},
})

View file

@ -1,9 +1,9 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useState } from 'react'
import { trpc } from '../lib/trpc-client'
import { Tag, Loader2, AlertCircle } from 'lucide-react'
import { ProductCard } from '../components/ProductCard'
import { AppLayout } from '../components/AppLayout'
import { useAllProducts } from '../hooks/prominent-api-hooks'
import type { OffersSectionProps } from '@packages/shared'
export const Route = createFileRoute('/offers')({ component: OffersPage })
@ -61,10 +61,11 @@ function OffersPage() {
const [expandedOffers, setExpandedOffers] = useState(false)
const [expandedCombos, setExpandedCombos] = useState(false)
const { data, isLoading, error } = trpc.user.product.getOffersPage.useQuery()
const { data, isLoading, error } = useAllProducts()
const combos = data?.combos || []
const offers = data?.offers || []
const allProducts = data?.products || []
const combos = allProducts.filter((p: any) => p.productType === 'combo')
const offers = allProducts.filter((p: any) => p.isOffer)
const handleProductPress = (id: number) => {
navigate({ to: '/home/product/$id', params: { id: String(id) } })

View file

@ -37,7 +37,6 @@ export {
getAllCoupons,
getCouponById,
invalidateCoupon,
validateCoupon,
getReservedCoupons,
getUsersForCoupon,
createCouponWithRelations,
@ -75,11 +74,6 @@ export {
createProduct,
updateProduct,
checkProductExistsByName,
checkUnitExists,
getProductImagesById,
replaceProductTags,
updateSlotProducts,
getSlotsProductIds,
getAllProductTags,
getAllProductTagInfos,
getProductTagInfoById,
@ -100,12 +94,10 @@ export {
export {
// Slots
getActiveSlotsWithProducts,
getActiveSlots,
getSlotsAfterDate,
getSlotByIdWithRelations,
createSlotWithRelations,
updateSlotWithRelations,
deleteSlotById,
updateSlotCapacity,
getSlotDeliverySequence,
updateSlotDeliverySequence,
@ -117,8 +109,6 @@ export {
getStaffUserByName,
getStaffUserById,
getAllStaff,
getAllUsers,
getUserWithDetails,
checkStaffUserExists,
checkStaffRoleExists,
createStaffUser,
@ -136,7 +126,6 @@ export {
export {
// User
createUserByMobile,
getUserByMobile,
getUnresolvedComplaintsCount,
getAllUsersWithFilters,
@ -170,7 +159,6 @@ export {
getVendorSlotById,
getVendorOrdersBySlotId,
updateVendorOrderItemPackaging,
getVendorOrders,
} from './src/admin-apis/vendor-snippets';
export {

View file

@ -191,60 +191,6 @@ export async function invalidateCoupon(id: number): Promise<Coupon> {
return result[0] as Coupon;
}
export async function validateCoupon(
code: string,
userId: number,
orderAmount: number
): Promise<CouponValidationResult> {
const coupon = await db.query.coupons.findFirst({
where: and(
eq(coupons.couponCode, code.toUpperCase()),
eq(coupons.isInvalidated, false)
),
});
if (!coupon) {
return { valid: false, message: "Coupon not found or invalidated" };
}
if (coupon.validTill && new Date(coupon.validTill) < new Date()) {
return { valid: false, message: "Coupon has expired" };
}
if (!coupon.isApplyForAll && !coupon.isUserBased) {
return { valid: false, message: "Coupon is not available for use" };
}
const minOrderValue = coupon.minOrder ? parseFloat(coupon.minOrder) : 0;
if (minOrderValue > 0 && orderAmount < minOrderValue) {
return { valid: false, message: `Minimum order amount is ${minOrderValue}` };
}
let discountAmount = 0;
if (coupon.discountPercent) {
const percent = parseFloat(coupon.discountPercent);
discountAmount = (orderAmount * percent) / 100;
} else if (coupon.flatDiscount) {
discountAmount = parseFloat(coupon.flatDiscount);
}
const maxValueLimit = coupon.maxValue ? parseFloat(coupon.maxValue) : 0;
if (maxValueLimit > 0 && discountAmount > maxValueLimit) {
discountAmount = maxValueLimit;
}
return {
valid: true,
discountAmount,
coupon: {
id: coupon.id,
discountPercent: coupon.discountPercent,
flatDiscount: coupon.flatDiscount,
maxValue: coupon.maxValue,
}
};
}
export async function getReservedCoupons(
cursor?: number,
limit: number = 50,

View file

@ -174,33 +174,6 @@ export async function updateProduct(id: number, updates: ProductInfoUpdate): Pro
return mapProduct(product)
}
export async function updateSlotProducts(slotId: string, productIds: string[]): Promise<AdminUpdateSlotProductsResult> {
const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, parseInt(slotId)),
})
if (!slot) {
throw new Error(`Slot ${slotId} not found`)
}
const currentProductIds = slot.productIds || []
const newProductIds = productIds.map((id) => parseInt(id))
// Simply update the productIds array
await db.update(deliverySlotInfo)
.set({ productIds: newProductIds })
.where(eq(deliverySlotInfo.id, parseInt(slotId)))
const productsToAdd = newProductIds.filter((id) => !currentProductIds.includes(id))
const productsToRemove = currentProductIds.filter((id) => !newProductIds.includes(id))
return {
message: 'Slot products updated successfully',
added: productsToAdd.length,
removed: productsToRemove.length,
}
}
export async function getAllProductTags(): Promise<AdminProductTagWithProducts[]> {
const tags = await db.query.productTagInfo.findMany({
with: {
@ -243,29 +216,6 @@ export async function getProductTagInfoById(tagId: number): Promise<AdminProduct
return mapTagInfo(tag)
}
export async function getSlotsProductIds(slotIds: number[]): Promise<Record<number, number[]>> {
if (slotIds.length === 0) {
return {}
}
const slots = await db.query.deliverySlotInfo.findMany({
where: inArray(deliverySlotInfo.id, slotIds),
})
const result: Record<number, number[]> = {}
for (const slot of slots) {
result[slot.id] = slot.productIds || []
}
slotIds.forEach((slotId) => {
if (!result[slotId]) {
result[slotId] = []
}
})
return result
}
export async function getProductReviews(productId: number, limit: number, offset: number) {
const reviews = await db
.select({
@ -617,40 +567,3 @@ export async function checkProductExistsByName(name: string): Promise<boolean> {
return !!product
}
export async function checkUnitExists(unitId: number): Promise<boolean> {
const unit = await db.query.units.findFirst({
where: eq(units.id, unitId),
columns: { id: true },
})
return !!unit
}
export async function getProductImagesById(productId: number): Promise<string[] | null> {
const product = await db.query.productInfo.findFirst({
where: eq(productInfo.id, productId),
columns: { images: true },
})
if (!product) {
return null
}
return getStringArray(product.images) || []
}
export async function replaceProductTags(productId: number, tagIds: number[]): Promise<void> {
await db.delete(productTags).where(eq(productTags.productId, productId))
if (tagIds.length === 0) {
return
}
const tagAssociations = tagIds.map((tagId) => ({
productId,
tagId,
}))
await db.insert(productTags).values(tagAssociations)
}

View file

@ -122,14 +122,6 @@ export async function staleSlotsCleanup(): Promise<number> {
return result.rowCount || 0
}
export async function getActiveSlots(): Promise<AdminDeliverySlot[]> {
const slots = await db.query.deliverySlotInfo.findMany({
where: eq(deliverySlotInfo.isActive, true),
})
return slots.map(mapDeliverySlot)
}
export async function getSlotsAfterDate(afterDate: Date): Promise<AdminDeliverySlot[]> {
const slots = await db.query.deliverySlotInfo.findMany({
where: and(
@ -307,20 +299,6 @@ export async function updateSlotWithRelations(input: {
return result
}
export async function deleteSlotById(id: number): Promise<AdminDeliverySlot | null> {
const [deletedSlot] = await db
.update(deliverySlotInfo)
.set({ isActive: false })
.where(eq(deliverySlotInfo.id, id))
.returning()
if (!deletedSlot) {
return null
}
return mapDeliverySlot(deletedSlot)
}
export async function getSlotDeliverySequence(slotId: number): Promise<AdminDeliverySlot | null> {
const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, slotId),

View file

@ -41,56 +41,6 @@ export async function getAllStaff(): Promise<any[]> {
return staff;
}
export async function getAllUsers(
cursor?: number,
limit: number = 20,
search?: string
): Promise<{ users: any[]; hasMore: boolean }> {
let whereCondition = undefined;
if (search) {
whereCondition = or(
ilike(users.name, `%${search}%`),
ilike(users.email, `%${search}%`),
ilike(users.mobile, `%${search}%`)
);
}
if (cursor) {
const cursorCondition = lt(users.id, cursor);
whereCondition = whereCondition ? and(whereCondition, cursorCondition) : cursorCondition;
}
const allUsers = await db.query.users.findMany({
where: whereCondition,
with: {
userDetails: true,
},
orderBy: desc(users.id),
limit: limit + 1,
});
const hasMore = allUsers.length > limit;
const usersToReturn = hasMore ? allUsers.slice(0, limit) : allUsers;
return { users: usersToReturn, hasMore };
}
export async function getUserWithDetails(userId: number): Promise<any | null> {
const user = await db.query.users.findFirst({
where: eq(users.id, userId),
with: {
userDetails: true,
orders: {
orderBy: desc(orders.createdAt),
limit: 1,
},
},
});
return user || null;
}
export async function checkStaffUserExists(name: string): Promise<boolean> {
const existingUser = await db.query.staffUsers.findFirst({
where: eq(staffUsers.name, name),

View file

@ -2,19 +2,6 @@ import { db } from '../db/db_index';
import { users, userDetails, orders, orderItems, complaints, notifCreds, unloggedUserTokens, userIncidents, orderStatus } from '../db/schema';
import { eq, sql, desc, asc, count, max, inArray } from 'drizzle-orm';
export async function createUserByMobile(mobile: string): Promise<any> {
const [newUser] = await db
.insert(users)
.values({
name: null,
email: null,
mobile,
})
.returning();
return newUser;
}
export async function getUserByMobile(mobile: string): Promise<any | null> {
const [existingUser] = await db
.select()

View file

@ -166,24 +166,6 @@ export async function getVendorOrdersBySlotId(slotId: number) {
})
}
export async function getVendorOrders() {
return await db.query.orders.findMany({
with: {
user: true,
orderItems: {
with: {
product: {
with: {
unit: true,
},
},
},
},
},
orderBy: (orders, { desc }) => [desc(orders.createdAt)],
})
}
export async function updateVendorOrderItemPackaging(
orderItemId: number,
isPackaged: boolean

View file

@ -40,7 +40,6 @@ export {
getAllCoupons,
getCouponById,
invalidateCoupon,
validateCoupon,
getReservedCoupons,
getUsersForCoupon,
createCouponWithRelations,
@ -78,12 +77,7 @@ export {
createProduct,
updateProduct,
checkProductExistsByName,
checkUnitExists,
getProductImagesById,
replaceProductTags,
replaceTagProducts,
updateSlotProducts,
getSlotsProductIds,
getAllProductTags,
getAllProductTagInfos,
getProductTagInfoById,
@ -102,20 +96,13 @@ export {
toggleProductOutOfStock,
} from './src/admin-apis/product'
export {
// Merge duplicate products into multi-SKU products
mergeDuplicateProducts,
} from './src/admin-apis/merge-duplicate-products'
export {
// Slots
getActiveSlotsWithProducts,
getActiveSlots,
getSlotsAfterDate,
getSlotByIdWithRelations,
createSlotWithRelations,
updateSlotWithRelations,
deleteSlotById,
updateSlotCapacity,
getSlotDeliverySequence,
updateSlotDeliverySequence,
@ -127,8 +114,6 @@ export {
getStaffUserByName,
getStaffUserById,
getAllStaff,
getAllUsers,
getUserWithDetails,
checkStaffUserExists,
checkStaffRoleExists,
createStaffUser,
@ -146,7 +131,6 @@ export {
export {
// User
createUserByMobile,
getUserByMobile,
getUnresolvedComplaintsCount,
getAllUsersWithFilters,
@ -180,7 +164,6 @@ export {
getVendorSlotById,
getVendorOrdersBySlotId,
updateVendorOrderItemPackaging,
getVendorOrders,
} from './src/admin-apis/vendor-snippets'
export {
@ -220,7 +203,6 @@ export {
createProductReview as createUserProductReview,
getAllProductsWithUnits,
getAllSkusSummary,
getOffersAndCombos,
type ProductSummaryData,
} from './src/user-apis/product'

View file

@ -168,60 +168,6 @@ export async function invalidateCoupon(id: number): Promise<Coupon> {
return result[0] as Coupon
}
export async function validateCoupon(
code: string,
userId: number,
orderAmount: number
): Promise<CouponValidationResult> {
const coupon = await db.query.coupons.findFirst({
where: and(
eq(coupons.couponCode, code.toUpperCase()),
eq(coupons.isInvalidated, false)
),
})
if (!coupon) {
return { valid: false, message: 'Coupon not found or invalidated' }
}
if (coupon.validTill && new Date(coupon.validTill) < new Date()) {
return { valid: false, message: 'Coupon has expired' }
}
if (!coupon.isApplyForAll && !coupon.isUserBased) {
return { valid: false, message: 'Coupon is not available for use' }
}
const minOrderValue = coupon.minOrder ? parseFloat(coupon.minOrder) : 0
if (minOrderValue > 0 && orderAmount < minOrderValue) {
return { valid: false, message: `Minimum order amount is ${minOrderValue}` }
}
let discountAmount = 0
if (coupon.discountPercent) {
const percent = parseFloat(coupon.discountPercent)
discountAmount = (orderAmount * percent) / 100
} else if (coupon.flatDiscount) {
discountAmount = parseFloat(coupon.flatDiscount)
}
const maxValueLimit = coupon.maxValue ? parseFloat(coupon.maxValue) : 0
if (maxValueLimit > 0 && discountAmount > maxValueLimit) {
discountAmount = maxValueLimit
}
return {
valid: true,
discountAmount,
coupon: {
id: coupon.id,
discountPercent: coupon.discountPercent,
flatDiscount: coupon.flatDiscount,
maxValue: coupon.maxValue,
}
}
}
export async function getReservedCoupons(
cursor?: number,
limit: number = 50,

View file

@ -1,494 +0,0 @@
/**
* merge-duplicate-products.ts
*
* Merges duplicate products (the same item entered as separate product_info rows)
* into single products with multiple SKUs.
*
* The merge groups are defined by PRODUCT NAME, sourced from
* docs/duplicate-products-local_8_aug.md not by hardcoded ids. The md's
* "Clear duplicates" section is embedded below (DUPLICATES_MD) and parsed at
* module load; GROUPS_TO_MERGE decides which sections actually get merged.
*
* For every selected group this function:
* 1. Resolves the group's product rows in the DB by normalized-name matching
* 2. Creates ONE new product_info row (clean name = md section header)
* 3. Creates a new product_skus row per old product, copying images + features
* (plus any group-specific extra features) and product_market_stats
* 4. Re-points every reference to the old SKUs / old products:
* product_tags.product_id, product_group_membership.product_id,
* order_items.sku_id, cart_items.sku_id, special_deals.sku_id,
* coupon_applicable_products.sku_id, product_combos.sku_id & combo_sku_id,
* product_reviews.product_id, and the JSON `sku_ids` arrays in
* home_banners, delivery_slot_info, vendor_snippets, coupons, reserved_coupons
* 5. Deletes the old product_market_stats, sku_features, product_skus and
* product_info rows
*
* Call it whenever you wish:
*
* import { mergeDuplicateProducts } from '@packages/db_helper_sqlite'
* const report = await mergeDuplicateProducts()
*
* You can also pass the md content explicitly (e.g. read from disk in a script):
*
* const md = await Bun.file('docs/duplicate-products-local_8_aug.md').text()
* const report = await mergeDuplicateProducts(md)
*
* It is safe to call repeatedly: groups whose rows no longer exist (e.g. from
* a previous run) are skipped and reported in `report.skippedGroups` instead
* of throwing. It only throws on genuinely anomalous data (a matched product
* with more than one SKU) or if any leftover reference to old ids survives.
*/
import { db } from '../db/db_index'
import { eq, inArray, isNotNull, sql } from 'drizzle-orm'
import {
productInfo,
productSkus,
skuFeatures,
productMarketStats,
productTags,
productGroupMembership,
orderItems,
cartItems,
specialDeals,
couponApplicableProducts,
productCombos,
productReviews,
homeBanners,
deliverySlotInfo,
vendorSnippets,
coupons,
reservedCoupons,
} from '../db/schema'
// ============================================================
// SOURCE OF TRUTH — snapshot of docs/duplicate-products-local_8_aug.md
// ("Clear duplicates" section). Keep this in sync with the doc.
// ============================================================
const DUPLICATES_MD = `### 1. Tomato
| 17 | \`Tomato\` | 1.0 Kg | Vegetables |
| 45 | \`Tomato\` | 0.5 Kg | Vegetables |
### 2. Mutton Curry Cut (Regular)
| 98 | \`Mutton Curry Cut (Regular) 500 g\` | 0.5 Kg | Meat Store |
| 99 | \`Mutton Curry Cut (Regular) 750 g\` | 0.75 Kg | Meat Store |
### 3. Mutton Boneless
| 28 | \`Mutton Boneless\` | 0.5 Kg | Meat Store |
| 34 | \`Mutton Boneless Mini Pack\` | 0.25 Kg | Meat Store |
### 4. Chicken Breast
| 2 | \`Chicken Breast 500 g\` | 0.5 Kg | Meat Store |
| 42 | \`Chicken Breast Boneless - Mini Pack\` | 0.25 Kg | Meat Store |
### 5. Chicken (regular cut)
| 23 | \`Chicken ( regular cut )- Small pieces\` | 0.5 Kg | Meat Store |
| 40 | \`Chicken (regular cut) Large pieces\` | 0.5 Kg | Meat Store |
### 6. Coriander Leaves (Kothimeer)
| 76 | \`Coriander Leaves (Kothimeer)\` | 1.0 Pc | Vegetables |
| 101 | \`Coriander Leaves (Kothimeer) small 1\` | 1.0 Pc | Vegetables |
### 7. Pineapple
| 57 | \`Pineapple\` | 1.0 Pc | The Fruit Store |
| 96 | \`Pineapple Large\` | 1.0 Pc | The Fruit Store |
### 8. Carrot / Red Carrot
| 69 | \`Carrot\` | 0.25 Kg | Vegetables |
| 79 | \`Red Carrot\` | 0.5 Kg | Vegetables |
### 9. Mutton / Lamb Mutton
| 4 | \`Mutton\` | 1.0 Kg | Meat Store |
| 12 | \`Lamb Mutton\` | 1.0 Kg | Meat Store |
### 10. Raisins (kishmish)
| 131 | \`Raisins (kishmish)\` | 0.25 Kg | Dry Fruits store |
| 134 | \`black raisin ( kishmish)\` | 0.25 Kg | Dry Fruits store |
### 11. Fig (Anjeer) / Dried Figs
| 26 | \`Fig (Anjeer)\` | 0.25 Kg | The Fruit Store |
| 132 | \`Dried figs (Anjeer)\` | 0.25 Kg | Dry Fruits store |
### 12. Country Chicken
| 80 | \`Country Chicken (Desi / Natu Kodi) live .\` | 1.0 Kg | Meat Store |
| 128 | \`Country Chicken (Farm Raised)\` | 1.0 Kg | Meat Store |`
/** Sections (by header name) that should actually be merged */
const GROUPS_TO_MERGE = [
'Tomato',
'Mutton Curry Cut (Regular)',
'Mutton Boneless',
'Chicken Breast',
'Coriander Leaves (Kothimeer)',
'Pineapple',
]
/**
* Extra sku_features per OLD product name (keyed exactly as in the md table
* rows). Applied to the SKU created from whichever row matches that name.
*/
const EXTRA_FEATURES: Record<string, [string, string][]> = {
'Coriander Leaves (Kothimeer)': [['bunch size', 'large']],
'Coriander Leaves (Kothimeer) small 1': [['bunch size', 'small']],
}
// ============================================================
// md parsing — build groups from the doc
// ============================================================
/** Parses "### N. Section" headers + "| id | `name` | ..." rows from the md. */
export function parseDuplicateProductsMd(md: string): Map<string, string[]> {
const sections = new Map<string, string[]>()
let current: string | null = null
for (const line of md.split('\n')) {
const header = line.match(/^###\s+\d+\.\s+(.+)$/)
if (header) {
current = header[1].trim()
sections.set(current, [])
continue
}
if (!current) continue
const row = line.match(/^\|\s*\d+\s*\|\s*`(.+?)`\s*\|/)
if (row) sections.get(current)!.push(row[1])
}
return sections
}
interface MergeGroup {
/** Name of the new merged product (md section header) */
name: string
/** Product names to match (md table rows) */
productNames: string[]
/** Extra features keyed by the md product name they apply to */
extraFeatures: Record<string, [string, string][]>
}
const buildMergeGroups = (md: string): MergeGroup[] => {
const sections = parseDuplicateProductsMd(md)
return GROUPS_TO_MERGE
.map((sectionName) => ({
name: sectionName,
productNames: sections.get(sectionName) ?? [],
extraFeatures: EXTRA_FEATURES,
}))
.filter((g) => g.productNames.length >= 2)
}
// ============================================================
// Types
// ============================================================
export interface MergedGroup {
name: string
newProductId: number
newSkuIds: number[]
oldProductIds: number[]
}
export interface MergeReport {
groups: MergedGroup[]
skippedGroups: { name: string; reason: string }[]
updatedRows: number
reviewsUpdated: number
jsonRowsUpdated: Record<string, number>
finalCounts: {
productInfo: number
productSkus: number
skuFeatures: number
productMarketStats: number
}
}
// ============================================================
// Name matching
// ============================================================
/** Trim, lowercase, collapse whitespace, unify dash variants (-, , —, ). */
const normalizeName = (s: string): string =>
s
.toLowerCase()
.replace(/[\u2010\u2011\u2012\u2013\u2014\u2015\u2212-]/g, '-')
.replace(/\s+/g, ' ')
.trim()
/**
* Returns which of the group's md names `dbName` matches (longest name wins,
* so e.g. "… small 1" is matched before the plain "…" name), or null.
*/
const matchProductName = (dbName: string, groupNames: string[]): string | null => {
const norm = normalizeName(dbName)
const byLengthDesc = [...groupNames].sort((a, b) => b.length - a.length)
for (const name of byLengthDesc) {
if (norm.includes(normalizeName(name))) return name
}
return null
}
// ============================================================
// Implementation
// ============================================================
export async function mergeDuplicateProducts(mdContent?: string): Promise<MergeReport> {
const mergeGroups = buildMergeGroups(mdContent ?? DUPLICATES_MD)
// ---- resolve products by name (no hardcoded ids) ------------------------
const allProducts = await db.select().from(productInfo)
const assignedTo = new Set<number>() // product ids already claimed by a group
const resolvedGroups: { group: MergeGroup; productIds: number[]; matchedNames: Map<number, string> }[] = []
for (const group of mergeGroups) {
const productIds: number[] = []
const matchedNames = new Map<number, string>()
for (const product of allProducts) {
if (assignedTo.has(product.id) || !product.name) continue
const matched = matchProductName(product.name, group.productNames)
if (matched !== null) {
assignedTo.add(product.id)
productIds.push(product.id)
matchedNames.set(product.id, matched)
}
}
resolvedGroups.push({ group, productIds, matchedNames })
}
const skippedGroups: { name: string; reason: string }[] = []
const productMap = new Map<number, number>() // old product id -> new product id
const skuMap = new Map<number, number>() // old sku id -> new sku id
const mergedGroups: MergedGroup[] = []
// ---- sanity checks per candidate + create new products and SKUs ---------
for (const { group, productIds, matchedNames } of resolvedGroups) {
const usableIds: number[] = []
for (const pid of productIds) {
const skus = await db.query.productSkus.findMany({
where: eq(productSkus.productId, pid),
})
if (skus.length === 0) {
console.log(`mergeDuplicateProducts: product ${pid} has no SKUs — skipping it`)
continue
}
if (skus.length > 1) {
// A previously-merged product (named exactly like the group, with one
// SKU per merged row) legitimately has multiple SKUs — treat it as
// already merged and skip instead of throwing.
const product = allProducts.find((p) => p.id === pid)
if (product?.name && normalizeName(product.name) === normalizeName(group.name)) {
console.log(`mergeDuplicateProducts: product ${pid} is the previously merged "${group.name}" — skipping`)
continue
}
throw new Error(`mergeDuplicateProducts: product ${pid} has ${skus.length} SKUs, expected exactly 1`)
}
usableIds.push(pid)
}
if (usableIds.length < 2) {
const reason =
usableIds.length === 0
? 'no rows matched by name (already merged?)'
: `only ${usableIds.length} of the group's products still exist`
console.log(`mergeDuplicateProducts: skipping group "${group.name}" — ${reason}`)
skippedGroups.push({ name: group.name, reason })
continue
}
const [first] = await db.select().from(productInfo).where(eq(productInfo.id, usableIds[0]))
const [newProduct] = await db.insert(productInfo)
.values({
name: group.name,
shortDescription: first.shortDescription,
longDescription: first.longDescription,
storeId: first.storeId,
incrementStep: first.incrementStep ?? 1,
productType: first.productType ?? 'item',
createdAt: first.createdAt,
})
.returning({ id: productInfo.id })
const newProductId = newProduct.id
const newSkuIds: number[] = []
for (const oldPid of usableIds) {
productMap.set(oldPid, newProductId)
const [oldSku] = await db.query.productSkus.findMany({
where: eq(productSkus.productId, oldPid),
})
const [newSku] = await db.insert(productSkus)
.values({
productId: newProductId,
name: null,
images: oldSku.images,
isOffer: oldSku.isOffer,
isComboOnly: oldSku.isComboOnly,
isDeleted: oldSku.isDeleted,
createdAt: oldSku.createdAt,
})
.returning({ id: productSkus.id })
const newSkuId = newSku.id
newSkuIds.push(newSkuId)
skuMap.set(oldSku.id, newSkuId)
// copy features, then any group-specific extras for the matched name
const oldFeatures = await db.query.skuFeatures.findMany({
where: eq(skuFeatures.skuId, oldSku.id),
})
for (const f of oldFeatures) {
await db.insert(skuFeatures).values({
skuId: newSkuId,
featureName: f.featureName,
featureValue: f.featureValue,
})
}
const matched = matchedNames.get(oldPid)
for (const [featureName, featureValue] of (matched ? group.extraFeatures[matched] : undefined) ?? []) {
await db.insert(skuFeatures).values({ skuId: newSkuId, featureName, featureValue })
}
// copy market stats
const [ms] = await db.query.productMarketStats.findMany({
where: eq(productMarketStats.skuId, oldSku.id),
})
if (ms) {
await db.insert(productMarketStats).values({
skuId: newSkuId,
marketPrice: ms.marketPrice,
ourPrice: ms.ourPrice,
isFlashAvailable: ms.isFlashAvailable,
flashPrice: ms.flashPrice,
isOutOfStock: ms.isOutOfStock,
isSuspended: ms.isSuspended,
})
}
}
mergedGroups.push({ name: group.name, newProductId, newSkuIds, oldProductIds: usableIds })
}
// ---- re-point FK references (old sku -> new sku) ---------------------
let updatedRows = 0
const remapColumn = async (
table: any,
column: string,
map: Map<number, number>
): Promise<number> => {
let n = 0
for (const [oldId, newId] of map) {
const res = await db.update(table)
.set({ [column]: newId })
.where(eq(table[column], oldId))
n += Number(res?.meta?.changes ?? res?.changes ?? 0)
}
return n
}
// product_tags / product_group_membership hold SKU ids (post sku-split schema)
updatedRows += await remapColumn(productTags, 'productId', skuMap)
updatedRows += await remapColumn(productGroupMembership, 'productId', skuMap)
updatedRows += await remapColumn(orderItems, 'skuId', skuMap)
updatedRows += await remapColumn(cartItems, 'skuId', skuMap)
updatedRows += await remapColumn(specialDeals, 'skuId', skuMap)
updatedRows += await remapColumn(couponApplicableProducts, 'skuId', skuMap)
updatedRows += await remapColumn(productCombos, 'skuId', skuMap)
updatedRows += await remapColumn(productCombos, 'comboSkuId', skuMap)
// product_reviews references product_info ids
let reviewsUpdated = 0
for (const [oldPid, newPid] of productMap) {
const res = await db.update(productReviews)
.set({ productId: newPid })
.where(eq(productReviews.productId, oldPid))
reviewsUpdated += Number(res?.meta?.changes ?? res?.changes ?? 0)
}
// ---- re-point JSON sku_ids arrays ------------------------------------
const jsonRowsUpdated: Record<string, number> = {}
const remapJsonArray = async (
table: any,
column: string,
map: Map<number, number>
): Promise<number> => {
let n = 0
const rows = await db.select().from(table).where(isNotNull(table[column]))
for (const row of rows) {
const arr = row[column]
if (!Array.isArray(arr)) continue
let changed = false
const next = arr.map((x: number) => {
const mapped = map.get(Number(x))
if (mapped !== undefined) {
changed = true
return mapped
}
return x
})
if (changed) {
await db.update(table)
.set({ [column]: next })
.where(eq(table.id, row.id))
n++
}
}
return n
}
jsonRowsUpdated.homeBanners = await remapJsonArray(homeBanners, 'skuIds', skuMap)
jsonRowsUpdated.deliverySlotInfo = await remapJsonArray(deliverySlotInfo, 'skuIds', skuMap)
jsonRowsUpdated.vendorSnippets = await remapJsonArray(vendorSnippets, 'skuIds', skuMap)
jsonRowsUpdated.coupons = await remapJsonArray(coupons, 'skuIds', skuMap)
jsonRowsUpdated.reservedCoupons = await remapJsonArray(reservedCoupons, 'skuIds', skuMap)
// ---- delete the old rows ----------------------------------------------
const oldSkuIds = [...skuMap.keys()]
await db.delete(productMarketStats).where(inArray(productMarketStats.skuId, oldSkuIds))
await db.delete(skuFeatures).where(inArray(skuFeatures.skuId, oldSkuIds))
await db.delete(productSkus).where(inArray(productSkus.id, oldSkuIds))
await db.delete(productInfo).where(inArray(productInfo.id, [...productMap.keys()]))
// ---- verification: no leftover references ---------------------------------
const leftoverChecks: [string, number][] = []
const countLeftovers = async (table: any, column: string): Promise<number> => {
if (oldSkuIds.length === 0) return 0
const rows = await db.select({ c: sql`1` }).from(table).where(inArray(table[column], oldSkuIds))
return rows.length
}
leftoverChecks.push(['product_tags.product_id', await countLeftovers(productTags, 'productId')])
leftoverChecks.push(['product_group_membership.product_id', await countLeftovers(productGroupMembership, 'productId')])
leftoverChecks.push(['order_items.sku_id', await countLeftovers(orderItems, 'skuId')])
leftoverChecks.push(['cart_items.sku_id', await countLeftovers(cartItems, 'skuId')])
leftoverChecks.push(['special_deals.sku_id', await countLeftovers(specialDeals, 'skuId')])
leftoverChecks.push(['coupon_applicable_products.sku_id', await countLeftovers(couponApplicableProducts, 'skuId')])
leftoverChecks.push(['product_combos.sku_id', await countLeftovers(productCombos, 'skuId')])
leftoverChecks.push(['product_combos.combo_sku_id', await countLeftovers(productCombos, 'comboSkuId')])
leftoverChecks.push(['product_market_stats.sku_id', await countLeftovers(productMarketStats, 'skuId')])
leftoverChecks.push(['sku_features.sku_id', await countLeftovers(skuFeatures, 'skuId')])
const bad = leftoverChecks.filter(([, n]) => n > 0)
if (bad.length > 0) {
throw new Error(
`mergeDuplicateProducts: leftover references to old ids: ${bad.map(([t, n]) => `${t}: ${n}`).join(', ')}`
)
}
// ---- final counts ---------------------------------------------------------
const countRows = async (table: any): Promise<number> => {
const rows = await db.select({ c: sql`1` }).from(table)
return rows.length
}
return {
groups: mergedGroups,
skippedGroups,
updatedRows,
reviewsUpdated,
jsonRowsUpdated,
finalCounts: {
productInfo: await countRows(productInfo),
productSkus: await countRows(productSkus),
skuFeatures: await countRows(skuFeatures),
productMarketStats: await countRows(productMarketStats),
},
}
}

View file

@ -580,32 +580,6 @@ export async function updateProduct(id: number, input: any): Promise<AdminProduc
}
}
export async function updateSlotProducts(slotId: string, productIds: string[]): Promise<AdminUpdateSlotProductsResult> {
const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, parseInt(slotId)),
})
if (!slot) {
throw new Error(`Slot ${slotId} not found`)
}
const currentSkuIds = slot.skuIds || []
const newSkuIds = productIds.map((id: string) => parseInt(id))
await db.update(deliverySlotInfo)
.set({ skuIds: newSkuIds })
.where(eq(deliverySlotInfo.id, parseInt(slotId)))
const productsToAdd = newSkuIds.filter((id: number) => !currentSkuIds.includes(id))
const productsToRemove = currentSkuIds.filter((id: number) => !newSkuIds.includes(id))
return {
message: 'Slot products updated successfully',
added: productsToAdd.length,
removed: productsToRemove.length,
}
}
export async function getAllProductTags(): Promise<AdminProductTagWithProducts[]> {
const tags = await db.query.productTagInfo.findMany({
with: {
@ -753,29 +727,6 @@ export async function checkProductTagExistsByName(tagName: string): Promise<bool
return !!tag
}
export async function getSlotsProductIds(slotIds: number[]): Promise<Record<number, number[]>> {
if (slotIds.length === 0) {
return {}
}
const slots = await db.query.deliverySlotInfo.findMany({
where: inArray(deliverySlotInfo.id, slotIds),
})
const result: Record<number, number[]> = {}
for (const slot of slots) {
result[slot.id] = slot.skuIds || []
}
slotIds.forEach((slotId) => {
if (!result[slotId]) {
result[slotId] = []
}
})
return result
}
export async function getProductReviews(productId: number, limit: number, offset: number) {
const reviews = await db
.select({
@ -1073,43 +1024,6 @@ export async function checkProductExistsByName(name: string): Promise<boolean> {
return !!product
}
export async function checkUnitExists(unitId: number): Promise<boolean> {
const unit = await db.query.units.findFirst({
where: eq(units.id, unitId),
columns: { id: true },
})
return !!unit
}
export async function getProductImagesById(productId: number): Promise<string[] | null> {
const product = await db.query.productSkus.findFirst({
where: eq(productSkus.id, productId),
columns: { images: true },
})
if (!product) {
return null
}
return getStringArray(product.images) || []
}
export async function replaceProductTags(productId: number, tagIds: number[]): Promise<void> {
await db.delete(productTags).where(eq(productTags.productId, productId))
if (tagIds.length === 0) {
return
}
const tagAssociations = tagIds.map((tagId) => ({
productId,
tagId,
}))
await db.insert(productTags).values(tagAssociations)
}
export async function replaceTagProducts(tagId: number, productIds: number[]): Promise<void> {
await db.delete(productTags).where(eq(productTags.tagId, tagId))

View file

@ -158,14 +158,6 @@ export async function staleSlotsCleanup(): Promise<number> {
return 1
}
export async function getActiveSlots(): Promise<AdminDeliverySlot[]> {
const slots = await db.query.deliverySlotInfo.findMany({
where: eq(deliverySlotInfo.isActive, true),
})
return slots.map(mapDeliverySlot)
}
export async function getSlotsAfterDate(afterDate: Date): Promise<AdminDeliverySlot[]> {
const slots = await db.query.deliverySlotInfo.findMany({
where: and(
@ -366,20 +358,6 @@ export async function updateSlotWithRelations(input: {
return result
}
export async function deleteSlotById(id: number): Promise<AdminDeliverySlot | null> {
const [deletedSlot] = await db
.update(deliverySlotInfo)
.set({ isActive: false })
.where(eq(deliverySlotInfo.id, id))
.returning()
if (!deletedSlot) {
return null
}
return mapDeliverySlot(deletedSlot)
}
export async function getSlotDeliverySequence(slotId: number): Promise<AdminDeliverySlot | null> {
const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, slotId),

View file

@ -41,56 +41,6 @@ export async function getAllStaff(): Promise<any[]> {
return staff
}
export async function getAllUsers(
cursor?: number,
limit: number = 20,
search?: string
): Promise<{ users: any[]; hasMore: boolean }> {
let whereCondition = undefined
if (search) {
whereCondition = or(
like(users.name, `%${search}%`),
like(users.email, `%${search}%`),
like(users.mobile, `%${search}%`)
)
}
if (cursor) {
const cursorCondition = lt(users.id, cursor)
whereCondition = whereCondition ? and(whereCondition, cursorCondition) : cursorCondition
}
const allUsers = await db.query.users.findMany({
where: whereCondition,
with: {
userDetails: true,
},
orderBy: desc(users.id),
limit: limit + 1,
})
const hasMore = allUsers.length > limit
const usersToReturn = hasMore ? allUsers.slice(0, limit) : allUsers
return { users: usersToReturn, hasMore }
}
export async function getUserWithDetails(userId: number): Promise<any | null> {
const user = await db.query.users.findFirst({
where: eq(users.id, userId),
with: {
userDetails: true,
orders: {
orderBy: desc(orders.createdAt),
limit: 1,
},
},
})
return user || null
}
export async function checkStaffUserExists(name: string): Promise<boolean> {
const existingUser = await db.query.staffUsers.findFirst({
where: eq(staffUsers.name, name),

View file

@ -2,19 +2,6 @@ import { db } from '../db/db_index'
import { users, userDetails, orders, orderItems, complaints, notifCreds, unloggedUserTokens, userIncidents, orderStatus } from '../db/schema'
import { eq, sql, desc, asc, count, max, inArray } from 'drizzle-orm'
export async function createUserByMobile(mobile: string): Promise<any> {
const [newUser] = await db
.insert(users)
.values({
name: null,
email: null,
mobile,
})
.returning()
return newUser
}
export async function getUserByMobile(mobile: string): Promise<any | null> {
const [existingUser] = await db
.select()

View file

@ -174,25 +174,6 @@ export async function getVendorOrdersBySlotId(slotId: number) {
})
}
export async function getVendorOrders() {
return await db.query.orders.findMany({
with: {
user: true,
orderItems: {
with: {
sku: {
with: {
product: true,
features: true,
},
},
},
},
},
orderBy: desc(orders.createdAt),
})
}
export async function updateVendorOrderItemPackaging(
orderItemId: number,
isPackaged: boolean

View file

@ -302,7 +302,7 @@ export async function getNextDeliveryDateWithCapacity(skuId: number): Promise<Da
}
// Single source in @packages/shared (admin.ts); re-exported for module compat.
import type { ProductSummaryCore, SkuSummary } from '@packages/shared'
import type { SkuSummary } from '@packages/shared'
export type { SkuSummary } from '@packages/shared'
export async function getAllSkusSummary(): Promise<SkuSummary[]> {
@ -333,61 +333,3 @@ export async function getAllSkusSummary(): Promise<SkuSummary[]> {
})
}
export interface OffersPageProductData extends ProductSummaryCore {
}
export interface OffersPageData {
combos: OffersPageProductData[]
offers: OffersPageProductData[]
}
const mapOffersPageProduct = (sku: {
id: number
marketStats: {
ourPrice: string | null
marketPrice: string | null
isOutOfStock: boolean
} | null
images: unknown
name: string | null
product: { name: string; incrementStep: number | null } | null
features: Array<{ featureValue: string }>
}): OffersPageProductData => {
const features = sku.features || []
return {
id: sku.id,
name: composeSkuName(sku.product?.name ?? 'Unknown', features, sku.name),
price: sku.marketStats?.ourPrice ? String(sku.marketStats.ourPrice) : '0',
marketPrice: sku.marketStats?.marketPrice ? String(sku.marketStats.marketPrice) : null,
unitNotation: composeUnitNotation(features),
images: (sku.images ?? null) as string[] | null,
isOutOfStock: sku.marketStats?.isOutOfStock ?? false,
incrementStep: sku.product?.incrementStep ?? 1,
}
}
export async function getOffersAndCombos(): Promise<OffersPageData> {
const skus = await db.query.productSkus.findMany({
with: {
product: true,
features: true,
marketStats: true,
},
})
const combos: OffersPageProductData[] = []
const offers: OffersPageProductData[] = []
for (const sku of skus) {
if (sku.marketStats?.isSuspended) continue
if (sku.isDeleted) continue
if (sku.product?.productType === 'combo') {
combos.push(mapOffersPageProduct(sku))
}
if (sku.isOffer) {
offers.push(mapOffersPageProduct(sku))
}
}
return { combos, offers }
}