manicanldes-backend/test/procurement.spec.ts

185 lines
5.1 KiB
TypeScript

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: '<unsafe>' })
.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');
});
});