mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 12:28:21 +00:00
enhance contract and booking services with server-side search and validation improvements
- Added parameter to and for server-side free-text search on contract reference, company name, and booking details. - Introduced new validation errors in for container clashes and space issues when creating bookings. - Implemented paginated dropdown settings retrieval in . - Updated to fetch active yards using a new method that handles pagination. - Enhanced with a method to fetch all records by walking through pages. - Refactored to support filtering and pagination in schedule listings. - Improved to return a paginated list of facilities. - Updated UI components in and to utilize debounced search inputs for better performance. - Added alerts in to inform users about booking constraints related to splits and capacity. - Enhanced to display notifications for split bookings and capacity usage.
This commit is contained in:
@@ -9,7 +9,6 @@ import { BillingService } from '../billing/billing.service';
|
||||
import { Booking } from '../bookings/entities/booking.entity';
|
||||
import { BookingContainer } from '../bookings/entities/booking-container.entity';
|
||||
import { BookingContainerUnit } from '../bookings/entities/booking-container-unit.entity';
|
||||
import { Contract } from '../contracts/entities/contract.entity';
|
||||
import {
|
||||
BookingBatchOffer,
|
||||
OfferedLine,
|
||||
@@ -34,11 +33,14 @@ export interface SizedOffer {
|
||||
* GENERAL and ONE_TIME commercial bookings are offered partials: the remainder
|
||||
* returns to the contract's quantity cap (derived live from booking_container
|
||||
* rows, so reducing the lines releases it automatically) and can be rebooked in
|
||||
* any later window within contract validity. A ONE_TIME contract is promoted to
|
||||
* GENERAL on split (see applySplit) so its remainder is actually rebookable.
|
||||
* Once the remainder is rebooked and the cap hits zero, ContractBookingService
|
||||
* completes the contract (CONTRACT_CLOSED): no further bookings or shipment
|
||||
* requests, even while validity and a booking window are still open.
|
||||
* any later window within contract validity. The reduced booking is flagged
|
||||
* is_split (see applySplit); the contract kind never changes. On a ONE_TIME
|
||||
* contract a split booking releases the single-active-booking slot, but the
|
||||
* next booking must take the WHOLE remainder — the split chain is the only way
|
||||
* a ONE_TIME contract produces multiple bookings. Once the remainder is
|
||||
* rebooked and the cap hits zero, ContractBookingService completes the
|
||||
* contract (CONTRACT_CLOSED): no further bookings or shipment requests, even
|
||||
* while validity and a booking window are still open.
|
||||
*/
|
||||
@Injectable()
|
||||
export class BookingSplitService {
|
||||
@@ -215,11 +217,26 @@ export class BookingSplitService {
|
||||
if (!offer) return;
|
||||
|
||||
await this.dataSource.transaction(async (manager) => {
|
||||
// Snapshot what the booking carried BEFORE the reduction: on a ONE_TIME
|
||||
// contract this is the ledger the outstanding remainder is derived from
|
||||
// (there is no contract quantity cap to fall back on).
|
||||
const preSplit = await manager.getRepository(Booking).findOne({
|
||||
where: { id: bookingId },
|
||||
select: { id: true, cargoTotalWeightVgm: true },
|
||||
});
|
||||
const preSplitQuantities: { bulkTons?: number; bySize?: Record<string, number> } = {};
|
||||
|
||||
if (offer.offeredLines?.length) {
|
||||
const keptByLine = new Map(offer.offeredLines.map((l) => [l.bookingContainerId, l]));
|
||||
const lines = await manager.getRepository(BookingContainer).find({
|
||||
where: { bookingId },
|
||||
});
|
||||
const bySize: Record<string, number> = {};
|
||||
for (const line of lines) {
|
||||
const size = line.containerSize ?? '';
|
||||
bySize[size] = (bySize[size] ?? 0) + Number(line.quantity ?? 0);
|
||||
}
|
||||
preSplitQuantities.bySize = bySize;
|
||||
for (const line of lines) {
|
||||
const kept = keptByLine.get(line.id);
|
||||
if (!kept) {
|
||||
@@ -251,34 +268,24 @@ export class BookingSplitService {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
preSplitQuantities.bulkTons = Number(preSplit?.cargoTotalWeightVgm ?? 0);
|
||||
}
|
||||
|
||||
// is_split releases the ONE_TIME single-active-booking slot for the
|
||||
// remainder (whole-remainder-only, enforced at booking creation) and
|
||||
// switches the contract into remainder-based completion. The contract
|
||||
// kind is NOT changed: a ONE_TIME contract stays ONE_TIME through the
|
||||
// split chain.
|
||||
await manager.getRepository(Booking).update(bookingId, {
|
||||
wagonsRequired: offer.offeredWagons,
|
||||
cargoTotalWeightVgm: offer.offeredWeightTons,
|
||||
totalAmount: offer.offeredAmount,
|
||||
pricingBreakdown: offer.offeredPricingBreakdown,
|
||||
isSplit: true,
|
||||
preSplitQuantities,
|
||||
} as never);
|
||||
|
||||
// A ONE_TIME contract permits a single active booking, which would block the
|
||||
// split remainder from ever being rebooked. Promote the parent contract (and
|
||||
// the booking's denormalized copy) to GENERAL so the leftover quantity draws
|
||||
// down against the cap like any general contract, within the same validity.
|
||||
const booking = await manager.getRepository(Booking).findOne({
|
||||
where: { id: bookingId },
|
||||
select: { id: true, contractId: true, contractKind: true },
|
||||
});
|
||||
if (booking?.contractKind === 'ONE_TIME') {
|
||||
await manager
|
||||
.getRepository(Booking)
|
||||
.update(bookingId, { contractKind: 'GENERAL' } as never);
|
||||
if (booking.contractId) {
|
||||
await manager
|
||||
.getRepository(Contract)
|
||||
.update(booking.contractId, { contractKind: 'GENERAL' } as never);
|
||||
}
|
||||
}
|
||||
|
||||
await manager
|
||||
.getRepository(BookingBatchOffer)
|
||||
.update(offer.id, { status: 'APPLIED' });
|
||||
|
||||
Reference in New Issue
Block a user