Add customers management module with database schema and validation

- Introduced `CustomersModule` to manage customer data, including read and write controllers.
- Created database migrations for the `customers` and `customer_contacts` tables, including constraints and unique indexes.
- Implemented validation for customer fields such as name, code, and address with corresponding utility functions.
- Developed service and repository layers for handling customer data operations.
- Added unit tests for the customers service, repository, and controllers to ensure functionality and correctness.
- Updated application module to include the new `CustomersModule` for better organization.
This commit is contained in:
shancheas
2026-08-24 14:23:20 +07:00
parent d28506e878
commit cdcc508947
20 changed files with 4123 additions and 2 deletions
@@ -0,0 +1,105 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CustomersWriteController } from './customers-write.controller';
import { CustomersService } from './customers.service';
const createDto = {
code: 'CUST_01',
name: 'Acme Corp',
phone: '+6281234567890',
address: 'Jl Sudirman No 1',
};
describe('CustomersWriteController', () => {
let controller: CustomersWriteController;
const service = {
create: jest.fn(),
update: jest.fn(),
updateStatus: jest.fn(),
delete: jest.fn(),
bulkDelete: jest.fn(),
bulkUpdateStatus: jest.fn(),
importCsv: jest.fn(),
addContact: jest.fn(),
updateContact: jest.fn(),
deleteContact: jest.fn(),
};
beforeEach(async () => {
jest.clearAllMocks();
const moduleRef: TestingModule = await Test.createTestingModule({
controllers: [CustomersWriteController],
providers: [{ provide: CustomersService, useValue: service }],
}).compile();
controller = moduleRef.get(CustomersWriteController);
});
it('create passes dto fields and user id', async () => {
service.create.mockResolvedValue({ id: 'cu-1' });
await controller.create(createDto, 'user-1');
expect(service.create).toHaveBeenCalledWith({
...createDto,
userId: 'user-1',
});
});
it('update, updateStatus, and delete delegate', async () => {
service.update.mockResolvedValue({ id: 'cu-1' });
service.updateStatus.mockResolvedValue({ id: 'cu-1' });
service.delete.mockResolvedValue(undefined);
await controller.update('cu-1', { name: 'Acme Corp' }, 'user-1');
await controller.updateStatus('cu-1', { status: 'active' }, 'user-1');
await controller.delete('cu-1');
expect(service.updateStatus).toHaveBeenCalledWith(
'cu-1',
'active',
'user-1',
);
expect(service.delete).toHaveBeenCalledWith('cu-1');
});
it('nested contact routes delegate', async () => {
service.addContact.mockResolvedValue({ id: 'cu-1' });
service.updateContact.mockResolvedValue({ id: 'cu-1' });
service.deleteContact.mockResolvedValue(undefined);
await controller.addContact('cu-1', { name: 'Ada Lovelace' }, 'user-1');
await controller.updateContact(
'cu-1',
'ct-1',
{ jobTitle: 'Buyer' },
'user-1',
);
await controller.deleteContact('cu-1', 'ct-1');
expect(service.addContact).toHaveBeenCalledWith(
'cu-1',
{ name: 'Ada Lovelace' },
'user-1',
);
expect(service.updateContact).toHaveBeenCalledWith('cu-1', 'ct-1', {
jobTitle: 'Buyer',
userId: 'user-1',
});
expect(service.deleteContact).toHaveBeenCalledWith('cu-1', 'ct-1');
});
it('bulk and import delegate', async () => {
service.bulkDelete.mockResolvedValue({ deleted: 1 });
service.bulkUpdateStatus.mockResolvedValue({ updated: 1 });
service.importCsv.mockResolvedValue({ imported: 1 });
await controller.bulkDelete({ ids: ['cu-1'] });
await controller.bulkStatus(
{ ids: ['cu-1'], status: 'archived' },
'user-1',
);
await controller.importCsv(
{ buffer: Buffer.from('code,name\nCUST_01,Acme') },
'user-1',
);
expect(service.importCsv).toHaveBeenCalled();
});
it('importCsv uses empty string when file is missing', async () => {
service.importCsv.mockResolvedValue({ imported: 0 });
await controller.importCsv(undefined, 'user-1');
expect(service.importCsv).toHaveBeenCalledWith('', 'user-1');
});
});