feat: implement consolidation approval process for shared-wagon bookings

- Add migration for consolidation approvals table and status enum
- Create ConsolidationApprovalService to handle approval logic
- Implement repository for managing consolidation approvals
- Add entity for consolidation approval with necessary fields
- Develop frontend components for displaying and managing consolidation approvals
- Create tests for consolidation approval service to ensure correct behavior
This commit is contained in:
Marshal
2026-08-18 13:17:55 +00:00
parent 22a3fb98ee
commit 40904049cf
36 changed files with 1898 additions and 184 deletions

View File

@@ -50,6 +50,7 @@ import { BookingReferenceDataService } from './booking-reference-data.service';
import { scopedDirections } from '../user-trade-access/trade-scope.util';
import { UserTradeAccessService } from '../user-trade-access/user-trade-access.service';
import { BookingsService } from './bookings.service';
import { ConsolidationApprovalService } from './consolidation-approval.service';
import { BookingReferenceDataDto } from './dto/booking-reference-data.dto';
import { CreateBookingDto } from './dto/create-booking.dto';
import { BookingListSummaryDto } from './dto/booking-list-summary.dto';
@@ -58,8 +59,10 @@ import { GeneratePriceResponseDto } from './dto/generate-price-response.dto';
import { SubmitBookingResponseDto } from './dto/submit-booking-response.dto';
import {
AcceptIntakeDto,
ApproveConsolidationDto,
CancelBookingDto,
PairedDecisionDto,
RejectConsolidationDto,
RejectBookingDto,
RequestChangesDto,
ReviewDocumentDto,
@@ -166,6 +169,7 @@ export class BookingsController {
private readonly lastMileService: LastMileService,
private readonly userTradeAccessService: UserTradeAccessService,
private readonly wagonCancellationService: BookingWagonCancellationService,
private readonly consolidationApprovalService: ConsolidationApprovalService,
) {}
@Post()
@@ -1542,6 +1546,67 @@ export class BookingsController {
return this.transitionService.enrichBookingResponse(booking);
}
// ── Shared-wagon (consolidation) approval gate ────────────────────────────
// A consolidated pair is held here, not in the operations queue: two
// customers' cargo on one wagon is a commercial call, so a person signs off
// on the pairing before Operations sees either half.
@Get("consolidation-approvals/queue")
@BookingStaff(FREIGHT_PERMS.bookings.approveConsolidation)
@ApiOperation({
summary:
"Shared-wagon pairings awaiting approval, oldest first. Each row covers BOTH bookings on the wagon.",
})
consolidationApprovalQueue() {
return this.consolidationApprovalService.queue();
}
@Get(":id/consolidation-approvals")
@BookingStaff(FREIGHT_PERMS.bookings.view)
@ApiOperation({
summary:
"Approval history for this booking's shared wagon — who decided what, when, and why.",
})
consolidationApprovalHistory(@Param("id", ParseUUIDPipe) id: string) {
return this.consolidationApprovalService.historyForBooking(id);
}
@Post("consolidation-approvals/:approvalId/approve")
@BookingStaff(FREIGHT_PERMS.bookings.approveConsolidation)
@ApiOperation({
summary:
"Approve a shared wagon: both bookings leave the gate and continue to Operations together.",
})
approveConsolidation(
@Param("approvalId", ParseUUIDPipe) approvalId: string,
@Body() dto: ApproveConsolidationDto,
@CurrentUser() user: AuthUserPayload,
) {
return this.consolidationApprovalService.approve(
approvalId,
resolveAuthUserId(user) ?? "",
dto.note,
);
}
@Post("consolidation-approvals/:approvalId/reject")
@BookingStaff(FREIGHT_PERMS.bookings.approveConsolidation)
@ApiOperation({
summary:
"Reject a shared wagon: both bookings go back to GL for changes with the reason.",
})
rejectConsolidation(
@Param("approvalId", ParseUUIDPipe) approvalId: string,
@Body() dto: RejectConsolidationDto,
@CurrentUser() user: AuthUserPayload,
) {
return this.consolidationApprovalService.reject(
approvalId,
resolveAuthUserId(user) ?? "",
dto.reason,
);
}
@Post(":id/paired-decision")
@BookingStaff(FREIGHT_PERMS.bookings.cancel)
@ApiOperation({