///|
/// A communication packet groups all affected shipments for one recipient.
/// It is a draft for human review, never an automatically sent notice.
pub(all) struct ConsigneePacket {
consignee : String
shipment_ids : Array[String]
lot_ids : Array[String]
references : Array[String]
total_quantity_g : Int64
earliest_shipment_at : Int64
latest_shipment_at : Int64
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) struct ConsigneeReport {
recall_id : String
as_of : Int64
packets : Array[ConsigneePacket]
shipment_count : Int32
total_quantity_g : Int64
} derive(Eq, Debug, ToJson, FromJson)
///|
fn has_text(items : Array[String], value : String) -> Bool {
for item in items {
if item == value {
return true
}
}
false
}
///|
/// Produce one packet per consignee in first-shipment order. A repeated lot
/// or invoice is listed once, while the quantity sums every distinct shipment.
pub fn group_consignees(
ledger : Ledger,
request : RecallRequest,
) -> Result[ConsigneeReport, ValidationReport] {
let impact = match analyze(ledger, request) {
Err(validation) => return Err(validation)
Ok(value) => value
}
let positions : Map[String, Int32] = Map([])
let packets : Array[ConsigneePacket] = []
let mut total_quantity_g = 0L
for shipment in impact.affected_shipments {
let position = match positions.get(shipment.consignee) {
Some(index) => index
None => {
let index = packets.length()
positions[shipment.consignee] = index
packets.push({
consignee: shipment.consignee,
shipment_ids: [],
lot_ids: [],
references: [],
total_quantity_g: 0L,
earliest_shipment_at: shipment.shipped_at,
latest_shipment_at: shipment.shipped_at,
})
index
}
}
let packet = packets[position]
packet.shipment_ids.push(shipment.shipment_id)
if !has_text(packet.lot_ids, shipment.lot_id) {
packet.lot_ids.push(shipment.lot_id)
}
if !has_text(packet.references, shipment.reference) {
packet.references.push(shipment.reference)
}
let earliest = if shipment.shipped_at < packet.earliest_shipment_at {
shipment.shipped_at
} else {
packet.earliest_shipment_at
}
let latest = if shipment.shipped_at > packet.latest_shipment_at {
shipment.shipped_at
} else {
packet.latest_shipment_at
}
packets[position] = {
..packet,
total_quantity_g: packet.total_quantity_g + shipment.quantity_g,
earliest_shipment_at: earliest,
latest_shipment_at: latest,
}
total_quantity_g += shipment.quantity_g
}
Ok({
recall_id: request.id,
as_of: request.as_of,
packets,
shipment_count: impact.affected_shipments.length(),
total_quantity_g,
})
}
///|
pub fn render_consignees_markdown(report : ConsigneeReport) -> String {
let out = StringBuilder()
out.write_string(
"# Consignee review packets: \{markdown_cell(report.recall_id)}\n\n",
)
out.write_string("Cutoff (Unix seconds): \{report.as_of}\n\n")
out.write_string(
"One packet per consignee. Verify identity and contact details before sending any notice.\n\n",
)
out.write_string(
"| Consignee | Shipments | Lots | References | Quantity (g) | First dispatch | Last dispatch |\n",
)
out.write_string("| --- | --- | --- | --- | ---: | ---: | ---: |\n")
for packet in report.packets {
out.write_string(
"| \{markdown_cell(packet.consignee)} | \{markdown_cell(packet.shipment_ids.join(", "))} | \{markdown_cell(packet.lot_ids.join(", "))} | \{markdown_cell(packet.references.join(", "))} | \{packet.total_quantity_g} | \{packet.earliest_shipment_at} | \{packet.latest_shipment_at} |\n",
)
}
out.write_string(
"\n\{report.packets.length()} consignees, \{report.shipment_count} shipments, \{report.total_quantity_g} g for review.\n",
)
out.to_string()
}
///|
/// A quoted CSV can be joined to the operator's own contact directory.
pub fn render_consignees_csv(report : ConsigneeReport) -> String {
let out = StringBuilder()
out.write_string(
"recall_id,consignee,shipment_ids,lot_ids,references,total_quantity_g,earliest_shipment_at,latest_shipment_at\n",
)
for packet in report.packets {
out.write_string(
"\{csv_cell(report.recall_id)},\{csv_cell(packet.consignee)},\{csv_cell(packet.shipment_ids.join(";"))},\{csv_cell(packet.lot_ids.join(";"))},\{csv_cell(packet.references.join(";"))},\{packet.total_quantity_g},\{packet.earliest_shipment_at},\{packet.latest_shipment_at}\n",
)
}
out.to_string()
}