Rename AssetItemStatus 'assigned' to 'in_use'
CI / test (push) Failing after 9s

'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:
toanhoang
2026-08-24 21:57:33 -04:00
parent 10508f9d04
commit 03d74d69c1
11 changed files with 63 additions and 45 deletions
+18 -8
View File
@@ -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
View File
@@ -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 () => {
+3 -3
View File
@@ -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() },