Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/api/portal/client/browse-meta/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ export async function GET(request: NextRequest) {
prisma.supplier.count({ where: { status: false } }),
prisma.category.count({ where: { status: true } }),
prisma.category.count({ where: { status: false } }),
prisma.warehouse.count({ where: { status: true } }),
prisma.warehouse.count({ where: { status: false } }),
prisma.warehouse.count({ where: { isActive: true } }),
prisma.warehouse.count({ where: { isActive: false } }),
]);

const meta: ClientBrowseMeta = {
Expand Down
224 changes: 0 additions & 224 deletions app/api/stock-allocations/route.ts

This file was deleted.

59 changes: 59 additions & 0 deletions app/api/stock-movements/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { NextRequest, NextResponse } from "next/server";
import { getSession } from "next-auth/react"; // Assuming next-auth is used
import { StockMovementService } from "@/modules/stock-movement/api/stock-movement.service";
import { StockMovementType } from "@prisma/client";
Comment on lines +2 to +4
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Use the authenticated user in both handlers.

Sibling inventory routes already call getSessionFromRequest(req), but this endpoint is public on GET and writes movements under a hardcoded placeholder on POST. That exposes movement history and breaks ownership/auditability for writes. Resolve the session up front in both handlers, return 401 when absent, and pass session.id into the service.

Suggested change
 import { NextRequest, NextResponse } from "next/server";
-import { getSession } from "next-auth/react"; // Assuming next-auth is used
+import { getSessionFromRequest } from "@/utils/auth";
 import { StockMovementService } from "@/modules/stock-movement/api/stock-movement.service";
 import { StockMovementType } from "@prisma/client";
@@
 export async function GET(req: NextRequest) {
   try {
+    const session = await getSessionFromRequest(req);
+    if (!session) {
+      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+    }
+
     const { searchParams } = new URL(req.url);
@@
     const movements = await StockMovementService.getAllMovements({
+      userId: session.id,
       productId,
       warehouseId,
       movementType,
@@
 export async function POST(req: NextRequest) {
   try {
-    // In a real app, you'd get the user ID from the session
-    // const session = await getSession({ req });
-    const userId = "manual_admin"; // Placeholder
+    const session = await getSessionFromRequest(req);
+    if (!session) {
+      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+    }
 
     const body = await req.json();
     const movement = await StockMovementService.createMovement({
       ...body,
-      userId,
+      userId: session.id,
     });

Also applies to: 9-26, 39-49

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/stock-movements/route.ts` around lines 2 - 4, Both GET and POST
handlers in route.ts must authenticate requests: resolve the session up front
(use the same helper as siblings, e.g. getSessionFromRequest(req) or getSession)
and if no session return a 401 response, then pass the authenticated user id
into StockMovementService methods instead of using a hardcoded placeholder;
update both the GET handler (where movements are currently publicly returned)
and the POST handler (where new movements are being created under a placeholder
owner) to call/get session before any work and forward session.id into
StockMovementService methods (refer to StockMovementService and
StockMovementType in this file).


/**
* Handle GET requests for stock movements.
*/
export async function GET(req: NextRequest) {
try {
const { searchParams } = new URL(req.url);
const productId = searchParams.get("productId") || undefined;
const warehouseId = searchParams.get("warehouseId") || undefined;
const movementType = searchParams.get("movementType") as StockMovementType | undefined;
const startDate = searchParams.get("startDate") ? new Date(searchParams.get("startDate")!) : undefined;
const endDate = searchParams.get("endDate") ? new Date(searchParams.get("endDate")!) : undefined;
Comment on lines +11 to +16
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reject malformed filter params with 400.

An arbitrary movementType string or an invalid date currently falls through to the service and becomes a server error. Validate the enum/date inputs here before calling Prisma so bad requests stay bad requests instead of turning into 500s.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/stock-movements/route.ts` around lines 11 - 16, Validate and reject
malformed filter params before calling the service: check that movementType
(from movementType variable) is either undefined or one of the StockMovementType
enum values and that startDate/endDate strings parse to valid dates (new
Date(...) yields a valid date and not NaN); if any validation fails return a 400
response (e.g., throw new Response or NextResponse with status 400 and a clear
error message). Also ensure you stop execution when rejecting (don’t call the
Prisma/service methods) and optionally validate startDate <= endDate if both
provided. Use the existing variables movementType, startDate, endDate and the
StockMovementType enum to implement these checks.


const movements = await StockMovementService.getAllMovements({
productId,
warehouseId,
movementType,
startDate,
endDate,
});

return NextResponse.json(movements);
} catch (error: any) {
console.error("Error fetching stock movements:", error);
return NextResponse.json(
{ error: error.message || "Internal Server Error" },
{ status: 500 }
);
}
}

/**
* Handle POST requests for manual stock movements (e.g., Adjustments).
*/
export async function POST(req: NextRequest) {
try {
// In a real app, you'd get the user ID from the session
// const session = await getSession({ req });
const userId = "manual_admin"; // Placeholder

const body = await req.json();
const movement = await StockMovementService.createMovement({
...body,
userId,
});

return NextResponse.json(movement, { status: 201 });
} catch (error: any) {
console.error("Error creating stock movement:", error);
return NextResponse.json(
{ error: error.message || "Internal Server Error" },
{ status: 500 }
);
}
}
54 changes: 54 additions & 0 deletions app/api/stocks/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { NextRequest, NextResponse } from "next/server";
import { getSessionFromRequest } from "@/utils/auth";
import { StockService } from "@/modules/stock/api/stock.service";

/**
* PUT: Update stock quantity or reserved quantity by ID
*/
export async function PUT(
req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
let id: string | null = null;
try {
const user = await getSessionFromRequest(req);
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

id = (await params).id;
if (!id) {
return NextResponse.json(
{ success: false, error: "Stock ID is required." },
{ status: 400 }
);
}

const body = await req.json();
const { quantity, reservedQuantity } = body;

const dataToUpdate: any = {};
if (quantity !== undefined) {
dataToUpdate.quantity = parseInt(String(quantity), 10);
}
if (reservedQuantity !== undefined) {
dataToUpdate.reservedQuantity = parseInt(String(reservedQuantity), 10);
}
Comment on lines +31 to +36
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reject invalid numeric payloads before calling the service.

On Line 32 and Line 35, parseInt can yield NaN (e.g., "abc"), which currently falls into a 500 path instead of a 400 validation response.

💡 Suggested fix
     if (quantity !== undefined) {
-      dataToUpdate.quantity = parseInt(String(quantity), 10);
+      const parsedQuantity = Number(quantity);
+      if (!Number.isInteger(parsedQuantity)) {
+        return NextResponse.json(
+          { success: false, error: "quantity must be an integer." },
+          { status: 400 }
+        );
+      }
+      dataToUpdate.quantity = parsedQuantity;
     }
     if (reservedQuantity !== undefined) {
-      dataToUpdate.reservedQuantity = parseInt(String(reservedQuantity), 10);
+      const parsedReserved = Number(reservedQuantity);
+      if (!Number.isInteger(parsedReserved)) {
+        return NextResponse.json(
+          { success: false, error: "reservedQuantity must be an integer." },
+          { status: 400 }
+        );
+      }
+      dataToUpdate.reservedQuantity = parsedReserved;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (quantity !== undefined) {
dataToUpdate.quantity = parseInt(String(quantity), 10);
}
if (reservedQuantity !== undefined) {
dataToUpdate.reservedQuantity = parseInt(String(reservedQuantity), 10);
}
if (quantity !== undefined) {
const parsedQuantity = Number(quantity);
if (!Number.isInteger(parsedQuantity)) {
return NextResponse.json(
{ success: false, error: "quantity must be an integer." },
{ status: 400 }
);
}
dataToUpdate.quantity = parsedQuantity;
}
if (reservedQuantity !== undefined) {
const parsedReserved = Number(reservedQuantity);
if (!Number.isInteger(parsedReserved)) {
return NextResponse.json(
{ success: false, error: "reservedQuantity must be an integer." },
{ status: 400 }
);
}
dataToUpdate.reservedQuantity = parsedReserved;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/stocks/`[id]/route.ts around lines 31 - 36, When parsing incoming
numeric fields `quantity` and `reservedQuantity` in the route handler (where
`dataToUpdate.quantity` and `dataToUpdate.reservedQuantity` are set using
`parseInt`), validate the parsed values and reject invalid payloads with a 400
error before calling the service: parse each value, check
Number.isInteger(result) (or !Number.isNaN(result)), and if invalid return a
400/Bad Request with a clear message; only assign the parsed integer to
`dataToUpdate` when the value is valid to avoid letting `NaN` propagate to the
service layer.


const updatedStock = await StockService.updateStock(id, dataToUpdate);

Comment on lines +30 to +39
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Pass user.id into stock updates to prevent ledger write failures.

On Line 38, StockService.updateStock is called without userId. When quantity changes, movement logging uses data.userId; this becomes undefined, causing update flows to fail after the stock row update has already happened.

💡 Suggested fix
-    const dataToUpdate: any = {};
+    const dataToUpdate: {
+      userId: string;
+      quantity?: number;
+      reservedQuantity?: number;
+    } = { userId: user.id };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/stocks/`[id]/route.ts around lines 30 - 39, The update call omits the
user id, causing downstream movement logging to see data.userId as undefined;
add the authenticated user's id into the update payload (e.g., set
dataToUpdate.userId = user.id) before calling StockService.updateStock(id,
dataToUpdate) so movement/ledger flows have the required userId; if your
updateStock signature expects a separate userId parameter, pass user.id as that
argument instead and ensure references to data.userId in movement logging will
receive a valid value.

return NextResponse.json({ success: true, data: updatedStock });
} catch (error: any) {
console.error("Error updating stock:", error);
if (error.message?.includes("not found")) {
return NextResponse.json(
{ success: false, error: "Stock record not found" },
{ status: 404 }
);
}
return NextResponse.json(
{ success: false, error: error.message || "Failed to update stock" },
{ status: 500 }
);
}
}
36 changes: 36 additions & 0 deletions app/api/stocks/product/[productId]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { NextRequest, NextResponse } from "next/server";
import { getSessionFromRequest } from "@/utils/auth";
import { StockService } from "@/modules/stock/api/stock.service";

/**
* GET: Retrieve stock records for a specific product across all warehouses
*/
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ productId: string }> }
) {
let productId: string | null = null;
try {
const user = await getSessionFromRequest(req);
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

productId = (await params).productId;
if (!productId) {
return NextResponse.json(
{ success: false, error: "Product ID is required." },
{ status: 400 }
);
}

const stocks = await StockService.getStockByProduct(productId);
return NextResponse.json({ success: true, data: stocks });
} catch (error: any) {
console.error(`Error fetching stock for product ${productId || 'unknown'}:`, error);
return NextResponse.json(
{ success: false, error: error.message || "Failed to fetch stock for product" },
{ status: 500 }
Comment on lines +29 to +33
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Do not expose raw exception messages in API responses.

Returning error.message can leak internal details. Log full errors server-side and return a generic client-safe message.

🛡️ Suggested error-handling hardening
-  } catch (error: any) {
-    console.error(`Error fetching stock for product ${productId || 'unknown'}:`, error);
+  } catch (error: unknown) {
+    console.error(`Error fetching stock for product ${productId || "unknown"}:`, error);
     return NextResponse.json(
-      { success: false, error: error.message || "Failed to fetch stock for product" },
+      { success: false, error: "Failed to fetch stock for product" },
       { status: 500 }
     );
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (error: any) {
console.error(`Error fetching stock for product ${productId || 'unknown'}:`, error);
return NextResponse.json(
{ success: false, error: error.message || "Failed to fetch stock for product" },
{ status: 500 }
} catch (error: unknown) {
console.error(`Error fetching stock for product ${productId || "unknown"}:`, error);
return NextResponse.json(
{ success: false, error: "Failed to fetch stock for product" },
{ status: 500 }
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/stocks/product/`[productId]/route.ts around lines 29 - 33, The catch
block currently returns error.message to the client which can leak internals;
instead keep logging the full error server-side (e.g., via console.error or
processLogger) including productId, but change the NextResponse.json payload to
a generic client-safe message (e.g., "Failed to fetch stock for product" or
"Internal server error") and a 500 status; update the catch in route.ts (the
block referencing productId and NextResponse.json) to remove error.message from
the response body while retaining detailed server-side logging.

);
}
}
Loading