import { suite } from '../lib/context.js' suite('Inventory', { tags: ['inventory'] }, (t) => { // ─── Categories ──────────────────────────────────────────────────────────── t.test('creates a category', { tags: ['categories', 'create'] }, async () => { const res = await t.api.post('/v1/categories', { name: 'Violins', sortOrder: 1 }) t.assert.status(res, 201) t.assert.equal(res.data.name, 'Violins') t.assert.equal(res.data.sortOrder, 1) t.assert.equal(res.data.isActive, true) t.assert.ok(res.data.id) }) t.test('rejects category without name', { tags: ['categories', 'create', 'validation'] }, async () => { const res = await t.api.post('/v1/categories', {}) t.assert.status(res, 400) }) t.test('lists categories with pagination', { tags: ['categories', 'list'] }, async () => { await t.api.post('/v1/categories', { name: 'Cat List A' }) await t.api.post('/v1/categories', { name: 'Cat List B' }) const res = await t.api.get('/v1/categories', { page: 1, limit: 25 }) t.assert.status(res, 200) t.assert.ok(res.data.data.length >= 2) t.assert.ok(res.data.pagination) }) t.test('gets category by id', { tags: ['categories', 'read'] }, async () => { const created = await t.api.post('/v1/categories', { name: 'Get By ID Cat' }) const res = await t.api.get(`/v1/categories/${created.data.id}`) t.assert.status(res, 200) t.assert.equal(res.data.name, 'Get By ID Cat') }) t.test('returns 404 for missing category', { tags: ['categories', 'read'] }, async () => { const res = await t.api.get('/v1/categories/a0000000-0000-0000-0000-999999999999') t.assert.status(res, 404) }) t.test('updates a category', { tags: ['categories', 'update'] }, async () => { const created = await t.api.post('/v1/categories', { name: 'Before Update' }) const res = await t.api.patch(`/v1/categories/${created.data.id}`, { name: 'After Update', sortOrder: 5 }) t.assert.status(res, 200) t.assert.equal(res.data.name, 'After Update') t.assert.equal(res.data.sortOrder, 5) }) t.test('soft-deletes a category', { tags: ['categories', 'delete'] }, async () => { const created = await t.api.post('/v1/categories', { name: 'To Delete Cat' }) const res = await t.api.del(`/v1/categories/${created.data.id}`) t.assert.status(res, 200) t.assert.equal(res.data.isActive, false) }) // ─── Suppliers ───────────────────────────────────────────────────────────── t.test('creates a supplier', { tags: ['suppliers', 'create'] }, async () => { const res = await t.api.post('/v1/suppliers', { name: 'Shar Music', email: 'orders@sharmusic.com', phone: '800-248-7427', paymentTerms: 'Net 30', }) t.assert.status(res, 201) t.assert.equal(res.data.name, 'Shar Music') t.assert.equal(res.data.paymentTerms, 'Net 30') t.assert.ok(res.data.id) }) t.test('rejects supplier without name', { tags: ['suppliers', 'create', 'validation'] }, async () => { const res = await t.api.post('/v1/suppliers', { email: 'x@x.com' }) t.assert.status(res, 400) }) t.test('lists suppliers with pagination', { tags: ['suppliers', 'list'] }, async () => { await t.api.post('/v1/suppliers', { name: 'Sup List A' }) await t.api.post('/v1/suppliers', { name: 'Sup List B' }) const res = await t.api.get('/v1/suppliers', { page: 1, limit: 25 }) t.assert.status(res, 200) t.assert.ok(res.data.data.length >= 2) t.assert.ok(res.data.pagination.total >= 2) }) t.test('searches suppliers by name', { tags: ['suppliers', 'list', 'search'] }, async () => { await t.api.post('/v1/suppliers', { name: 'SearchableSupplierXYZ' }) const res = await t.api.get('/v1/suppliers', { q: 'SearchableSupplierXYZ' }) t.assert.status(res, 200) t.assert.equal(res.data.data.length, 1) t.assert.equal(res.data.data[0].name, 'SearchableSupplierXYZ') }) t.test('updates a supplier', { tags: ['suppliers', 'update'] }, async () => { const created = await t.api.post('/v1/suppliers', { name: 'Old Supplier Name' }) const res = await t.api.patch(`/v1/suppliers/${created.data.id}`, { name: 'New Supplier Name', accountNumber: 'ACC-001' }) t.assert.status(res, 200) t.assert.equal(res.data.name, 'New Supplier Name') t.assert.equal(res.data.accountNumber, 'ACC-001') }) t.test('soft-deletes a supplier', { tags: ['suppliers', 'delete'] }, async () => { const created = await t.api.post('/v1/suppliers', { name: 'To Delete Sup' }) const res = await t.api.del(`/v1/suppliers/${created.data.id}`) t.assert.status(res, 200) t.assert.equal(res.data.isActive, false) }) // ─── Products ────────────────────────────────────────────────────────────── t.test('creates a product', { tags: ['products', 'create'] }, async () => { const res = await t.api.post('/v1/products', { name: 'Violin 4/4 Student', sku: 'VLN-44-TST', brand: 'Eastman', price: 399, isSerialized: true, isRental: true, isActive: true, }) t.assert.status(res, 201) t.assert.equal(res.data.name, 'Violin 4/4 Student') t.assert.equal(res.data.sku, 'VLN-44-TST') t.assert.equal(res.data.price, '399.00') t.assert.equal(res.data.isSerialized, true) t.assert.ok(res.data.id) }) t.test('rejects product without name', { tags: ['products', 'create', 'validation'] }, async () => { const res = await t.api.post('/v1/products', { sku: 'NO-NAME' }) t.assert.status(res, 400) }) t.test('lists products with pagination', { tags: ['products', 'list'] }, async () => { await t.api.post('/v1/products', { name: 'Prod List A', price: 100 }) await t.api.post('/v1/products', { name: 'Prod List B', price: 200 }) const res = await t.api.get('/v1/products', { page: 1, limit: 25 }) t.assert.status(res, 200) t.assert.ok(res.data.data.length >= 2) t.assert.ok(res.data.pagination) }) t.test('searches products by name', { tags: ['products', 'list', 'search'] }, async () => { await t.api.post('/v1/products', { name: 'SearchableViolinXYZ' }) const res = await t.api.get('/v1/products', { q: 'SearchableViolinXYZ' }) t.assert.status(res, 200) t.assert.equal(res.data.data.length, 1) t.assert.equal(res.data.data[0].name, 'SearchableViolinXYZ') }) t.test('filters products by categoryId', { tags: ['products', 'list', 'filter'] }, async () => { const cat = await t.api.post('/v1/categories', { name: 'FilterCat' }) await t.api.post('/v1/products', { name: 'In Cat Prod', categoryId: cat.data.id }) await t.api.post('/v1/products', { name: 'No Cat Prod' }) const res = await t.api.get('/v1/products', { categoryId: cat.data.id }) t.assert.status(res, 200) t.assert.ok(res.data.data.every((p: any) => p.categoryId === cat.data.id)) }) t.test('gets product by id', { tags: ['products', 'read'] }, async () => { const created = await t.api.post('/v1/products', { name: 'Get By ID Prod' }) const res = await t.api.get(`/v1/products/${created.data.id}`) t.assert.status(res, 200) t.assert.equal(res.data.name, 'Get By ID Prod') }) t.test('returns 404 for missing product', { tags: ['products', 'read'] }, async () => { const res = await t.api.get('/v1/products/a0000000-0000-0000-0000-999999999999') t.assert.status(res, 404) }) t.test('updates a product and records price history', { tags: ['products', 'update', 'price-history'] }, async () => { const created = await t.api.post('/v1/products', { name: 'Price Test Prod', price: 100 }) const res = await t.api.patch(`/v1/products/${created.data.id}`, { price: 150, name: 'Price Test Prod Updated' }) t.assert.status(res, 200) t.assert.equal(res.data.price, '150.00') const history = await t.api.get(`/v1/products/${created.data.id}/price-history`) t.assert.status(history, 200) t.assert.equal(history.data.data.length, 1) t.assert.equal(history.data.data[0].previousPrice, '100.00') t.assert.equal(history.data.data[0].newPrice, '150.00') }) t.test('price history is empty for new product', { tags: ['products', 'price-history'] }, async () => { const created = await t.api.post('/v1/products', { name: 'No History Prod', price: 50 }) const res = await t.api.get(`/v1/products/${created.data.id}/price-history`) t.assert.status(res, 200) t.assert.equal(res.data.data.length, 0) }) t.test('soft-deletes a product', { tags: ['products', 'delete'] }, async () => { const created = await t.api.post('/v1/products', { name: 'To Delete Prod' }) const res = await t.api.del(`/v1/products/${created.data.id}`) t.assert.status(res, 200) t.assert.equal(res.data.isActive, false) }) // ─── Inventory Units ─────────────────────────────────────────────────────── t.test('creates a serialized unit', { tags: ['units', 'create'] }, async () => { const prod = await t.api.post('/v1/products', { name: 'Serial Prod', isSerialized: true }) const res = await t.api.post(`/v1/products/${prod.data.id}/units`, { serialNumber: 'SN-001', condition: 'new', status: 'available', purchaseCost: 185, }) t.assert.status(res, 201) t.assert.equal(res.data.serialNumber, 'SN-001') t.assert.equal(res.data.condition, 'new') t.assert.equal(res.data.status, 'available') t.assert.equal(res.data.purchaseCost, '185.00') }) t.test('lists units for a product', { tags: ['units', 'list'] }, async () => { const prod = await t.api.post('/v1/products', { name: 'Multi Unit Prod', isSerialized: true }) await t.api.post(`/v1/products/${prod.data.id}/units`, { serialNumber: 'SN-A', condition: 'new', status: 'available' }) await t.api.post(`/v1/products/${prod.data.id}/units`, { serialNumber: 'SN-B', condition: 'good', status: 'rented' }) const res = await t.api.get(`/v1/products/${prod.data.id}/units`) t.assert.status(res, 200) t.assert.equal(res.data.data.length, 2) }) t.test('updates a unit status', { tags: ['units', 'update'] }, async () => { const prod = await t.api.post('/v1/products', { name: 'Update Unit Prod', isSerialized: true }) const unit = await t.api.post(`/v1/products/${prod.data.id}/units`, { serialNumber: 'SN-UPD', condition: 'good', status: 'available' }) const res = await t.api.patch(`/v1/units/${unit.data.id}`, { status: 'rented', notes: 'Rented to customer A' }) t.assert.status(res, 200) t.assert.equal(res.data.status, 'rented') t.assert.equal(res.data.notes, 'Rented to customer A') }) // ─── Product Suppliers ───────────────────────────────────────────────────── t.test('links a supplier to a product', { tags: ['product-suppliers', 'create'] }, async () => { const prod = await t.api.post('/v1/products', { name: 'Linkable Prod' }) const sup = await t.api.post('/v1/suppliers', { name: 'Link Supplier' }) const res = await t.api.post(`/v1/products/${prod.data.id}/suppliers`, { supplierId: sup.data.id, supplierSku: 'SUP-SKU-001', isPreferred: true, }) t.assert.status(res, 201) t.assert.equal(res.data.supplierId, sup.data.id) t.assert.equal(res.data.supplierSku, 'SUP-SKU-001') t.assert.equal(res.data.isPreferred, true) }) t.test('lists suppliers for a product with joined supplier details', { tags: ['product-suppliers', 'list'] }, async () => { const prod = await t.api.post('/v1/products', { name: 'Supplier List Prod' }) const sup1 = await t.api.post('/v1/suppliers', { name: 'Supplier Alpha', paymentTerms: 'Net 30' }) const sup2 = await t.api.post('/v1/suppliers', { name: 'Supplier Beta' }) await t.api.post(`/v1/products/${prod.data.id}/suppliers`, { supplierId: sup1.data.id, isPreferred: true }) await t.api.post(`/v1/products/${prod.data.id}/suppliers`, { supplierId: sup2.data.id, isPreferred: false }) const res = await t.api.get(`/v1/products/${prod.data.id}/suppliers`) t.assert.status(res, 200) t.assert.equal(res.data.data.length, 2) // preferred comes first t.assert.equal(res.data.data[0].supplierName, 'Supplier Alpha') t.assert.equal(res.data.data[0].isPreferred, true) t.assert.equal(res.data.data[0].supplierPaymentTerms, 'Net 30') t.assert.equal(res.data.data[1].supplierName, 'Supplier Beta') }) t.test('preferred flag moves to new supplier when set', { tags: ['product-suppliers', 'preferred'] }, async () => { const prod = await t.api.post('/v1/products', { name: 'Preferred Test Prod' }) const sup1 = await t.api.post('/v1/suppliers', { name: 'Pref Sup A' }) const sup2 = await t.api.post('/v1/suppliers', { name: 'Pref Sup B' }) const link1 = await t.api.post(`/v1/products/${prod.data.id}/suppliers`, { supplierId: sup1.data.id, isPreferred: true }) t.assert.equal(link1.data.isPreferred, true) // Link second supplier as preferred — first should lose preferred status await t.api.post(`/v1/products/${prod.data.id}/suppliers`, { supplierId: sup2.data.id, isPreferred: true }) const list = await t.api.get(`/v1/products/${prod.data.id}/suppliers`) const a = list.data.data.find((s: any) => s.supplierId === sup1.data.id) const b = list.data.data.find((s: any) => s.supplierId === sup2.data.id) t.assert.equal(a.isPreferred, false) t.assert.equal(b.isPreferred, true) }) t.test('updates a product-supplier link', { tags: ['product-suppliers', 'update'] }, async () => { const prod = await t.api.post('/v1/products', { name: 'Update Link Prod' }) const sup = await t.api.post('/v1/suppliers', { name: 'Update Link Sup' }) const link = await t.api.post(`/v1/products/${prod.data.id}/suppliers`, { supplierId: sup.data.id, supplierSku: 'OLD-SKU' }) const res = await t.api.patch(`/v1/products/${prod.data.id}/suppliers/${link.data.id}`, { supplierSku: 'NEW-SKU' }) t.assert.status(res, 200) t.assert.equal(res.data.supplierSku, 'NEW-SKU') }) t.test('removes a supplier link', { tags: ['product-suppliers', 'delete'] }, async () => { const prod = await t.api.post('/v1/products', { name: 'Remove Link Prod' }) const sup = await t.api.post('/v1/suppliers', { name: 'Remove Link Sup' }) const link = await t.api.post(`/v1/products/${prod.data.id}/suppliers`, { supplierId: sup.data.id }) const res = await t.api.del(`/v1/products/${prod.data.id}/suppliers/${link.data.id}`) t.assert.status(res, 200) const list = await t.api.get(`/v1/products/${prod.data.id}/suppliers`) t.assert.equal(list.data.data.length, 0) }) t.test('returns 404 for missing product-supplier link update', { tags: ['product-suppliers', 'update'] }, async () => { const prod = await t.api.post('/v1/products', { name: 'Missing Link Prod' }) const res = await t.api.patch(`/v1/products/${prod.data.id}/suppliers/a0000000-0000-0000-0000-999999999999`, { supplierSku: 'X' }) t.assert.status(res, 404) }) // ─── Stock Receipts ──────────────────────────────────────────────────────── t.test('records a stock receipt and increments qty_on_hand', { tags: ['stock-receipts', 'create'] }, async () => { const prod = await t.api.post('/v1/products', { name: 'Rosin Stock', qtyOnHand: 10 }) const res = await t.api.post(`/v1/products/${prod.data.id}/stock-receipts`, { qty: 5, costPerUnit: 6.50, receivedDate: '2026-03-15', invoiceNumber: 'INV-2026-001', }) t.assert.status(res, 201) t.assert.equal(res.data.qty, 5) t.assert.equal(res.data.costPerUnit, '6.50') t.assert.equal(res.data.totalCost, '32.50') t.assert.equal(res.data.invoiceNumber, 'INV-2026-001') // qty_on_hand should have increased const updated = await t.api.get(`/v1/products/${prod.data.id}`) t.assert.equal(updated.data.qtyOnHand, 15) }) t.test('records receipt with supplier link', { tags: ['stock-receipts', 'create'] }, async () => { const prod = await t.api.post('/v1/products', { name: 'Strings Stock' }) const sup = await t.api.post('/v1/suppliers', { name: 'Receipt Supplier' }) const res = await t.api.post(`/v1/products/${prod.data.id}/stock-receipts`, { supplierId: sup.data.id, qty: 12, costPerUnit: 19.50, receivedDate: '2026-03-20', }) t.assert.status(res, 201) t.assert.equal(res.data.supplierId, sup.data.id) }) t.test('does not increment qty for serialized products', { tags: ['stock-receipts', 'create'] }, async () => { const prod = await t.api.post('/v1/products', { name: 'Serial Stock', isSerialized: true, qtyOnHand: 0 }) await t.api.post(`/v1/products/${prod.data.id}/stock-receipts`, { qty: 1, costPerUnit: 185, receivedDate: '2026-03-01', }) const updated = await t.api.get(`/v1/products/${prod.data.id}`) t.assert.equal(updated.data.qtyOnHand, 0) }) t.test('lists stock receipts for a product', { tags: ['stock-receipts', 'list'] }, async () => { const prod = await t.api.post('/v1/products', { name: 'List Receipts Prod' }) await t.api.post(`/v1/products/${prod.data.id}/stock-receipts`, { qty: 10, costPerUnit: 5, receivedDate: '2026-01-10' }) await t.api.post(`/v1/products/${prod.data.id}/stock-receipts`, { qty: 20, costPerUnit: 4.75, receivedDate: '2026-02-15' }) const res = await t.api.get(`/v1/products/${prod.data.id}/stock-receipts`) t.assert.status(res, 200) t.assert.equal(res.data.data.length, 2) // most recent date first t.assert.equal(res.data.data[0].receivedDate, '2026-02-15') }) t.test('rejects receipt with missing required fields', { tags: ['stock-receipts', 'validation'] }, async () => { const prod = await t.api.post('/v1/products', { name: 'Invalid Receipt Prod' }) const res = await t.api.post(`/v1/products/${prod.data.id}/stock-receipts`, { qty: 5 }) t.assert.status(res, 400) }) })