'assigned' implied a specific person always holds the item, but
createAssignment() only requires status=in_stock to hand something out --
the status value itself never guaranteed a matching Assignment row exists.
~158 of the 613 real-imported assets carry this status with no Assignment
at all (shared/unowned equipment like meeting-room gear), so "assigned"
was actively misleading for them. 'in_use' doesn't carry that implication.
DB: `ALTER TYPE "AssetItemStatus" RENAME VALUE 'assigned' TO 'in_use'`
(hand-written migration -- runs cleanly, no data migration needed, every
existing row's status just reads under the new name). Renamed across:
schema.prisma enum, src/lib/assets.ts (type union + IMPORT_STATUS_MAP,
which keeps an `assigned` key mapped to `in_use` for backward-compat with
pre-2026-08-25 exports/CSVs), src/lib/assignments.ts (the literal status
value createAssignment() sets), src/app/assets/page.tsx + api/assets/
route.ts (filter dropdown + validStatuses), src/app/maintenance-records/
page.tsx (its own copy of the status list for the "close" targetStatus
picker). maintenance-records.ts itself needed no logic change -- its
closeableTargetStatusValues was already just ["in_stock", "retired"],
"assigned" was never a valid close target, only mentioned in comments
(updated for accuracy).
Also fixes a real regression found while verifying this: tests/
assets.test.ts (a pre-existing, properly-reviewed test file, not written
this session) broke 2 commits ago when GET /api/assets's signature and
response shape changed for pagination -- it still called GET() with no
Request and expected a bare array. tsc didn't catch this until now
because this container has no dev bind mount (Dockerfile is COPY-based,
`docker exec` only reflects the last build) -- see the
ubi-dan-itam-docker-no-bind-mount memory note. Added a getRequest()
helper and updated assertions to the {items, total} shape.
Verified by actually rebuilding and running the full suite fresh (not
just tsc/lint smoke checks): 369/369 tests pass, including
assignments.test.ts and maintenance-records.test.ts, which this rename
touches directly. tsc/lint clean against the rebuilt image. Migration
applied to the real DB, data verified intact (542 in_use + 71 in_stock =
613, matches pre-rename counts exactly).
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
-- Hand-written (same deviation as other 2026-08-2x migrations -- see CLAUDE.md).
|
||||
ALTER TYPE "AssetItemStatus" RENAME VALUE 'assigned' TO 'in_use';
|
||||
@@ -90,10 +90,15 @@ model Employee {
|
||||
// 2026-08-25 (ALTER TYPE ... ADD VALUE, not a table -- see the
|
||||
// 2026-08-24/25 discussion in CLAUDE.md on why this stays an enum: the
|
||||
// values are read directly by business logic, e.g. createAssignment()'s
|
||||
// `status !== "in_stock"` gate, not just display labels).
|
||||
// `status !== "in_stock"` gate, not just display labels). `assigned`
|
||||
// renamed to `in_use` same day (ALTER TYPE ... RENAME VALUE) -- "assigned"
|
||||
// implied a specific person always holds it, but createAssignment() only
|
||||
// requires in_stock -> the enum value itself, and ~158 real-imported items
|
||||
// have this status with no Assignment row at all (shared/unowned
|
||||
// equipment) -- "in_use" doesn't carry that false implication.
|
||||
enum AssetItemStatus {
|
||||
in_stock
|
||||
assigned
|
||||
in_use
|
||||
repair
|
||||
retired
|
||||
lost
|
||||
@@ -157,7 +162,7 @@ model AssetItem {
|
||||
// Nullable: real-world coverage is thin (the ITAM export's own location
|
||||
// data was nearly useless -- "Physical location" was the literal string
|
||||
// "DAN" for all 613 rows, no per-item detail; only "Stockroom" carried
|
||||
// any signal, and only for the ~71 in_stock items -- an assigned item is
|
||||
// any signal, and only for the ~71 in_stock items -- an in_use item is
|
||||
// with a person, not sitting in a stockroom bin, so it has no area here
|
||||
// by design, not by data gap).
|
||||
areaId String? @map("area_id") @db.Uuid
|
||||
|
||||
@@ -86,7 +86,7 @@ export async function GET(request: Request): Promise<NextResponse> {
|
||||
const areaId = params.get("areaId") ?? undefined;
|
||||
const search = params.get("search") ?? undefined;
|
||||
|
||||
const validStatuses = ["in_stock", "assigned", "repair", "retired", "lost"];
|
||||
const validStatuses = ["in_stock", "in_use", "repair", "retired", "lost"];
|
||||
const result = await listAssetItems({
|
||||
page,
|
||||
pageSize,
|
||||
|
||||
@@ -30,7 +30,7 @@ interface AssetItemRecord {
|
||||
model_id: string;
|
||||
purchase_date: string | null;
|
||||
warranty_end_date: string | null;
|
||||
status: "in_stock" | "assigned" | "repair" | "retired" | "lost";
|
||||
status: "in_stock" | "in_use" | "repair" | "retired" | "lost";
|
||||
cost: number | null;
|
||||
cost_currency: string | null;
|
||||
area_id: string | null;
|
||||
@@ -467,7 +467,7 @@ export default function AssetsPage() {
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All</SelectItem>
|
||||
<SelectItem value="in_stock">in_stock</SelectItem>
|
||||
<SelectItem value="assigned">assigned</SelectItem>
|
||||
<SelectItem value="in_use">in_use</SelectItem>
|
||||
<SelectItem value="repair">repair</SelectItem>
|
||||
<SelectItem value="retired">retired</SelectItem>
|
||||
<SelectItem value="lost">lost</SelectItem>
|
||||
@@ -623,7 +623,7 @@ export default function AssetsPage() {
|
||||
), <code>purchase date</code> (không bắt buộc -- cũng nhận <code>order received</code>).
|
||||
Tuỳ chọn: <code>asset tag</code>, <code>warranty end date</code> (bỏ trống thì tự tính
|
||||
từ warranty của model), <code>status</code>/<code>state</code> (nhận{" "}
|
||||
<code>In use</code> → assigned, <code>In stock</code> → in_stock, bỏ trống thì mặc định
|
||||
<code>In use</code> → in_use, <code>In stock</code> → in_stock, bỏ trống thì mặc định
|
||||
in_stock), <code>cost</code>, <code>cost currency</code>,{" "}
|
||||
<code>po number</code> (text tự do, khác <code>purchase order id</code> là UUID trong
|
||||
hệ thống), <code>area</code> (tên khu vực -- tự tạo mới nếu chưa có, cũng nhận{" "}
|
||||
|
||||
@@ -58,7 +58,7 @@ interface VendorRecord {
|
||||
// assetItemStatusValues constant (not re-derived from an API call, this is
|
||||
// a closed set already known client-side, same precedent as licenses
|
||||
// page's licenseType/perpetual|subscription <Select>).
|
||||
const ASSET_ITEM_STATUS_VALUES = ["in_stock", "assigned", "repair", "retired"] as const;
|
||||
const ASSET_ITEM_STATUS_VALUES = ["in_stock", "in_use", "repair", "retired"] as const;
|
||||
|
||||
// Radix <Select.Item> can't take an empty-string value, so "no vendor" is
|
||||
// represented by this sentinel in the form state and translated to "omit
|
||||
|
||||
+6
-5
@@ -4,7 +4,7 @@ import { prisma } from "./prisma";
|
||||
import { splitCsvLine, splitCsvLines, normalizeHeader, csvField } from "./csv";
|
||||
import { createOrGetArea } from "./areas";
|
||||
|
||||
export type AssetItemStatus = "in_stock" | "assigned" | "repair" | "retired" | "lost";
|
||||
export type AssetItemStatus = "in_stock" | "in_use" | "repair" | "retired" | "lost";
|
||||
|
||||
export interface AssetItemRecord {
|
||||
id: string;
|
||||
@@ -526,14 +526,15 @@ export interface ImportAssetItemsResult {
|
||||
// Maps free-text status values from a real export (e.g. ServiceNow's
|
||||
// "State" column: "In use"/"In stock") onto this app's AssetItemStatus
|
||||
// enum -- plus this app's own enum values, in case a re-exported CSV
|
||||
// round-trips one back in. "In use" -> "assigned" is a simplification: it
|
||||
// round-trips one back in. "In use" -> "in_use" is a simplification: it
|
||||
// doesn't require (or create) a matching Assignment row, just reflects
|
||||
// that the item is deployed rather than sitting in stock -- see
|
||||
// docs/asset-license-management.md's 2026-08-24 section for why linking
|
||||
// actual Employee assignments is explicitly out of scope for this import.
|
||||
const IMPORT_STATUS_MAP: Record<string, AssetItemStatus> = {
|
||||
"in use": "assigned",
|
||||
assigned: "assigned",
|
||||
"in use": "in_use",
|
||||
in_use: "in_use",
|
||||
assigned: "in_use", // backward-compat: pre-2026-08-25 exports/CSVs said "assigned"
|
||||
"in stock": "in_stock",
|
||||
in_stock: "in_stock",
|
||||
repair: "repair",
|
||||
@@ -742,7 +743,7 @@ export async function importAssetItems(rows: ImportAssetItemRow[]): Promise<Impo
|
||||
* parseAssetItemsCsv() reads -- so "export, edit in Excel, re-import" is a
|
||||
* real round trip, not just a one-way dump. `status` round-trips too as of
|
||||
* 2026-08-24 (see IMPORT_STATUS_MAP above) -- exported as this app's own
|
||||
* enum text ("in_stock"/"assigned"/...), which IMPORT_STATUS_MAP also
|
||||
* enum text ("in_stock"/"in_use"/...), which IMPORT_STATUS_MAP also
|
||||
* recognizes on the way back in.
|
||||
*/
|
||||
export async function exportAssetItemsCsv(): Promise<string> {
|
||||
|
||||
@@ -228,7 +228,7 @@ export type CreateAssignmentResult =
|
||||
* see the doc's "chốt 2026-08-02 (2)" note), rejects if there's already an
|
||||
* ACTIVE Assignment (returnedAt IS NULL) for it (explicit
|
||||
* belt-and-suspenders invariant per the doc, on top of the status check),
|
||||
* then atomically flips AssetItem.status to "assigned".
|
||||
* then atomically flips AssetItem.status to "in_use".
|
||||
* - License branch: rejects if the License doesn't exist, rejects if
|
||||
* seatsUsed >= seatsTotal (no overbooking), then atomically increments
|
||||
* seatsUsed by 1.
|
||||
@@ -238,7 +238,7 @@ export type CreateAssignmentResult =
|
||||
* `updateMany` with a WHERE guard" pattern instead of a plain read-then-write
|
||||
* (`findUnique` then `update`). The WHERE-guarded `updateMany` — e.g.
|
||||
* `updateMany({ where: { id, status: "in_stock" }, data: { status:
|
||||
* "assigned" } })` — is the actual race-closing mechanism: Postgres
|
||||
* "in_use" } })` — is the actual race-closing mechanism: Postgres
|
||||
* evaluates and applies that as a single atomic UPDATE statement, taking a
|
||||
* row lock for its duration. Two concurrent transactions racing on the same
|
||||
* AssetItem/License row serialize on that lock — the first to commit wins,
|
||||
@@ -262,7 +262,7 @@ export type CreateAssignmentResult =
|
||||
* is intentionally layered BEFORE the atomic status update, as defense in
|
||||
* depth against AssetItem.status/Assignment-row drift (e.g. manual DB
|
||||
* intervention) — under normal operation this codebase always keeps
|
||||
* `status === "assigned"` in exact sync with "has an active Assignment", so
|
||||
* `status === "in_use"` in exact sync with "has an active Assignment", so
|
||||
* the atomic status-guarded `updateMany` above is what actually closes the
|
||||
* concurrency race for the AssetItem branch; this check does not need its
|
||||
* own atomic guard on top because the transaction's row lock (from the
|
||||
@@ -298,7 +298,7 @@ export async function createAssignment(
|
||||
// Atomic race-closing gate — see this function's doc comment.
|
||||
const statusUpdate = await tx.assetItem.updateMany({
|
||||
where: { id: input.assetItemId, status: "in_stock" },
|
||||
data: { status: "assigned" },
|
||||
data: { status: "in_use" },
|
||||
});
|
||||
if (statusUpdate.count === 0) {
|
||||
throw new AssignmentBusinessError(
|
||||
@@ -463,7 +463,7 @@ export type TransferAssignmentResult =
|
||||
* transfer (no race window for a third party to grab either one mid-transfer
|
||||
* — that's the whole point of doing this in one transaction instead of a
|
||||
* separate PATCH-return followed by a POST-create, which is exactly the gap
|
||||
* this function closes). AssetItem.status stays "assigned" throughout;
|
||||
* this function closes). AssetItem.status stays "in_use" throughout;
|
||||
* seatsUsed is net zero (same seat, new holder).
|
||||
*
|
||||
* Race-condition note: mirrors returnAssignment()'s exact race-closing
|
||||
|
||||
@@ -111,7 +111,7 @@ export function validateCreateMaintenanceRecordInput(input: {
|
||||
// Closed set of the 2 AssetItemStatus values a maintenance-record close is
|
||||
// allowed to land on — deliberately NOT the full 4-value AssetItemStatus
|
||||
// enum (see closeMaintenanceRecordSchema's targetStatus comment below for
|
||||
// why "assigned"/"repair" are excluded).
|
||||
// why "in_use"/"repair" are excluded).
|
||||
const closeableTargetStatusValues = ["in_stock", "retired"] as const;
|
||||
|
||||
const closeMaintenanceRecordSchema = z.object({
|
||||
@@ -130,8 +130,8 @@ const closeMaintenanceRecordSchema = z.object({
|
||||
// Restricted to "in_stock" | "retired" only (NOT the full 4-value
|
||||
// AssetItemStatus enum) — those are the only two outcomes a repair can
|
||||
// actually resolve to (fixed → back to stock, unfixable → write off).
|
||||
// "assigned" is deliberately rejected: setting AssetItem.status to
|
||||
// "assigned" here would NOT create a corresponding Assignment row, so
|
||||
// "in_use" is deliberately rejected: setting AssetItem.status to
|
||||
// "in_use" here would NOT create a corresponding Assignment row, so
|
||||
// Assignment (the source of truth for "who currently holds what") and
|
||||
// AssetItem.status would go out of sync with no way back — createAssignment()
|
||||
// only accepts a "in_stock" AssetItem, and there is no standalone
|
||||
@@ -252,7 +252,7 @@ export type CreateMaintenanceRecordResult =
|
||||
* (`where: { id: assetItemId, status: "in_stock" }`) rather than a
|
||||
* plain read-then-write. Pinning the exact required prior value (not
|
||||
* excluding "retired") matters: an excluding guard would still match an
|
||||
* item another concurrent transaction just flipped to "assigned" or
|
||||
* item another concurrent transaction just flipped to "in_use" or
|
||||
* "repair", letting two concurrent opens (or an open racing an Assignment
|
||||
* create) both win. That `updateMany`'s `count` is the actual
|
||||
* race-closing gate: two concurrent opens racing on the same AssetItem
|
||||
@@ -304,7 +304,7 @@ export async function createMaintenanceRecord(
|
||||
// exact prior value ("in_stock") rather than excluding a value
|
||||
// ("not retired"), mirroring createAssignment()'s own status guard
|
||||
// (src/lib/assignments.ts) — the excluding form would still match an
|
||||
// AssetItem another concurrent transaction just flipped to "assigned"
|
||||
// AssetItem another concurrent transaction just flipped to "in_use"
|
||||
// or "repair" (neither equals "retired"), letting two opens (or an
|
||||
// open racing an Assignment create) both win.
|
||||
const statusUpdate = await tx.assetItem.updateMany({
|
||||
|
||||
+18
-8
@@ -24,6 +24,13 @@ function postRequest(body: unknown): Request {
|
||||
});
|
||||
}
|
||||
|
||||
// GET /api/assets takes a Request now (2026-08-25, see route.ts) so it can
|
||||
// read pagination/filter query params -- query is an optional raw query
|
||||
// string (e.g. "status=in_use&page=2"), empty by default.
|
||||
function getRequest(query = ""): Request {
|
||||
return new Request(`http://localhost/api/assets${query ? `?${query}` : ""}`);
|
||||
}
|
||||
|
||||
// AssetItem.purchaseOrderId is a real FK to PurchaseOrder (see
|
||||
// schema.prisma / src/lib/assets.ts), and PurchaseOrder.vendorId is a real
|
||||
// FK to Vendor — tests that need a valid purchaseOrderId create real rows
|
||||
@@ -331,23 +338,26 @@ describe("GET /api/assets", () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||
await POST(postRequest({ serialNumber: "SN-011", modelId, purchaseDate: "2026-02-01" }));
|
||||
|
||||
const res = await GET();
|
||||
const res = await GET(getRequest());
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
// {items, total} now (2026-08-25, see listAssetItems()'s doc comment
|
||||
// in src/lib/assets.ts) -- was a bare array before pagination.
|
||||
const body = await res.json();
|
||||
expect(Array.isArray(body)).toBe(true);
|
||||
expect(body).toHaveLength(2);
|
||||
expect(Array.isArray(body.items)).toBe(true);
|
||||
expect(body.items).toHaveLength(2);
|
||||
expect(body.total).toBe(2);
|
||||
|
||||
// newest first
|
||||
expect(body[0]).toMatchObject({ serial_number: "SN-011", model_id: modelId });
|
||||
expect(body[1]).toMatchObject({ serial_number: "SN-010", model_id: modelId });
|
||||
expect(body.items[0]).toMatchObject({ serial_number: "SN-011", model_id: modelId });
|
||||
expect(body.items[1]).toMatchObject({ serial_number: "SN-010", model_id: modelId });
|
||||
});
|
||||
|
||||
it("returns an empty list when there are no asset items", async () => {
|
||||
const res = await GET();
|
||||
const res = await GET(getRequest());
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(body).toEqual([]);
|
||||
expect(body).toEqual({ items: [], total: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -375,7 +385,7 @@ describe("auth gating on /api/assets", () => {
|
||||
it("GET /api/assets returns 401 without a session", async () => {
|
||||
mockedAuth.mockResolvedValue(null);
|
||||
|
||||
const res = await GET();
|
||||
const res = await GET(getRequest());
|
||||
|
||||
expect(res.status).toBe(401);
|
||||
const body = await res.json();
|
||||
|
||||
+11
-11
@@ -72,7 +72,7 @@ async function createTestModelId(): Promise<string> {
|
||||
}
|
||||
|
||||
let serialCounter = 0;
|
||||
function createTestAssetItem(overrides: { modelId: string; status?: "in_stock" | "assigned" | "repair" | "retired" }) {
|
||||
function createTestAssetItem(overrides: { modelId: string; status?: "in_stock" | "in_use" | "repair" | "retired" }) {
|
||||
serialCounter += 1;
|
||||
return prisma.assetItem.create({
|
||||
data: {
|
||||
@@ -223,7 +223,7 @@ describe("validateReturnAssignmentInput", () => {
|
||||
});
|
||||
|
||||
describe("createAssignment", () => {
|
||||
it("assigns an in_stock AssetItem, flips its status to assigned", async () => {
|
||||
it("assigns an in_stock AssetItem, flips its status to in_use", async () => {
|
||||
const employee = await createTestEmployee();
|
||||
const modelId = await createTestModelId();
|
||||
const assetItem = await createTestAssetItem({ modelId });
|
||||
@@ -236,10 +236,10 @@ describe("createAssignment", () => {
|
||||
expect(created.value.returned_at).toBeNull();
|
||||
|
||||
const updatedItem = await prisma.assetItem.findUniqueOrThrow({ where: { id: assetItem.id } });
|
||||
expect(updatedItem.status).toBe("assigned");
|
||||
expect(updatedItem.status).toBe("in_use");
|
||||
});
|
||||
|
||||
it.each(["repair", "retired", "assigned"] as const)(
|
||||
it.each(["repair", "retired", "in_use"] as const)(
|
||||
"rejects assigning an AssetItem whose status is %s",
|
||||
async (status) => {
|
||||
const employee = await createTestEmployee();
|
||||
@@ -312,7 +312,7 @@ describe("createAssignment", () => {
|
||||
expect(second.value.employee_id).toBe(employee2.id);
|
||||
|
||||
const updatedItem = await prisma.assetItem.findUniqueOrThrow({ where: { id: assetItem.id } });
|
||||
expect(updatedItem.status).toBe("assigned");
|
||||
expect(updatedItem.status).toBe("in_use");
|
||||
});
|
||||
|
||||
it("returns a clean error when assetItemId doesn't reference an existing asset item", async () => {
|
||||
@@ -434,7 +434,7 @@ describe("createAssignment", () => {
|
||||
expect(failed).toHaveLength(1);
|
||||
|
||||
const item = await prisma.assetItem.findUniqueOrThrow({ where: { id: assetItem.id } });
|
||||
expect(item.status).toBe("assigned");
|
||||
expect(item.status).toBe("in_use");
|
||||
|
||||
const activeAssignments = await prisma.assignment.findMany({
|
||||
where: { assetItemId: assetItem.id, returnedAt: null },
|
||||
@@ -545,7 +545,7 @@ describe("returnAssignment", () => {
|
||||
});
|
||||
|
||||
describe("transferAssignment", () => {
|
||||
it("transfers an AssetItem assignment to a new employee, keeping status assigned throughout", async () => {
|
||||
it("transfers an AssetItem assignment to a new employee, keeping status in_use throughout", async () => {
|
||||
const employee1 = await createTestEmployee({ name: "Employee 1" });
|
||||
const employee2 = await createTestEmployee({ name: "Employee 2" });
|
||||
const modelId = await createTestModelId();
|
||||
@@ -562,11 +562,11 @@ describe("transferAssignment", () => {
|
||||
expect(transferred.value.asset_item_id).toBe(assetItem.id);
|
||||
expect(transferred.value.returned_at).toBeNull();
|
||||
|
||||
// The AssetItem must remain "assigned" throughout the transfer — never
|
||||
// The AssetItem must remain "in_use" throughout the transfer — never
|
||||
// flipped to in_stock mid-way (that's the whole point of doing this in
|
||||
// one transaction rather than a separate return + create).
|
||||
const item = await prisma.assetItem.findUniqueOrThrow({ where: { id: assetItem.id } });
|
||||
expect(item.status).toBe("assigned");
|
||||
expect(item.status).toBe("in_use");
|
||||
|
||||
const original = await prisma.assignment.findUniqueOrThrow({ where: { id: created.value.id } });
|
||||
expect(original.returnedAt).not.toBeNull();
|
||||
@@ -658,7 +658,7 @@ describe("transferAssignment", () => {
|
||||
const original = await prisma.assignment.findUniqueOrThrow({ where: { id: created.value.id } });
|
||||
expect(original.returnedAt).toBeNull();
|
||||
const item = await prisma.assetItem.findUniqueOrThrow({ where: { id: assetItem.id } });
|
||||
expect(item.status).toBe("assigned");
|
||||
expect(item.status).toBe("in_use");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -898,7 +898,7 @@ describe("PATCH /api/assignments/[id]", () => {
|
||||
expect(body.returned_at).toBeNull();
|
||||
|
||||
const item = await prisma.assetItem.findUniqueOrThrow({ where: { id: assetItem.id } });
|
||||
expect(item.status).toBe("assigned");
|
||||
expect(item.status).toBe("in_use");
|
||||
});
|
||||
|
||||
it("rejects a body with both employeeId and returnedAt with a mutual-exclusivity 400", async () => {
|
||||
|
||||
@@ -61,7 +61,7 @@ async function createTestModelId(): Promise<string> {
|
||||
let serialCounter = 0;
|
||||
function createTestAssetItem(overrides: {
|
||||
modelId: string;
|
||||
status?: "in_stock" | "assigned" | "repair" | "retired";
|
||||
status?: "in_stock" | "in_use" | "repair" | "retired";
|
||||
}) {
|
||||
serialCounter += 1;
|
||||
return prisma.assetItem.create({
|
||||
@@ -223,7 +223,7 @@ describe("createMaintenanceRecord", () => {
|
||||
|
||||
it("rejects opening a record for an AssetItem that has an active assignment", async () => {
|
||||
const modelId = await createTestModelId();
|
||||
const assetItem = await createTestAssetItem({ modelId, status: "assigned" });
|
||||
const assetItem = await createTestAssetItem({ modelId, status: "in_use" });
|
||||
const employee = await createTestEmployee();
|
||||
await prisma.assignment.create({
|
||||
data: { employeeId: employee.id, assetItemId: assetItem.id, assignedAt: new Date() },
|
||||
@@ -495,7 +495,7 @@ describe("POST /api/maintenance-records", () => {
|
||||
|
||||
it("rejects creating a record for an AssetItem with an active assignment", async () => {
|
||||
const modelId = await createTestModelId();
|
||||
const assetItem = await createTestAssetItem({ modelId, status: "assigned" });
|
||||
const assetItem = await createTestAssetItem({ modelId, status: "in_use" });
|
||||
const employee = await createTestEmployee();
|
||||
await prisma.assignment.create({
|
||||
data: { employeeId: employee.id, assetItemId: assetItem.id, assignedAt: new Date() },
|
||||
|
||||
Reference in New Issue
Block a user