Add products and sales management modules with database schema and validation
- Introduced `ProductsModule` to manage product data, including read and write controllers. - Created database migrations for the `products`, `sales_requests`, `sales_orders`, `sales_invoices`, and related tables, including constraints and unique indexes. - Implemented validation for product fields such as code, name, unit, and brand with corresponding utility functions. - Developed service and repository layers for handling product and sales data operations. - Added unit tests for the products and sales services, repositories, and controllers to ensure functionality and correctness. - Updated application module to include the new `ProductsModule` and related sales modules for better organization.
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
import { BadRequestException, NotFoundException } from '@nestjs/common';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { DateTime } from '../../../common/value-objects/date-time/date-time';
|
||||
import { Decimal } from '../../../common/value-objects/decimal/decimal';
|
||||
import { Status } from '../../../common/value-objects/status/status';
|
||||
import { SalesInvoicesService } from '../sales-invoices/sales-invoices.service';
|
||||
import { SALES_PAYMENT_STATUSES } from '../shared/sales-fields';
|
||||
import type { SalesPayment } from './sales-payment';
|
||||
import { SalesPaymentsRepository } from './sales-payments.repository';
|
||||
import { SalesPaymentsService } from './sales-payments.service';
|
||||
|
||||
describe('SalesPaymentsService', () => {
|
||||
let service: SalesPaymentsService;
|
||||
const repository: jest.Mocked<
|
||||
Pick<
|
||||
SalesPaymentsRepository,
|
||||
| 'list'
|
||||
| 'findById'
|
||||
| 'create'
|
||||
| 'createMany'
|
||||
| 'update'
|
||||
| 'updateStatus'
|
||||
| 'bulkUpdateStatus'
|
||||
| 'delete'
|
||||
| 'bulkDelete'
|
||||
>
|
||||
> = {
|
||||
list: jest.fn(),
|
||||
findById: jest.fn(),
|
||||
create: jest.fn(),
|
||||
createMany: jest.fn(),
|
||||
update: jest.fn(),
|
||||
updateStatus: jest.fn(),
|
||||
bulkUpdateStatus: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
bulkDelete: jest.fn(),
|
||||
};
|
||||
const salesInvoicesService = {
|
||||
findById: jest.fn(),
|
||||
getTotals: jest.fn(),
|
||||
applyPaymentEffects: jest.fn(),
|
||||
};
|
||||
|
||||
const now = DateTime.fromUnixMs(1_700_000_000_000);
|
||||
const sample: SalesPayment = {
|
||||
id: 'sp-1',
|
||||
code: 'SP-20260824-0001',
|
||||
date: now,
|
||||
notes: null,
|
||||
invoices: [
|
||||
{
|
||||
id: 'alloc-1',
|
||||
invoiceId: 'si-1',
|
||||
amount: Decimal.create('10000'),
|
||||
},
|
||||
],
|
||||
images: [],
|
||||
status: Status.create('draft', SALES_PAYMENT_STATUSES),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
createdBy: 'user-1',
|
||||
updatedBy: 'user-1',
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
salesInvoicesService.findById.mockResolvedValue({ id: 'si-1' });
|
||||
const moduleRef: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
SalesPaymentsService,
|
||||
{ provide: SalesPaymentsRepository, useValue: repository },
|
||||
{ provide: SalesInvoicesService, useValue: salesInvoicesService },
|
||||
],
|
||||
}).compile();
|
||||
service = moduleRef.get(SalesPaymentsService);
|
||||
});
|
||||
|
||||
it('create persists invoice allocations', async () => {
|
||||
repository.create.mockResolvedValue(sample);
|
||||
const result = await service.create({
|
||||
date: '2026-08-24T10:00:00+07:00',
|
||||
invoices: [{ invoiceId: 'si-1', amount: '10000' }],
|
||||
userId: 'user-1',
|
||||
});
|
||||
expect(result.code).toBe('SP-20260824-0001');
|
||||
expect(repository.create).toHaveBeenCalled();
|
||||
const [arg] = repository.create.mock.calls[0];
|
||||
expect(arg.invoices[0]?.invoiceId).toBe('si-1');
|
||||
expect(arg.invoices[0]?.amount.value).toBe('10000.0000');
|
||||
});
|
||||
|
||||
it('update rejects status on PATCH', async () => {
|
||||
await expect(
|
||||
service.update('sp-1', { status: 'approved', userId: 'user-1' }),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
|
||||
it('findById throws when missing', async () => {
|
||||
repository.findById.mockResolvedValue(null);
|
||||
await expect(service.findById('missing')).rejects.toBeInstanceOf(
|
||||
NotFoundException,
|
||||
);
|
||||
});
|
||||
|
||||
it('approving a payment recomputes referenced invoices', async () => {
|
||||
repository.findById.mockResolvedValue(sample);
|
||||
salesInvoicesService.getTotals.mockResolvedValue({
|
||||
total: Decimal.create('25000'),
|
||||
paid: Decimal.create('0'),
|
||||
balance: Decimal.create('25000'),
|
||||
});
|
||||
repository.updateStatus.mockResolvedValue({
|
||||
...sample,
|
||||
status: Status.create('approved', SALES_PAYMENT_STATUSES),
|
||||
});
|
||||
await service.updateStatus('sp-1', 'approved', 'user-1');
|
||||
expect(salesInvoicesService.applyPaymentEffects).toHaveBeenCalledWith(
|
||||
'si-1',
|
||||
'user-1',
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects approve when allocation would exceed the invoice total', async () => {
|
||||
repository.findById.mockResolvedValue(sample);
|
||||
salesInvoicesService.getTotals.mockResolvedValue({
|
||||
total: Decimal.create('5000'),
|
||||
paid: Decimal.create('0'),
|
||||
balance: Decimal.create('5000'),
|
||||
});
|
||||
await expect(
|
||||
service.updateStatus('sp-1', 'approved', 'user-1'),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(repository.updateStatus).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user