enh
This commit is contained in:
parent
ee60799a8b
commit
8aaaa12ca0
50 changed files with 185 additions and 2149 deletions
|
|
@ -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
162
DEAD_CODE_REPORT.md
Normal 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.)
|
||||
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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] }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:*"
|
||||
|
|
|
|||
|
|
@ -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[]>> {
|
||||
|
|
|
|||
|
|
@ -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:*')
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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> => {
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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" };
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
};
|
||||
}),
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -69,6 +69,3 @@ export const protectedProcedure = t.procedure.use(errorLoggerMiddleware).use(
|
|||
return next();
|
||||
})
|
||||
);
|
||||
|
||||
export const createCallerFactory = t.createCallerFactory;
|
||||
export const createTRPCRouter = t.router;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ export default {
|
|||
if (!app) {
|
||||
app = createApp()
|
||||
}
|
||||
// await mergeDuplicateProducts()
|
||||
return app.fetch(request, env, ctx)
|
||||
},
|
||||
async queue(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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) } })
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue