///|
/// A signed-off stock observation made at a physical location. The operator
/// supplies the count; this library never invents a measured quantity.
pub(all) struct StockCount {
lot_id : String
facility : String
counted_g : Int64
counted_at : Int64
reference : String
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) struct ReconciliationRow {
lot_id : String
facility : String
expected_g : Int64
counted_g : Int64
difference_g : Int64
status : String
count_reference : String
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) struct ReconciliationReport {
as_of : Int64
rows : Array[ReconciliationRow]
missing_counts : Array[String]
total_expected_g : Int64
total_counted_g : Int64
balanced : Bool
} derive(Eq, Debug, ToJson, FromJson)
///|
/// The CSV is deliberately strict: a wrong facility or duplicate lot could
/// otherwise make a shortage look like an exact count.
pub fn parse_stock_counts_csv(
text : String,
) -> Result[Array[StockCount], ValidationReport] {
let table = match parse_csv(text) {
Err(diagnostic) => return Err(report_from([diagnostic]))
Ok(value) => value
}
let expected = ["lot_id", "facility", "counted_g", "counted_at", "reference"]
if !table.has_exact_headers(expected) {
return Err(
report_from([
issue(
"error", "count-csv-schema", "counts.csv", "expected lot_id,facility,counted_g,counted_at,reference",
),
]),
)
}
let counts : Array[StockCount] = []
let findings : Array[Diagnostic] = []
for row in table.rows {
let amount = csv_int(
row.cells[2],
"counts.csv",
row.line,
"counted_g",
findings,
)
let time = csv_int(
row.cells[3],
"counts.csv",
row.line,
"counted_at",
findings,
)
match (amount, time) {
(Some(counted_g), Some(counted_at)) =>
counts.push({
lot_id: row.cells[0],
facility: row.cells[1],
counted_g,
counted_at,
reference: row.cells[4],
})
_ => ()
}
}
if !findings.is_empty() {
Err(report_from(findings))
} else {
Ok(counts)
}
}
///|
fn validate_stock_counts(
ledger : Ledger,
counts : Array[StockCount],
as_of : Int64,
) -> ValidationReport {
let findings : Array[Diagnostic] = []
let lots : Map[String, Lot] = Map([])
let seen : Map[String, Bool] = Map([])
for lot in ledger.lots {
lots[lot.id] = lot
}
if counts.length() > 100000 {
findings.push(
issue(
"error", "count-limit", "counts", "at most 100000 count rows are supported",
),
)
}
for count in counts {
if seen.contains(count.lot_id) {
findings.push(
issue(
"error",
"duplicate-count",
count.lot_id,
"lot has more than one physical count",
),
)
}
seen[count.lot_id] = true
if !valid_id(count.lot_id) {
findings.push(
issue(
"error",
"invalid-count-lot",
count.lot_id,
"lot id has an invalid shape",
),
)
}
if !valid_label(count.facility) {
findings.push(
issue(
"error",
"invalid-count-facility",
count.lot_id,
"facility is required",
),
)
}
if !valid_label(count.reference) {
findings.push(
issue(
"error",
"invalid-count-reference",
count.lot_id,
"a physical count reference is required",
),
)
}
if count.counted_g < 0L || count.counted_g > 1000000000000L {
findings.push(
issue(
"error",
"invalid-count-quantity",
count.lot_id,
"counted_g must be 0..1e12",
),
)
}
if !valid_time(count.counted_at) || count.counted_at != as_of {
findings.push(
issue(
"error",
"invalid-count-time",
count.lot_id,
"count timestamp must equal the comparison cutoff",
),
)
}
match lots.get(count.lot_id) {
None =>
findings.push(
issue(
"error",
"unknown-count-lot",
count.lot_id,
"counted lot is absent from ledger",
),
)
Some(lot) => {
if lot.facility != count.facility {
findings.push(
issue(
"error",
"count-facility-mismatch",
count.lot_id,
"physical count belongs to a different facility",
),
)
}
if lot.produced_at > count.counted_at {
findings.push(
issue(
"error",
"count-before-production",
count.lot_id,
"lot did not yet exist at count time",
),
)
}
}
}
}
report_from(findings)
}
///|
/// Compare a full physical count against reconstructed book stock at the
/// cutoff. Missing rows remain explicit; they never count as zero stock.
pub fn reconcile_stock(
ledger : Ledger,
counts : Array[StockCount],
as_of : Int64,
) -> Result[ReconciliationReport, ValidationReport] {
let book = match inventory_at(ledger, as_of) {
Err(validation) => return Err(validation)
Ok(value) => value
}
let validation = validate_stock_counts(ledger, counts, as_of)
if !validation.valid {
return Err(validation)
}
let by_lot : Map[String, StockCount] = Map([])
for count in counts {
by_lot[count.lot_id] = count
}
let rows : Array[ReconciliationRow] = []
let missing_counts : Array[String] = []
let mut expected_total = 0L
let mut counted_total = 0L
let mut balanced = true
for expected in book.rows {
expected_total += expected.on_hand_g
match by_lot.get(expected.lot_id) {
None => {
missing_counts.push(expected.lot_id)
balanced = false
}
Some(count) => {
let delta = count.counted_g - expected.on_hand_g
let status = if delta == 0L {
"matched"
} else if delta < 0L {
"shortage"
} else {
"surplus"
}
if delta != 0L {
balanced = false
}
counted_total += count.counted_g
rows.push({
lot_id: expected.lot_id,
facility: expected.facility,
expected_g: expected.on_hand_g,
counted_g: count.counted_g,
difference_g: delta,
status,
count_reference: count.reference,
})
}
}
}
Ok({
as_of,
rows,
missing_counts,
total_expected_g: expected_total,
total_counted_g: counted_total,
balanced,
})
}
///|
pub fn render_reconciliation_markdown(report : ReconciliationReport) -> String {
let out = StringBuilder()
out.write_string("# Physical stock reconciliation\n\n")
out.write_string("Cutoff (Unix seconds): \{report.as_of}\n\n")
out.write_string(
"Counts and book stock use the same cutoff. Review each variance against the count sheet and event records.\n\n",
)
out.write_string(
"| Lot | Facility | Book (g) | Counted (g) | Difference (g) | Status | Count reference |\n",
)
out.write_string("| --- | --- | ---: | ---: | ---: | --- | --- |\n")
for row in report.rows {
out.write_string(
"| \{markdown_cell(row.lot_id)} | \{markdown_cell(row.facility)} | \{row.expected_g} | \{row.counted_g} | \{row.difference_g} | \{row.status} | \{markdown_cell(row.count_reference)} |\n",
)
}
out.write_string(
"\nMissing physical counts: \{report.missing_counts.join(", ")}\n",
)
out.write_string(
"Book total: \{report.total_expected_g} g; counted total of supplied rows: \{report.total_counted_g} g.\n",
)
out.to_string()
}