Skip to content

Commit e197a88

Browse files
committed
Refactor: Update constraints structure in supplier reports and enhance HTML rendering
1 parent 647e7d3 commit e197a88

3 files changed

Lines changed: 110 additions & 51 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# CLI Supplier Reports - instructions for agents
2+
3+
* These reports are for suppliers and should contain just the details needed for implementing a pack.
4+
* Supplier Pack ID is not required in the report since that is our internal mapping of the pack to a supplier

packages/cli-supplier-reports/src/__tests__/supplier-report.test.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,14 @@ const createMockData = (): ParseResult => ({
3737
printColour: "COLOUR",
3838
},
3939
constraints: {
40-
deliveryDays: 2,
41-
maxSheets: 10,
40+
deliveryDays: {
41+
value: 2,
42+
operator: "LESS_THAN" as const,
43+
},
44+
sheets: {
45+
value: 10,
46+
operator: "LESS_THAN" as const,
47+
},
4248
},
4349
createdAt: "2024-01-01T00:00:00Z",
4450
id: "pack-std-2day" as any,
@@ -198,8 +204,8 @@ describe("supplier-report", () => {
198204
expect(html).toContain("STANDARD");
199205

200206
// Check constraints
201-
expect(html).toContain("Max Sheets");
202-
expect(html).toContain("10");
207+
expect(html).toContain("Sheets");
208+
expect(html).toContain("< 10");
203209
expect(html).toContain("Delivery Days");
204210

205211
// Check assembly details
@@ -489,10 +495,11 @@ describe("supplier-report", () => {
489495
// Should have 2 packs (APPROVED and SUBMITTED) but not the DRAFT
490496
expect(printcoReport!.packCount).toBe(2);
491497

492-
// Read and verify HTML content doesn't contain draft pack ID
498+
// Read and verify HTML content doesn't contain draft approval status
493499
// eslint-disable-next-line security/detect-non-literal-fs-filename
494500
const html = fs.readFileSync(printcoReport!.filePath, "utf8");
495-
expect(html).not.toContain("sp-printco-draft");
501+
// Check that DRAFT approval status badge is not present
502+
expect(html).not.toContain('approval-status status-draft');
496503
});
497504

498505
it("includes draft supplier packs when excludeDrafts option is false or not provided", () => {
@@ -517,10 +524,12 @@ describe("supplier-report", () => {
517524
// Should have 3 packs (APPROVED, SUBMITTED, and DRAFT)
518525
expect(printcoReport!.packCount).toBe(3);
519526

520-
// Read and verify HTML content contains draft pack ID
527+
// Read and verify HTML content contains draft approval status
521528
// eslint-disable-next-line security/detect-non-literal-fs-filename
522529
const html = fs.readFileSync(printcoReport!.filePath, "utf8");
523-
expect(html).toContain("sp-printco-draft");
530+
// Check for DRAFT approval status badge
531+
expect(html).toContain('approval-status status-draft');
532+
expect(html).toContain('>DRAFT</span>');
524533
});
525534

526535
it("includes pack specification description in report when present", () => {

packages/cli-supplier-reports/src/supplier-report.ts

Lines changed: 89 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ import * as fs from "node:fs";
22
import path from "node:path";
33
import {
44
$PackSpecification,
5-
$Paper,
6-
$Postage,
75
PackSpecification,
86
} from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/domain/pack-specification";
7+
import { $Postage } from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/domain/postage";
8+
import { $Paper } from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/domain/paper";
9+
import { Constraint } from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/domain/constraint";
910
import { Supplier } from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/domain/supplier";
1011
import { SupplierAllocation } from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/domain/supplier-allocation";
1112
import { SupplierPack } from "@nhsdigital/nhs-notify-event-schemas-supplier-config/src/domain/supplier-pack";
@@ -47,6 +48,19 @@ function formatValue(value: unknown): string {
4748
return escapeHtml(String(value));
4849
}
4950

51+
function formatConstraint(constraint: Constraint | undefined): string {
52+
if (!constraint) return "<em>Not specified</em>";
53+
const operatorSymbols: Record<string, string> = {
54+
EQUALS: "=",
55+
NOT_EQUALS: "≠",
56+
GREATER_THAN: "&gt;",
57+
LESS_THAN: "&lt;",
58+
};
59+
const symbol =
60+
operatorSymbols[constraint.operator] || escapeHtml(constraint.operator);
61+
return `${symbol} ${escapeHtml(constraint.value)}`;
62+
}
63+
5064
function sanitizeAnchorId(text: string): string {
5165
return text
5266
.toLowerCase()
@@ -87,43 +101,49 @@ function getPackSpecificationStatusTooltip(status: string): string {
87101
return tooltips[status] || "";
88102
}
89103

90-
function renderOptionalRow(
91-
label: string,
92-
value: unknown,
93-
formatter: (v: unknown) => string = (v) => escapeHtml(String(v)),
94-
tooltip?: string,
95-
): string {
96-
if (value === undefined || value === null) return "";
97-
const tooltipAttr = tooltip
98-
? ` data-tooltip="${escapeHtml(tooltip)}" class="has-tooltip"`
99-
: "";
100-
return `<tr><th${tooltipAttr}>${label}</th><td>${formatter(value)}</td></tr>`;
101-
}
102-
103104
function renderConstraintsSection(
104105
constraints: PackSpecification["constraints"],
105106
): string {
106107
// Get the Constraints schema from PackSpecification
107108
const constraintsSchema = $PackSpecification.shape.constraints;
108-
const unwrapped = constraintsSchema.unwrap().shape;
109-
110-
const maxSheetsTooltip = unwrapped.maxSheets.meta()?.description;
111-
const deliveryDaysTooltip = unwrapped.deliveryDays.meta()?.description;
112-
const blackCoverageTooltip = unwrapped.blackCoveragePercentage.meta()?.description;
113-
const colourCoverageTooltip = unwrapped.colourCoveragePercentage.meta()?.description;
114-
115-
const maxSheetsHeader = maxSheetsTooltip ? ` data-tooltip="${escapeHtml(maxSheetsTooltip)}" class="has-tooltip"` : "";
116-
const deliveryDaysHeader = deliveryDaysTooltip ? ` data-tooltip="${escapeHtml(deliveryDaysTooltip)}" class="has-tooltip"` : "";
117-
const blackCoverageHeader = blackCoverageTooltip ? ` data-tooltip="${escapeHtml(blackCoverageTooltip)}" class="has-tooltip"` : "";
118-
const colourCoverageHeader = colourCoverageTooltip ? ` data-tooltip="${escapeHtml(colourCoverageTooltip)}" class="has-tooltip"` : "";
109+
const unwrapped = constraintsSchema.unwrap();
110+
111+
const sheetsTooltip = unwrapped.shape.sheets.unwrap().meta()?.description;
112+
const sidesTooltip = unwrapped.shape.sides.unwrap().meta()?.description;
113+
const deliveryDaysTooltip = unwrapped.shape.deliveryDays
114+
.unwrap()
115+
.meta()?.description;
116+
const blackCoverageTooltip = unwrapped.shape.blackCoveragePercentage
117+
.unwrap()
118+
.meta()?.description;
119+
const colourCoverageTooltip = unwrapped.shape.colourCoveragePercentage
120+
.unwrap()
121+
.meta()?.description;
122+
123+
const sheetsHeader = sheetsTooltip
124+
? ` data-tooltip="${escapeHtml(sheetsTooltip)}" class="has-tooltip"`
125+
: "";
126+
const sidesHeader = sidesTooltip
127+
? ` data-tooltip="${escapeHtml(sidesTooltip)}" class="has-tooltip"`
128+
: "";
129+
const deliveryDaysHeader = deliveryDaysTooltip
130+
? ` data-tooltip="${escapeHtml(deliveryDaysTooltip)}" class="has-tooltip"`
131+
: "";
132+
const blackCoverageHeader = blackCoverageTooltip
133+
? ` data-tooltip="${escapeHtml(blackCoverageTooltip)}" class="has-tooltip"`
134+
: "";
135+
const colourCoverageHeader = colourCoverageTooltip
136+
? ` data-tooltip="${escapeHtml(colourCoverageTooltip)}" class="has-tooltip"`
137+
: "";
119138

120139
return `
121140
<h4>Constraints</h4>
122141
<table class="details-table">
123-
<tr><th${maxSheetsHeader}>Max Sheets</th><td>${constraints?.maxSheets !== undefined ? escapeHtml(constraints.maxSheets) : "<em>Not specified</em>"}</td></tr>
124-
<tr><th${deliveryDaysHeader}>Delivery Days</th><td>${constraints?.deliveryDays !== undefined ? escapeHtml(constraints.deliveryDays) : "<em>Not specified</em>"}</td></tr>
125-
<tr><th${blackCoverageHeader}>Black Coverage (%)</th><td>${constraints?.blackCoveragePercentage !== undefined ? escapeHtml(constraints.blackCoveragePercentage) : "<em>Not specified</em>"}</td></tr>
126-
<tr><th${colourCoverageHeader}>Colour Coverage (%)</th><td>${constraints?.colourCoveragePercentage !== undefined ? escapeHtml(constraints.colourCoveragePercentage) : "<em>Not specified</em>"}</td></tr>
142+
<tr><th${sheetsHeader}>Sheets</th><td>${formatConstraint(constraints?.sheets)}</td></tr>
143+
<tr><th${sidesHeader}>Sides</th><td>${formatConstraint(constraints?.sides)}</td></tr>
144+
<tr><th${deliveryDaysHeader}>Delivery Days</th><td>${formatConstraint(constraints?.deliveryDays)}</td></tr>
145+
<tr><th${blackCoverageHeader}>Black Coverage (%)</th><td>${formatConstraint(constraints?.blackCoveragePercentage)}</td></tr>
146+
<tr><th${colourCoverageHeader}>Colour Coverage (%)</th><td>${formatConstraint(constraints?.colourCoveragePercentage)}</td></tr>
127147
</table>
128148
`;
129149
}
@@ -168,12 +188,29 @@ function renderPaperSection(
168188
function renderAssemblySection(
169189
assembly: PackSpecification["assembly"],
170190
): string {
171-
const envelopeId = assembly?.envelopeId ? escapeHtml(assembly.envelopeId) : "<em>Not specified</em>";
172-
const printColour = assembly?.printColour ? escapeHtml(assembly.printColour) : "<em>Not specified</em>";
173-
const duplex = assembly?.duplex !== undefined ? (assembly.duplex ? "Yes" : "No") : "<em>Not specified</em>";
174-
const features = assembly?.features && assembly.features.length > 0 ? formatValue(assembly.features) : "<em>Not specified</em>";
175-
const insertIds = assembly?.insertIds && assembly.insertIds.length > 0 ? formatValue(assembly.insertIds) : "<em>Not specified</em>";
176-
const additional = assembly?.additional ? `<pre>${escapeHtml(JSON.stringify(assembly.additional, null, 2))}</pre>` : "<em>Not specified</em>";
191+
const envelopeId = assembly?.envelopeId
192+
? escapeHtml(assembly.envelopeId)
193+
: "<em>Not specified</em>";
194+
const printColour = assembly?.printColour
195+
? escapeHtml(assembly.printColour)
196+
: "<em>Not specified</em>";
197+
const duplex =
198+
assembly?.duplex === undefined
199+
? "<em>Not specified</em>"
200+
: assembly.duplex
201+
? "Yes"
202+
: "No";
203+
const features =
204+
assembly?.features && assembly.features.length > 0
205+
? formatValue(assembly.features)
206+
: "<em>Not specified</em>";
207+
const insertIds =
208+
assembly?.insertIds && assembly.insertIds.length > 0
209+
? formatValue(assembly.insertIds)
210+
: "<em>Not specified</em>";
211+
const additional = assembly?.additional
212+
? `<pre>${escapeHtml(JSON.stringify(assembly.additional, null, 2))}</pre>`
213+
: "<em>Not specified</em>";
177214

178215
return `
179216
<h4>Assembly</h4>
@@ -194,9 +231,15 @@ function generatePackDetailsHtml(pack: PackSpecification): string {
194231
const maxWeightTooltip = $Postage.shape.maxWeightGrams.meta()?.description;
195232
const maxThicknessTooltip = $Postage.shape.maxThicknessMm.meta()?.description;
196233

197-
const deliveryDaysHeader = deliveryDaysTooltip ? ` data-tooltip="${escapeHtml(deliveryDaysTooltip)}" class="has-tooltip"` : "";
198-
const maxWeightHeader = maxWeightTooltip ? ` data-tooltip="${escapeHtml(maxWeightTooltip)}" class="has-tooltip"` : "";
199-
const maxThicknessHeader = maxThicknessTooltip ? ` data-tooltip="${escapeHtml(maxThicknessTooltip)}" class="has-tooltip"` : "";
234+
const deliveryDaysHeader = deliveryDaysTooltip
235+
? ` data-tooltip="${escapeHtml(deliveryDaysTooltip)}" class="has-tooltip"`
236+
: "";
237+
const maxWeightHeader = maxWeightTooltip
238+
? ` data-tooltip="${escapeHtml(maxWeightTooltip)}" class="has-tooltip"`
239+
: "";
240+
const maxThicknessHeader = maxThicknessTooltip
241+
? ` data-tooltip="${escapeHtml(maxThicknessTooltip)}" class="has-tooltip"`
242+
: "";
200243

201244
const versionTooltip = $PackSpecification.shape.version.meta()?.description;
202245
const versionHeader = versionTooltip
@@ -222,9 +265,9 @@ function generatePackDetailsHtml(pack: PackSpecification): string {
222265
<table class="details-table">
223266
<tr><th>Postage ID</th><td>${escapeHtml(pack.postage.id)}</td></tr>
224267
<tr><th>Size</th><td>${escapeHtml(pack.postage.size)}</td></tr>
225-
<tr><th${deliveryDaysHeader}>Delivery Days</th><td>${pack.postage.deliveryDays !== undefined ? escapeHtml(pack.postage.deliveryDays) : "<em>Not specified</em>"}</td></tr>
226-
<tr><th${maxWeightHeader}>Max Weight (grams)</th><td>${pack.postage.maxWeightGrams !== undefined ? escapeHtml(pack.postage.maxWeightGrams) : "<em>Not specified</em>"}</td></tr>
227-
<tr><th${maxThicknessHeader}>Max Thickness (mm)</th><td>${pack.postage.maxThicknessMm !== undefined ? escapeHtml(pack.postage.maxThicknessMm) : "<em>Not specified</em>"}</td></tr>
268+
<tr><th${deliveryDaysHeader}>Delivery Days</th><td>${pack.postage.deliveryDays === undefined ? "<em>Not specified</em>" : escapeHtml(pack.postage.deliveryDays)}</td></tr>
269+
<tr><th${maxWeightHeader}>Max Weight (grams)</th><td>${pack.postage.maxWeightGrams === undefined ? "<em>Not specified</em>" : escapeHtml(pack.postage.maxWeightGrams)}</td></tr>
270+
<tr><th${maxThicknessHeader}>Max Thickness (mm)</th><td>${pack.postage.maxThicknessMm === undefined ? "<em>Not specified</em>" : escapeHtml(pack.postage.maxThicknessMm)}</td></tr>
228271
</table>
229272
230273
${renderConstraintsSection(pack.constraints)}
@@ -909,7 +952,10 @@ function generateSupplierHtml(report: SupplierReport): string {
909952
</html>`;
910953
}
911954

912-
function buildSupplierReports(data: ParseResult, options: GenerateReportsOptions = {}): Map<string, SupplierReport> {
955+
function buildSupplierReports(
956+
data: ParseResult,
957+
options: GenerateReportsOptions = {},
958+
): Map<string, SupplierReport> {
913959
const reports = new Map<string, SupplierReport>();
914960

915961
// Initialize reports for all suppliers

0 commit comments

Comments
 (0)