import { randomUUID } from 'node:crypto'; import { identityApp, type IdentityApp } from './helpers/identity-app'; import { secondActor } from './helpers/commerce'; describe('supplier and material API', () => { let ctx: IdentityApp; const auth = { type: 'bearer' as const }; const supplierInput = { name: 'Candle Supply Co', code: 'CANDLE-SUPPLY', contactName: 'Asha Shah', email: 'asha@example.com', phone: '+919876543210', }; const materialInput = { name: 'Soy Wax', code: 'SOY-WAX', kind: 'WAX', unit: 'KILOGRAM', }; beforeAll(async () => { ctx = await identityApp(); }, 60000); afterAll(async () => { await ctx?.close(); }); beforeEach(async () => { await ctx.clearLimits(); }); it('creates scoped master data and a supplier-material quote', async () => { const supplier = await ctx .api() .post('/api/v1/suppliers') .auth(ctx.token, auth) .send(supplierInput) .expect(201); const material = await ctx .api() .post('/api/v1/materials') .auth(ctx.token, auth) .send(materialInput) .expect(201); const relation = await ctx .api() .put( `/api/v1/suppliers/${supplier.body.id}/materials/${material.body.id}`, ) .auth(ctx.token, auth) .send({ supplierSku: 'SW-25', leadTimeDays: 14, minOrderQuantity: '25.000', unitPrice: '320.00', }) .expect(200); expect(relation.body).toMatchObject({ supplierId: supplier.body.id, materialId: material.body.id, currency: 'INR', active: true, }); const listed = await ctx .api() .get(`/api/v1/suppliers/${supplier.body.id}/materials`) .auth(ctx.token, auth) .expect(200); expect(listed.body[0].material).toMatchObject({ id: material.body.id, name: 'Soy Wax', }); await ctx .api() .put(`/api/v1/suppliers/${supplier.body.id}`) .auth(ctx.token, auth) .send({ ...supplierInput, active: false }) .expect(200); await ctx .api() .put(`/api/v1/materials/${material.body.id}`) .auth(ctx.token, auth) .send({ ...materialInput, name: 'Refined Soy Wax' }) .expect(200); const activeOnly = await ctx .api() .get('/api/v1/suppliers?active=true') .auth(ctx.token, auth) .expect(200); expect(activeOnly.body).toHaveLength(0); expect( await ctx.db.auditEvent.count({ where: { organizationId: ctx.owner.organizationId, action: 'supplier_material.saved', }, }), ).toBe(1); }); it('enforces permissions, organization scope and strict validation', async () => { const reader = await secondActor(ctx, true, ['procurement.read']); await ctx .api() .get('/api/v1/suppliers') .auth(reader.token, auth) .expect(200); await ctx .api() .post('/api/v1/suppliers') .auth(reader.token, auth) .send(supplierInput) .expect(403); const outsider = await secondActor(ctx, false, ['procurement.manage']); await ctx .api() .put(`/api/v1/suppliers/${randomUUID()}`) .auth(outsider.token, auth) .send(supplierInput) .expect(404); const invalid = await ctx .api() .post('/api/v1/materials') .auth(ctx.token, auth) .send({ ...materialInput, code: '' }) .expect(400); expect(invalid.body.code).toBe('REQUEST_INVALID'); const duplicateInput = { ...materialInput, code: `SOY-${randomUUID().slice(0, 8)}`, }; await ctx .api() .post('/api/v1/materials') .auth(ctx.token, auth) .send(duplicateInput) .expect(201); const duplicate = await ctx .api() .post('/api/v1/materials') .auth(ctx.token, auth) .send(duplicateInput) .expect(409); expect(duplicate.body.code).toBe('RECORD_CONFLICT'); }); it('returns distinct missing-resource errors for compatibility mutations', async () => { const missingSupplier = await ctx .api() .put(`/api/v1/suppliers/${randomUUID()}/materials/${randomUUID()}`) .auth(ctx.token, auth) .send({ supplierSku: 'X', leadTimeDays: 0, minOrderQuantity: '1.000', unitPrice: '0.00', }) .expect(404); expect(missingSupplier.body.code).toBe('SUPPLIER_NOT_FOUND'); const missingList = await ctx .api() .get(`/api/v1/suppliers/${randomUUID()}/materials`) .auth(ctx.token, auth) .expect(404); expect(missingList.body.code).toBe('SUPPLIER_NOT_FOUND'); const supplier = await ctx .api() .post('/api/v1/suppliers') .auth(ctx.token, auth) .send({ ...supplierInput, code: 'S-' + randomUUID().slice(0, 8) }) .expect(201); const missingMaterial = await ctx .api() .put(`/api/v1/suppliers/${supplier.body.id}/materials/${randomUUID()}`) .auth(ctx.token, auth) .send({ supplierSku: 'X', leadTimeDays: 0, minOrderQuantity: '1.000', unitPrice: '0.00', }) .expect(404); expect(missingMaterial.body.code).toBe('MATERIAL_NOT_FOUND'); }); });