mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
add contract link
This commit is contained in:
@@ -28,6 +28,8 @@ describe("ConsolidationApprovalService", () => {
|
||||
approvals?: Partial<Record<string, jest.Mock>>;
|
||||
bookingsRepository?: Partial<Record<string, jest.Mock>>;
|
||||
bookingsService?: Partial<Record<string, jest.Mock>>;
|
||||
/** Contract rows the id→reference lookup should return. */
|
||||
contracts?: { id: string; reference: string }[];
|
||||
/** Yard ids the caller is scoped to; null = unrestricted. */
|
||||
yardScope?: string[] | null;
|
||||
} = {},
|
||||
@@ -68,8 +70,14 @@ describe("ConsolidationApprovalService", () => {
|
||||
consolidationRejectedToStaff: jest.fn(),
|
||||
operationRequestedToStaff: jest.fn(),
|
||||
};
|
||||
const contractRepo = {
|
||||
find: jest
|
||||
.fn()
|
||||
.mockResolvedValue(overrides.contracts ?? []),
|
||||
};
|
||||
const dataSource = {
|
||||
transaction: jest.fn(async (cb: () => Promise<unknown>) => cb()),
|
||||
getRepository: jest.fn(() => contractRepo),
|
||||
};
|
||||
const yardScope = {
|
||||
getScopedYardIds: jest
|
||||
@@ -85,7 +93,14 @@ describe("ConsolidationApprovalService", () => {
|
||||
dataSource as never,
|
||||
yardScope as never,
|
||||
);
|
||||
return { service, approvals, bookingsRepository, notifier, yardScope };
|
||||
return {
|
||||
service,
|
||||
approvals,
|
||||
bookingsRepository,
|
||||
notifier,
|
||||
yardScope,
|
||||
contractRepo,
|
||||
};
|
||||
}
|
||||
|
||||
it("holds BOTH halves at the gate when a pairing is created", async () => {
|
||||
@@ -416,4 +431,51 @@ describe("ConsolidationApprovalService", () => {
|
||||
status: "OPERATION_REQUEST_PENDING",
|
||||
});
|
||||
});
|
||||
|
||||
it("attaches each half's contract reference for the queue link", async () => {
|
||||
// Booking has no contract relation (contract–booking split), so the
|
||||
// references are batch-loaded by id — one query for the whole page.
|
||||
const { service, contractRepo } = makeService({
|
||||
approvals: {
|
||||
findQueuePage: jest.fn().mockResolvedValue({
|
||||
items: [
|
||||
{
|
||||
...PENDING,
|
||||
booking: { id: "b-1", contractId: "c-1" },
|
||||
partnerBooking: { id: "b-2", contractId: "c-2" },
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
}),
|
||||
},
|
||||
contracts: [
|
||||
{ id: "c-1", reference: "CT-001" },
|
||||
{ id: "c-2", reference: "CT-002" },
|
||||
],
|
||||
});
|
||||
|
||||
const { items } = await service.queue();
|
||||
|
||||
expect(items[0].contractReference).toBe("CT-001");
|
||||
expect(items[0].partnerContractReference).toBe("CT-002");
|
||||
expect(contractRepo.find).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("leaves the contract reference null when a half has no contract", async () => {
|
||||
const { service, contractRepo } = makeService({
|
||||
approvals: {
|
||||
findQueuePage: jest.fn().mockResolvedValue({
|
||||
items: [{ ...PENDING, booking: { id: "b-1" }, partnerBooking: null }],
|
||||
total: 1,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const { items } = await service.queue();
|
||||
|
||||
expect(items[0].contractReference).toBeNull();
|
||||
expect(items[0].partnerContractReference).toBeNull();
|
||||
// Nothing to look up — no query at all.
|
||||
expect(contractRepo.find).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
NotFoundException,
|
||||
forwardRef,
|
||||
} from "@nestjs/common";
|
||||
import { DataSource } from "typeorm";
|
||||
import { DataSource, In } from "typeorm";
|
||||
|
||||
import { Booking } from "./entities/booking.entity";
|
||||
import {
|
||||
@@ -20,6 +20,7 @@ import { BookingsRepository } from "./bookings.repository";
|
||||
import { BookingsService } from "./bookings.service";
|
||||
import { BookingLifecycleNotifierService } from "./booking-lifecycle-notifier.service";
|
||||
import { YardScopeService } from "../rule-engine/services/yard-scope.service";
|
||||
import { Contract } from "../contracts/entities/contract.entity";
|
||||
|
||||
/** Where a rejected pair goes back to, so GL can fix and resubmit. */
|
||||
const REJECTED_STATUS = "OPERATION_CHANGES_REQUESTED";
|
||||
@@ -31,6 +32,9 @@ export const CONSOLIDATION_APPROVAL_PENDING = "CONSOLIDATION_APPROVAL_PENDING";
|
||||
export type ConsolidationApprovalView = ConsolidationApproval & {
|
||||
requestedByName: string | null;
|
||||
decidedByName: string | null;
|
||||
/** Contract the booking half was created under — reviewers work by contract. */
|
||||
contractReference: string | null;
|
||||
partnerContractReference: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -281,12 +285,18 @@ export class ConsolidationApprovalService {
|
||||
const names = await this.bookingsRepository.resolveStaffNames(
|
||||
rows.flatMap((r) => [r.requestedBy, r.decidedBy]),
|
||||
);
|
||||
const contractRefs = await this.contractReferences(rows);
|
||||
const refOf = (contractId?: string | null) =>
|
||||
contractId ? (contractRefs.get(contractId) ?? null) : null;
|
||||
|
||||
const items = rows.map((row) => ({
|
||||
...row,
|
||||
requestedByName: row.requestedBy
|
||||
? (names.get(row.requestedBy) ?? null)
|
||||
: null,
|
||||
decidedByName: row.decidedBy ? (names.get(row.decidedBy) ?? null) : null,
|
||||
contractReference: refOf(row.booking?.contractId),
|
||||
partnerContractReference: refOf(row.partnerBooking?.contractId),
|
||||
}));
|
||||
|
||||
const totalPages = Math.ceil(total / pageSize);
|
||||
@@ -305,6 +315,32 @@ export class ConsolidationApprovalService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Contract id → reference for the bookings on this page.
|
||||
*
|
||||
* Booking has no contract relation (contract–booking split), so the
|
||||
* references are batch-loaded by id rather than joined — one query per page,
|
||||
* not one per row.
|
||||
*/
|
||||
private async contractReferences(
|
||||
rows: ConsolidationApproval[],
|
||||
): Promise<Map<string, string>> {
|
||||
const ids = [
|
||||
...new Set(
|
||||
rows
|
||||
.flatMap((r) => [r.booking?.contractId, r.partnerBooking?.contractId])
|
||||
.filter((id): id is string => !!id),
|
||||
),
|
||||
];
|
||||
if (!ids.length) return new Map();
|
||||
|
||||
const contracts = await this.dataSource.getRepository(Contract).find({
|
||||
where: { id: In(ids) },
|
||||
select: { id: true, reference: true },
|
||||
});
|
||||
return new Map(contracts.map((c) => [c.id, c.reference]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Yard ids the caller may see, or undefined for unrestricted.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user