fix: add atomic Assignment transfer (close old + create new in one transaction)
The only way to move an Assignment to a new employee was two separate calls (PATCH return + POST create), which is not atomic and left a race window where a third party could grab the now-in_stock AssetItem in between, plus no rollback if the second call failed. Adds transferAssignment() which closes the old Assignment and creates the new one inside a single prisma.$transaction(), per docs/asset-license-management.md's "Assignment (chốt 2026-08-02)" spec. Does not touch AssetItem.status or License.seatsUsed — the resource never becomes free mid-transfer. Wired into PATCH /api/assignments/[id]: an employeeId key in the body dispatches to transfer, a returnedAt key to return, both together is rejected as mutually exclusive.
This commit is contained in:
@@ -1,7 +1,12 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { auth } from "@/auth";
|
||||
import { returnAssignment, validateReturnAssignmentInput } from "@/lib/assignments";
|
||||
import {
|
||||
returnAssignment,
|
||||
transferAssignment,
|
||||
validateReturnAssignmentInput,
|
||||
validateTransferAssignmentInput,
|
||||
} from "@/lib/assignments";
|
||||
|
||||
// First dynamic-segment ([id]) route in the codebase — see
|
||||
// docs/asset-license-management.md's "Assignment (chốt 2026-08-02)" section,
|
||||
@@ -39,7 +44,34 @@ export async function PATCH(
|
||||
return NextResponse.json({ error: "Request body must be a JSON object" }, { status: 400 });
|
||||
}
|
||||
|
||||
const { returnedAt } = body as Record<string, unknown>;
|
||||
const { returnedAt, employeeId } = body as Record<string, unknown>;
|
||||
|
||||
// Dispatch on body shape — a body carrying employeeId is a transfer
|
||||
// request, everything else is the existing return behavior. The two are
|
||||
// mutually exclusive: don't guess intent if a caller sends both (see
|
||||
// docs/asset-license-management.md's "Assignment (chốt 2026-08-02)"
|
||||
// section, bullet right after the transfer rule, for why this stays a
|
||||
// single PATCH route rather than an action-based sub-route).
|
||||
if (employeeId !== undefined && returnedAt !== undefined) {
|
||||
return NextResponse.json(
|
||||
{ error: "Provide either employeeId (transfer) or returnedAt (return), not both" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (employeeId !== undefined) {
|
||||
const transferValidation = validateTransferAssignmentInput({ employeeId });
|
||||
if (!transferValidation.ok) {
|
||||
return NextResponse.json({ error: transferValidation.error }, { status: 400 });
|
||||
}
|
||||
|
||||
const transferred = await transferAssignment(params.id, transferValidation.value);
|
||||
if (!transferred.ok) {
|
||||
return NextResponse.json({ error: transferred.error }, { status: transferred.status });
|
||||
}
|
||||
return NextResponse.json(transferred.value, { status: 200 });
|
||||
}
|
||||
|
||||
const validation = validateReturnAssignmentInput({ returnedAt });
|
||||
|
||||
if (!validation.ok) {
|
||||
|
||||
Reference in New Issue
Block a user