refactor contract handling to support single route per contract and improve reference generation logic

This commit is contained in:
Marshal
2026-07-06 08:42:44 +00:00
parent 1b92c57e23
commit 8336a5aff8
7 changed files with 113 additions and 31 deletions

View File

@@ -62,16 +62,23 @@ export class BookingsRepository extends BaseRepository<Booking> {
return this.repository.findOne({ where: { reference } });
}
/** Count bookings created in a specific year. */
async countByYear(year: number): Promise<number> {
const startDate = new Date(year, 0, 1);
const endDate = new Date(year + 1, 0, 1);
return this.repository
/**
* Highest NNNNNN sequence already issued for `BK-<year>-…` references.
* Includes soft-deleted bookings so the next number clears references that
* still occupy the unique index. (A created-at count drifts below the issued
* sequence after any delete and then collides forever.)
*/
async maxReferenceSequence(year: number): Promise<number> {
const row = await this.repository
.createQueryBuilder('booking')
.where('booking.created_at >= :startDate', { startDate })
.andWhere('booking.created_at < :endDate', { endDate })
.getCount();
.withDeleted()
.select(
"COALESCE(MAX(CAST(SUBSTRING(booking.reference FROM '[0-9]+$') AS int)), 0)",
'max',
)
.where('booking.reference LIKE :prefix', { prefix: `BK-${year}-%` })
.getRawOne<{ max: string | number | null }>();
return Number(row?.max ?? 0);
}
/** Find a booking by reference with files and relations. */

View File

@@ -9,6 +9,7 @@ import {
NotFoundException,
} from '@nestjs/common';
import { Freight, SchedulingStatus } from '@edr/types';
import { insertWithGeneratedReference } from '@edr/api-common';
// import { CustomersService } from '../customers/customers.service';
import { CompaniesService } from '../companies/companies.service';
import { ProfileType } from '../companies/entities/company-profile.entity';
@@ -207,8 +208,8 @@ export class BookingsService {
/** Generate a unique booking reference number. */
private async generateReference(): Promise<string> {
const year = new Date().getFullYear();
const count = await this.bookingsRepository.countByYear(year);
return `BK-${year}-${String(count + 1).padStart(6, '0')}`;
const seq = await this.bookingsRepository.maxReferenceSequence(year);
return `BK-${year}-${String(seq + 1).padStart(6, '0')}`;
}
private buildCustomerTruckFreightOrderHtml(
@@ -593,7 +594,6 @@ export class BookingsService {
}
}
const reference = dto.reference || (await this.generateReference());
const containers = dto.containers ?? [];
assertFreightShape({
freightType: dto.freightType,
@@ -688,7 +688,10 @@ export class BookingsService {
// the customer clears it themselves and may name their broker.
const includesCustoms = await this.resolveIncludesCustoms(dto.serviceTypeId);
const booking = await this.bookingsRepository.create({
// Explicit reference is caller-chosen (a collision is a real conflict);
// auto-generated references retry past a concurrent same-sequence insert.
const insertBooking = (reference: string) =>
this.bookingsRepository.create({
reference,
companyId,
companyProfileId,
@@ -743,7 +746,14 @@ export class BookingsService {
priorityScore: ruleResult.priorityScore,
totalAmount: 0,
paymentStatus: 'PENDING',
});
});
const booking = dto.reference
? await insertBooking(dto.reference)
: await insertWithGeneratedReference(
() => this.generateReference(),
insertBooking,
);
if (dto.freightType === 'CONTAINER') {
await this.bookingsRepository.createContainers(