- Introduced `SalesDocumentFlowService` to manage the lifecycle of sales documents, including packing slips and invoices. - Implemented methods for processing orders, generating invoices, and handling packing slips based on order status. - Updated `SalesOrdersService`, `PackingSlipsService`, and `SalesInvoicesService` to integrate with the new document flow service. - Added new methods for marking drafts processed and checking if invoices are on sales plans. - Enhanced existing services and repositories to support new functionalities, including status transitions and related document management. - Updated DTOs to include new fields for packing slip and invoice IDs in sales order responses. - Added unit tests for the new service and updated existing tests to cover new functionalities.
145 lines
4.6 KiB
TypeScript
145 lines
4.6 KiB
TypeScript
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',
|
|
invoice: { id: 'si-1', code: 'SI-1' },
|
|
amount: Decimal.create('10000'),
|
|
},
|
|
],
|
|
images: [],
|
|
status: Status.create('draft', SALES_PAYMENT_STATUSES),
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
createdBy: 'user-1',
|
|
updatedBy: 'user-1',
|
|
createdByUser: { id: 'user-1', username: 'admin' },
|
|
updatedByUser: { id: 'user-1', username: 'admin' },
|
|
};
|
|
|
|
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,
|
|
status: Status.create('pending', SALES_PAYMENT_STATUSES),
|
|
});
|
|
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,
|
|
status: Status.create('pending', SALES_PAYMENT_STATUSES),
|
|
});
|
|
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();
|
|
});
|
|
});
|