185 lines
5.1 KiB
TypeScript
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');
|
|
});
|
|
});
|