-
Notifications
You must be signed in to change notification settings - Fork 37
feat:Implement Stock Movement Ledger and UI refinements #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| 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"; | ||
|
|
||
| /** | ||
| * 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reject malformed filter params with An arbitrary 🤖 Prompt for AI Agents |
||
|
|
||
| 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 } | ||
| ); | ||
| } | ||
| } | ||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reject invalid numeric payloads before calling the service. On Line 32 and Line 35, 💡 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const updatedStock = await StockService.updateStock(id, dataToUpdate); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+30
to
+39
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pass On Line 38, 💡 Suggested fix- const dataToUpdate: any = {};
+ const dataToUpdate: {
+ userId: string;
+ quantity?: number;
+ reservedQuantity?: number;
+ } = { userId: user.id };🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not expose raw exception messages in API responses. Returning 🛡️ 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use the authenticated user in both handlers.
Sibling inventory routes already call
getSessionFromRequest(req), but this endpoint is public onGETand writes movements under a hardcoded placeholder onPOST. That exposes movement history and breaks ownership/auditability for writes. Resolve the session up front in both handlers, return401when absent, and passsession.idinto 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