Add address fields to sales invoices and implement related validations
- Introduced new columns `address`, `latitude`, and `longitude` in the `sales_invoices` table to store location details. - Updated the `SalesInvoicesService` to handle the new fields, including validation for address format and geographical coordinates. - Enhanced the `SalesInvoiceDto` and related data transfer objects to include the new fields for API requests and responses. - Added unit and e2e tests to ensure proper handling of the new fields and validate their integration within the sales invoice workflow. - Created a new migration script to apply the database schema changes for the sales invoices.
This commit is contained in:
@@ -16,6 +16,37 @@ import type { Plan } from './plan';
|
||||
import { PlansRepository } from './plans.repository';
|
||||
import { PlansService } from './plans.service';
|
||||
|
||||
const MS_PER_DAY = 86_400_000;
|
||||
|
||||
function todayYmd(): string {
|
||||
return DateTime.fromUnixMs(Math.trunc(Date.now()))
|
||||
.startOfDay()
|
||||
.format()
|
||||
.slice(0, 10);
|
||||
}
|
||||
|
||||
function nextWeekdayYmd(weekday: string): string {
|
||||
const start = DateTime.fromUnixMs(Math.trunc(Date.now())).startOfDay();
|
||||
for (let offset = 0; offset < 8; offset += 1) {
|
||||
const day = DateTime.fromUnixMs(
|
||||
start.value + offset * MS_PER_DAY,
|
||||
).startOfDay();
|
||||
if (day.weekdayName() === weekday) {
|
||||
return day.format().slice(0, 10);
|
||||
}
|
||||
}
|
||||
return todayYmd();
|
||||
}
|
||||
|
||||
function addDaysYmd(date: string, days: number): string {
|
||||
return DateTime.fromUnixMs(
|
||||
DateTime.create(date).startOfDay().value + days * MS_PER_DAY,
|
||||
)
|
||||
.startOfDay()
|
||||
.format()
|
||||
.slice(0, 10);
|
||||
}
|
||||
|
||||
describe('PlansService', () => {
|
||||
let service: PlansService;
|
||||
let plansRepository: jest.Mocked<
|
||||
@@ -170,7 +201,7 @@ describe('PlansService', () => {
|
||||
service.create({
|
||||
employeeId: 'emp-1',
|
||||
purpose: 'sales',
|
||||
date: '2026-01-05',
|
||||
date: todayYmd(),
|
||||
startBranchId: 'br-1',
|
||||
endBranchId: 'br-2',
|
||||
customerIds: ['cus-1'],
|
||||
@@ -180,15 +211,73 @@ describe('PlansService', () => {
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
|
||||
it('rejects a plan date in the past', async () => {
|
||||
await expect(
|
||||
service.create({
|
||||
employeeId: 'emp-1',
|
||||
purpose: 'sales',
|
||||
date: '2020-01-01',
|
||||
startBranchId: 'br-1',
|
||||
endBranchId: 'br-2',
|
||||
customerIds: ['cus-1'],
|
||||
userId: 'user-1',
|
||||
}),
|
||||
).rejects.toMatchObject({ message: 'Date cannot be in the past' });
|
||||
});
|
||||
|
||||
it('rejects invoices that do not belong to selected customers', async () => {
|
||||
salesInvoicesService.findById.mockResolvedValue({
|
||||
id: 'inv-1',
|
||||
customer: { id: 'cus-2', code: 'C2', name: 'Beta' },
|
||||
});
|
||||
await expect(
|
||||
service.create({
|
||||
employeeId: 'emp-1',
|
||||
purpose: 'sales',
|
||||
date: todayYmd(),
|
||||
startBranchId: 'br-1',
|
||||
endBranchId: 'br-2',
|
||||
customerIds: ['cus-1'],
|
||||
invoiceIds: ['inv-1'],
|
||||
userId: 'user-1',
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
message: 'Invoice does not belong to a selected customer',
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps an existing past date when the date is unchanged', async () => {
|
||||
plansRepository.findById.mockResolvedValue(plan);
|
||||
plansRepository.update.mockResolvedValue(plan);
|
||||
await service.update(
|
||||
'pln-1',
|
||||
{ date: '2026-01-05', userId: 'user-1' },
|
||||
user,
|
||||
);
|
||||
expect(plansRepository.update).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects changing a plan date into the past', async () => {
|
||||
plansRepository.findById.mockResolvedValue({
|
||||
...plan,
|
||||
date: DateTime.create(todayYmd()),
|
||||
});
|
||||
await expect(
|
||||
service.update('pln-1', { date: '2020-01-01', userId: 'user-1' }, user),
|
||||
).rejects.toMatchObject({ message: 'Date cannot be in the past' });
|
||||
});
|
||||
|
||||
it('generate copies a weekday and skips an existing plan', async () => {
|
||||
const from = nextWeekdayYmd('monday');
|
||||
const to = addDaysYmd(from, 7);
|
||||
plansRepository.findLiveByKey
|
||||
.mockResolvedValueOnce(null)
|
||||
.mockResolvedValueOnce(plan);
|
||||
const result = await service.generate({
|
||||
employeeId: 'emp-1',
|
||||
purpose: 'sales',
|
||||
from: '2026-01-05',
|
||||
to: '2026-01-12',
|
||||
from,
|
||||
to,
|
||||
userId: 'user-1',
|
||||
});
|
||||
expect(result.created).toBe(1);
|
||||
@@ -205,8 +294,8 @@ describe('PlansService', () => {
|
||||
service.generate({
|
||||
employeeId: 'emp-1',
|
||||
purpose: 'sales',
|
||||
from: '2026-01-05',
|
||||
to: '2026-01-05',
|
||||
from: todayYmd(),
|
||||
to: todayYmd(),
|
||||
userId: 'user-1',
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
Reference in New Issue
Block a user