feat: add License.purchaseOrderId (nullable FK)
Nullable FK from License to PurchaseOrder for supply-side traceability (which PO a license came in through), mirroring AssetItem.purchaseOrderId's just-merged pattern exactly. Separate commit from the earlier License.vendorId retrofit per the project's 1-commit-per-business-change convention.
This commit is contained in:
+161
-2
@@ -49,11 +49,29 @@ function createTestVendor(overrides: { name?: string } = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
// License.purchaseOrderId is a real FK to PurchaseOrder (see schema.prisma /
|
||||
// src/lib/licenses.ts) — tests that need a valid purchaseOrderId create a
|
||||
// real PurchaseOrder row first and use its id. Mirrors
|
||||
// tests/assets.test.ts's createTestPurchaseOrder() helper.
|
||||
function createTestPurchaseOrder(overrides: { vendorId?: string } = {}) {
|
||||
return prisma.purchaseOrder.create({
|
||||
data: {
|
||||
vendorId: overrides.vendorId as string,
|
||||
orderDate: new Date("2026-08-01"),
|
||||
expectedDate: new Date("2026-08-10"),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
// Each test starts from empty tables so listing/order assertions are
|
||||
// deterministic. licenses must be cleared before vendors since
|
||||
// licenses.vendor_id has an FK (ON DELETE RESTRICT) to vendors.
|
||||
// deterministic. licenses must be cleared before purchase_orders/vendors
|
||||
// since licenses has FKs (ON DELETE RESTRICT to vendors, ON DELETE SET
|
||||
// NULL to purchase_orders) to both. purchase_orders must be cleared
|
||||
// before vendors (ON DELETE RESTRICT to vendors, mirrors
|
||||
// tests/assets.test.ts).
|
||||
await prisma.license.deleteMany();
|
||||
await prisma.purchaseOrder.deleteMany();
|
||||
await prisma.vendor.deleteMany();
|
||||
// Default to a logged-in session — the lib-level tests below don't go
|
||||
// through the route at all (so this is a no-op for them), and the
|
||||
@@ -222,6 +240,21 @@ describe("validateCreateLicenseInput", () => {
|
||||
expect(result.error).toMatch(/licenseType/i);
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects a purchaseOrderId that is not a valid UUID", () => {
|
||||
const result = validateCreateLicenseInput({
|
||||
name: "Adobe Creative Cloud",
|
||||
vendorId: "11111111-1111-1111-1111-111111111111",
|
||||
seatsTotal: 10,
|
||||
licenseType: "subscription",
|
||||
purchaseOrderId: "not-a-uuid",
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.error).toMatch(/purchaseOrderId/i);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("createLicense", () => {
|
||||
@@ -298,6 +331,64 @@ describe("createLicense", () => {
|
||||
const remaining = await prisma.license.findMany();
|
||||
expect(remaining).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("creates a license with a purchaseOrderId that references a real purchase order", async () => {
|
||||
const vendor = await createTestVendor();
|
||||
const purchaseOrder = await createTestPurchaseOrder({ vendorId: vendor.id });
|
||||
const result = validateCreateLicenseInput({
|
||||
name: "Adobe Creative Cloud",
|
||||
vendorId: vendor.id,
|
||||
seatsTotal: 10,
|
||||
licenseType: "subscription",
|
||||
purchaseOrderId: purchaseOrder.id,
|
||||
});
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
|
||||
const created = await createLicense(result.value);
|
||||
expect(created.ok).toBe(true);
|
||||
if (!created.ok) return;
|
||||
expect(created.value.purchase_order_id).toBe(purchaseOrder.id);
|
||||
});
|
||||
|
||||
it("creates a license with no purchaseOrderId (stays null)", async () => {
|
||||
const vendor = await createTestVendor();
|
||||
const result = validateCreateLicenseInput({
|
||||
name: "Adobe Creative Cloud",
|
||||
vendorId: vendor.id,
|
||||
seatsTotal: 10,
|
||||
licenseType: "subscription",
|
||||
});
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
|
||||
const created = await createLicense(result.value);
|
||||
expect(created.ok).toBe(true);
|
||||
if (!created.ok) return;
|
||||
expect(created.value.purchase_order_id).toBeNull();
|
||||
});
|
||||
|
||||
it("returns a clean error when purchaseOrderId doesn't reference an existing purchase order", async () => {
|
||||
const vendor = await createTestVendor();
|
||||
const result = validateCreateLicenseInput({
|
||||
name: "Adobe Creative Cloud",
|
||||
vendorId: vendor.id,
|
||||
seatsTotal: 10,
|
||||
licenseType: "subscription",
|
||||
purchaseOrderId: "00000000-0000-0000-0000-000000000000",
|
||||
});
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
|
||||
const created = await createLicense(result.value);
|
||||
expect(created.ok).toBe(false);
|
||||
if (created.ok) return;
|
||||
expect(created.error).toMatch(/purchaseOrderId/i);
|
||||
expect(created.error).toMatch(/does not reference an existing purchase order/i);
|
||||
|
||||
const remaining = await prisma.license.findMany();
|
||||
expect(remaining).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("listLicenses", () => {
|
||||
@@ -471,6 +562,74 @@ describe("POST /api/licenses", () => {
|
||||
expect(body.error).toMatch(/licenseType/i);
|
||||
});
|
||||
|
||||
it("creates a license with a purchaseOrderId that references a real purchase order", async () => {
|
||||
const vendor = await createTestVendor();
|
||||
const purchaseOrder = await createTestPurchaseOrder({ vendorId: vendor.id });
|
||||
const res = await POST(
|
||||
postRequest({
|
||||
name: "Adobe Creative Cloud",
|
||||
vendorId: vendor.id,
|
||||
seatsTotal: 10,
|
||||
licenseType: "subscription",
|
||||
purchaseOrderId: purchaseOrder.id,
|
||||
})
|
||||
);
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
const body = await res.json();
|
||||
expect(body.purchase_order_id).toBe(purchaseOrder.id);
|
||||
});
|
||||
|
||||
it("creates a license with no purchaseOrderId (stays null)", async () => {
|
||||
const vendor = await createTestVendor();
|
||||
const res = await POST(
|
||||
postRequest({ name: "Adobe Creative Cloud", vendorId: vendor.id, seatsTotal: 10, licenseType: "subscription" })
|
||||
);
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
const body = await res.json();
|
||||
expect(body.purchase_order_id).toBeNull();
|
||||
});
|
||||
|
||||
it("rejects a purchaseOrderId that is a well-formed UUID but doesn't reference an existing purchase order", async () => {
|
||||
const vendor = await createTestVendor();
|
||||
const res = await POST(
|
||||
postRequest({
|
||||
name: "Adobe Creative Cloud",
|
||||
vendorId: vendor.id,
|
||||
seatsTotal: 10,
|
||||
licenseType: "subscription",
|
||||
purchaseOrderId: "00000000-0000-0000-0000-000000000000",
|
||||
})
|
||||
);
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
const body = await res.json();
|
||||
expect(body.error).toMatch(/purchaseOrderId/i);
|
||||
expect(body.error).toMatch(/does not reference an existing purchase order/i);
|
||||
|
||||
// And it must not have created anything.
|
||||
const remaining = await prisma.license.findMany();
|
||||
expect(remaining).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("rejects a purchaseOrderId that is not a valid UUID", async () => {
|
||||
const vendor = await createTestVendor();
|
||||
const res = await POST(
|
||||
postRequest({
|
||||
name: "Adobe Creative Cloud",
|
||||
vendorId: vendor.id,
|
||||
seatsTotal: 10,
|
||||
licenseType: "subscription",
|
||||
purchaseOrderId: "not-a-uuid",
|
||||
})
|
||||
);
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
const body = await res.json();
|
||||
expect(body.error).toMatch(/purchaseOrderId/i);
|
||||
});
|
||||
|
||||
it("rejects an invalid JSON body", async () => {
|
||||
const req = new Request("http://localhost/api/licenses", {
|
||||
method: "POST",
|
||||
|
||||
Reference in New Issue
Block a user