diff --git a/apps/edr-freight-api/src/modules/bookings/consolidation-approval.service.spec.ts b/apps/edr-freight-api/src/modules/bookings/consolidation-approval.service.spec.ts
index d418fbd3c..5f121f630 100644
--- a/apps/edr-freight-api/src/modules/bookings/consolidation-approval.service.spec.ts
+++ b/apps/edr-freight-api/src/modules/bookings/consolidation-approval.service.spec.ts
@@ -11,9 +11,8 @@ import { Booking } from './entities/booking.entity';
* straight to Operations.
*
* The invariants that matter: both halves are held and released TOGETHER (a
- * decision on one side of a shared wagon is meaningless without the other), the
- * requester cannot approve their own pairing, and a decided pairing cannot be
- * decided twice.
+ * decision on one side of a shared wagon is meaningless without the other), and
+ * a decided pairing cannot be decided twice.
*/
describe('ConsolidationApprovalService', () => {
const PENDING = {
@@ -154,13 +153,19 @@ describe('ConsolidationApprovalService', () => {
});
});
- it('refuses to let the requester approve their own pairing', async () => {
- const { service, bookingsRepository } = makeService();
+ it('lets the requester approve their own pairing', async () => {
+ // No maker-checker separation: the permission alone decides who may approve,
+ // and the audit trail still records requester and approver separately.
+ const { service, approvals } = makeService();
- await expect(
- service.approve('ap-1', 'gl-user'),
- ).rejects.toThrow(/must be approved by someone else/i);
- expect(bookingsRepository.update).not.toHaveBeenCalled();
+ await service.approve('ap-1', 'gl-user');
+
+ expect(approvals.decide).toHaveBeenCalledWith(
+ 'ap-1',
+ ConsolidationApprovalStatus.Approved,
+ 'gl-user',
+ undefined,
+ );
});
it('requires a reason to reject', async () => {
diff --git a/apps/edr-freight-api/src/modules/bookings/consolidation-approval.service.ts b/apps/edr-freight-api/src/modules/bookings/consolidation-approval.service.ts
index ed8bdfe7a..ab4f4891d 100644
--- a/apps/edr-freight-api/src/modules/bookings/consolidation-approval.service.ts
+++ b/apps/edr-freight-api/src/modules/bookings/consolidation-approval.service.ts
@@ -37,6 +37,10 @@ export const CONSOLIDATION_APPROVAL_PENDING = "CONSOLIDATION_APPROVAL_PENDING";
* decision on one is meaningless without the other. Every request is kept,
* decided or not: the table is the audit trail of who approved which pairing,
* when, and why.
+ *
+ * No maker-checker separation: whoever holds the approve permission may decide a
+ * pairing, including the GL user who created it. The record of who requested and
+ * who decided is still kept either way.
*/
@Injectable()
export class ConsolidationApprovalService {
@@ -114,7 +118,6 @@ export class ConsolidationApprovalService {
note?: string,
): Promise<{ booking: Booking; partner: Booking }> {
const approval = await this.loadPending(approvalId);
- this.assertDifferentPerson(approval, decidedBy);
await this.dataSource.transaction(async () => {
const claimed = await this.approvals.decide(
@@ -166,7 +169,6 @@ export class ConsolidationApprovalService {
);
}
const approval = await this.loadPending(approvalId);
- this.assertDifferentPerson(approval, decidedBy);
await this.dataSource.transaction(async () => {
const claimed = await this.approvals.decide(
@@ -237,19 +239,4 @@ export class ConsolidationApprovalService {
}
return approval;
}
-
- /**
- * Maker–checker: the point of the gate is a second pair of eyes, so the GL
- * user who created the pairing cannot also approve it.
- */
- private assertDifferentPerson(
- approval: ConsolidationApproval,
- decidedBy: string,
- ): void {
- if (approval.requestedBy && approval.requestedBy === decidedBy) {
- throw new BadRequestException(
- "You created this consolidation — it must be approved by someone else.",
- );
- }
- }
}
diff --git a/apps/edr-freight-web/backoffice/src/components/layout/sidebar-sections.tsx b/apps/edr-freight-web/backoffice/src/components/layout/sidebar-sections.tsx
index a69d1d97f..fc5b90dbf 100644
--- a/apps/edr-freight-web/backoffice/src/components/layout/sidebar-sections.tsx
+++ b/apps/edr-freight-web/backoffice/src/components/layout/sidebar-sections.tsx
@@ -12,6 +12,7 @@ import {
Image as ImageIcon,
LayoutDashboard,
LayoutGrid,
+ Link2,
MapPin,
Network,
Package,
@@ -96,6 +97,14 @@ export const buildSidebarSections = (
icon: ,
permission: FREIGHT_PERMS.bookings.view,
},
+ // Shared-wagon gate: a consolidated pair waits for a human decision
+ // before either half reaches Operations.
+ {
+ label: "Shared wagon approvals",
+ href: "/dashboard/consolidation-approvals",
+ icon: ,
+ permission: FREIGHT_PERMS.bookings.approveConsolidation,
+ },
{
label: "Wagon cancellations",
href: "/dashboard/wagon-cancellations",