Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions docs/tally/TALLY_PROTOCOL_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1802,7 +1802,9 @@ carries no such flag, and adding one would mean authoring a request shape with n
behind it. Every party amount therefore lands On Account, exactly as the measured import did,
and every build naming a counterparty says so in its warnings.

**Two written elements are not verified: `EFFECTIVEDATE` and `PARTYLEDGERNAME`.** The
**Two written elements are not verified: `EFFECTIVEDATE` and `PARTYLEDGERNAME`.** *(Written before
#467 and #469. `EFFECTIVEDATE` is now verified and `PARTYLEDGERNAME` is still not; see the two
scoped corrections below.)* The
verification collection of §9.8 fetches neither, so `verify_import` compares the date, voucher
type and signed entries and cannot see whether Tally kept, rewrote or dropped either — nor
whether an operator later edited them. A readback with a wrong effective date, or a party
Expand Down Expand Up @@ -1841,7 +1843,8 @@ the `FETCH` list and looks at what arrives.
> | Contra | `20250423` / `20250423` | `20250423`, `TYPE="Date"` | none written | the debit bank ledger (`HDFC CC`) |
>
> - **`EFFECTIVEDATE` is returned, equal to `DATE`**, on all three types. Comparing it is now
> possible. `verify_import` still does not fetch it, so the limit above still holds for the code.
> possible. `verify_import` did not fetch it when this read was taken; #469 closed that, see the
> next correction.
> - **`PARTYLEDGERNAME` is returned but does not echo what was written.** On these vouchers it held
> a cash or bank ledger that was on the voucher, not the counterparty Bridge wrote. **Do not
> compare it with the written value:** that comparison would refuse every one of these legitimate
Expand All @@ -1852,6 +1855,19 @@ the `FETCH` list and looks at what arrives.
> One read, one company, one release, three Bridge-built vouchers in a six-voucher window. It does not establish
> what a Tally UI edit to either field returns.

> **Scoped correction, 2026-09-17 — `EFFECTIVEDATE` is now verified (#469).** A live read
> (#467: licensed 7.1 Silver, synthetic lab) returned `EFFECTIVEDATE` with `TYPE="Date"`, equal
> to `DATE`, on a Bridge-built Receipt, Payment and Contra. The committed capture
> `native-three-vouchers.utf16le.xml` also carries it on two of its three vouchers. The §9.8
> verification read now appends `EFFECTIVEDATE` to its `FETCH`. For Payment, Receipt and Contra, a
> returned value that differs from the written date is an `effective_date` diff, in `verify_import`
> and in the amendment compare-and-swap. An absent or empty element is **not** a diff, because
> requiring it would refuse every verification on a release that does not return it; the row
> reports `"not_observed": ["effective_date"]` instead. A Journal is written without the element
> and is neither compared nor flagged. The public voucher tools do not fetch it or return it.
> `PARTYLEDGERNAME` stays unfetched: on the same read it held a bank ledger, not the written
> counterparty.

### 9.9 Bulk import throughput

**VERIFIED.** One import request may carry many `<VOUCHER>` elements; the counters aggregate.
Expand Down
2 changes: 1 addition & 1 deletion docs/tally/compatibility/compatibility-matrix.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"schema_version": 1,
"bridge_commit_sha": "be1c20cc3fd66fa1ece196505c69f26e555e4b8e",
"compatibility_surface_sha256": "dd09546c46de8262ea8249e614748c89224b42746863d21dbb0e305f976b7478",
"compatibility_surface_sha256": "b44131d63a4d7384808982ceedb19ae19dce525f55c8ac349e5b270a3ac9b9d6",
"claims": [
{
"claim_id": "erp9-6-6-3-windows-education-xml-one-company",
Expand Down
14 changes: 7 additions & 7 deletions docs/tally/compatibility/compatibility-surface.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
},
{
"path": "docs/tally/TALLY_PROTOCOL_REFERENCE.md",
"sha256": "e0b65e7530f876f351b4a1a970ded87dca5d0a757e53c8a03823add0a5d84874"
"sha256": "75bc5a116ad025cd5cc05e731ce4b9d1b967390df0f6010e71bd2548cb59908f"
},
{
"path": "docs/tally/compatibility/README.md",
Expand Down Expand Up @@ -431,11 +431,11 @@
},
{
"path": "src-tauri/src/agent_import.rs",
"sha256": "0f8c49d42191fe3ebd52ee71e966be77ef8369f5ac2eebd1ab705dbf51d765c6"
"sha256": "248276bac362d6aabc4476289da77ffe411f4292469700b8560ffb67bd14ad8c"
},
{
"path": "src-tauri/src/agent_import_amend.rs",
"sha256": "7c4093d938d62b2fcf297c72e5e6acd59a9875ac8df6b3186c24b226c515b83c"
"sha256": "4a64f56f5a7392fdeeaf3955eb1e957778fdd9e99e92a2b0c148521c27ec339a"
},
{
"path": "src-tauri/src/agent_import_cash_bank.rs",
Expand Down Expand Up @@ -511,15 +511,15 @@
},
{
"path": "src-tauri/src/agent_voucher_parse.rs",
"sha256": "c3dced60925238d8721f1a5f5130f0ab01b7feda58f1e4c1d3c9af78066c4abc"
"sha256": "8bef4ec351614a9169fc3009eea23c815db223afdb8bc8c94d8f2724a518b797"
},
{
"path": "src-tauri/src/agent_voucher_parse_tests.rs",
"sha256": "58209e929c0643bd54767193f6561a89bdfa9626680c80829ca043e0d9c54bf6"
"sha256": "841d79fec36e78fed33d9ed871d965033a6a21d414013337408038b338e1cc59"
},
{
"path": "src-tauri/src/agent_voucher_scalars.rs",
"sha256": "464226a1eff411777c6a6f775e83ade3e2f8c38ed9489056c671b1516a3e77e8"
"sha256": "0bfcc56dc814553adc302897fc56eb299549ceab1f782ee8211730694f7e390a"
},
{
"path": "src-tauri/src/agent_vouchers.rs",
Expand Down Expand Up @@ -1066,5 +1066,5 @@
"sha256": "0ab5bfbe6d81b1e10530744c338fb91efa57fe5a66fb4bc0cfd382495eaf02a4"
}
],
"manifest_sha256": "dd09546c46de8262ea8249e614748c89224b42746863d21dbb0e305f976b7478"
"manifest_sha256": "b44131d63a4d7384808982ceedb19ae19dce525f55c8ac349e5b270a3ac9b9d6"
}
36 changes: 32 additions & 4 deletions src-tauri/src/agent_import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,9 @@ struct ReadVoucher {
master_id: Option<String>,
cancelled: Option<bool>,
optional: Option<bool>,
/// Absent when the response carried no `EFFECTIVEDATE` (or an empty one).
#[serde(default)]
effective_date: Option<String>,
#[serde(rename = "amounts")]
entries: Vec<ReadEntry>,
}
Expand Down Expand Up @@ -2004,7 +2007,7 @@ fn render_voucher_xml(voucher: &ImportVoucher, remote_id: Uuid, attribution_id:
}

fn render_import_verification_read(company: &str, from: &str, to: &str) -> String {
format!("<ENVELOPE><HEADER><VERSION>1</VERSION><TALLYREQUEST>Export</TALLYREQUEST><TYPE>Collection</TYPE><ID>Bridge Agent Import Verification</ID></HEADER><BODY><DESC><STATICVARIABLES><SVEXPORTFORMAT>$$SysName:XML</SVEXPORTFORMAT><SVCURRENTCOMPANY>{}</SVCURRENTCOMPANY><SVFROMDATE TYPE=\"Date\">{from}</SVFROMDATE><SVTODATE TYPE=\"Date\">{to}</SVTODATE></STATICVARIABLES><TDL><TDLMESSAGE><SYSTEM TYPE=\"Formulae\" NAME=\"BridgeImportWindow\">$Date &gt;= $$Date:\"{from}\" AND $Date &lt;= $$Date:\"{to}\"</SYSTEM><COLLECTION NAME=\"Bridge Agent Import Verification\" ISMODIFY=\"No\"><TYPE>Voucher</TYPE><FETCH>DATE,VOUCHERNUMBER,VOUCHERTYPENAME,REMOTEID,GUID,MASTERID,ALTERID,NARRATION,ISCANCELLED,ISOPTIONAL,ALLLEDGERENTRIES.LEDGERNAME,ALLLEDGERENTRIES.AMOUNT,ALLLEDGERENTRIES.ISDEEMEDPOSITIVE</FETCH><FILTERS>BridgeImportWindow</FILTERS></COLLECTION></TDLMESSAGE></TDL></DESC></BODY></ENVELOPE>", xml_escape(company))
format!("<ENVELOPE><HEADER><VERSION>1</VERSION><TALLYREQUEST>Export</TALLYREQUEST><TYPE>Collection</TYPE><ID>Bridge Agent Import Verification</ID></HEADER><BODY><DESC><STATICVARIABLES><SVEXPORTFORMAT>$$SysName:XML</SVEXPORTFORMAT><SVCURRENTCOMPANY>{}</SVCURRENTCOMPANY><SVFROMDATE TYPE=\"Date\">{from}</SVFROMDATE><SVTODATE TYPE=\"Date\">{to}</SVTODATE></STATICVARIABLES><TDL><TDLMESSAGE><SYSTEM TYPE=\"Formulae\" NAME=\"BridgeImportWindow\">$Date &gt;= $$Date:\"{from}\" AND $Date &lt;= $$Date:\"{to}\"</SYSTEM><COLLECTION NAME=\"Bridge Agent Import Verification\" ISMODIFY=\"No\"><TYPE>Voucher</TYPE><FETCH>DATE,VOUCHERNUMBER,VOUCHERTYPENAME,REMOTEID,GUID,MASTERID,ALTERID,NARRATION,ISCANCELLED,ISOPTIONAL,ALLLEDGERENTRIES.LEDGERNAME,ALLLEDGERENTRIES.AMOUNT,ALLLEDGERENTRIES.ISDEEMEDPOSITIVE,EFFECTIVEDATE</FETCH><FILTERS>BridgeImportWindow</FILTERS></COLLECTION></TDLMESSAGE></TDL></DESC></BODY></ENVELOPE>", xml_escape(company))
}

fn xml_escape(value: &str) -> String {
Expand All @@ -2017,7 +2020,7 @@ fn xml_escape(value: &str) -> String {
}

fn parse_import_vouchers(xml: &str, company_guid: &str) -> Result<ImportReadSource, String> {
let parsed = super::parse_agent_changed_rows(xml, company_guid).map_err(|code| {
let parsed = super::parse_import_verification_rows(xml, company_guid).map_err(|code| {
match code.as_str() {
// Preserve the import error contract while sharing scalar admission.
"agent_read_protocol_invalid" if super::validate_agent_envelope(xml).is_err() => {
Expand All @@ -2026,6 +2029,7 @@ fn parse_import_vouchers(xml: &str, company_guid: &str) -> Result<ImportReadSour
"agent_read_protocol_invalid"
| "change_row_core_field_invalid"
| "voucher_date_invalid"
| "voucher_effective_date_invalid"
| "voucher_accounting_state_not_observed" => "import_verification_export_invalid",
"change_row_identity_invalid"
| "voucher_source_identity_invalid"
Expand Down Expand Up @@ -2276,12 +2280,13 @@ fn verify_batch(line: &ImportLedgerLine, observed: &ImportReadSource) -> Result<
group.consume(matched_index);
}
let matched = &observed[matched_index];
let effective_date_unobserved = effective_date_not_observed(expected, matched);
let diffs = voucher_diffs(
expected,
matched,
expected_key.2 == observed_fingerprints[matched_index].2,
);
if fingerprint_fallback {
let mut matched_value = if fingerprint_fallback {
counts
.entry("matching_content_observed")
.and_modify(|count| *count += 1);
Expand All @@ -2302,7 +2307,11 @@ fn verify_batch(line: &ImportLedgerLine, observed: &ImportReadSource) -> Result<
.entry("posted_divergent")
.and_modify(|count| *count += 1);
json!({"bridge_txn_id":expected.bridge_txn_id,"status":"posted_divergent","marker":marker,"diffs":diffs,"voucher_number":matched.voucher_number,"guid":matched.guid,"master_id":matched.master_id})
};
if effective_date_unobserved {
matched_value["not_observed"] = json!(["effective_date"]);
}
matched_value
};
if fingerprint_fallback && fingerprint_ambiguous_within_batch {
value["ambiguous_within_batch"] = Value::Bool(true);
Expand Down Expand Up @@ -2403,15 +2412,34 @@ fn batch_duplicate_sets(
)
}

/// A bank voucher whose readback carried no `EFFECTIVEDATE`: its effective
/// date was written but could not be compared, which a clean status must not hide.
fn effective_date_not_observed(expected: &ImportVoucher, actual: &ReadVoucher) -> bool {
expected.voucher_type != VoucherType::Journal && actual.effective_date.is_none()
}

fn voucher_diffs(
expected: &ImportVoucher,
actual: &ReadVoucher,
entries_match: bool,
) -> Vec<Value> {
let mut diffs = Vec::new();
if actual.date.as_deref() != normalized_date(&expected.date).ok().as_deref() {
let expected_date = normalized_date(&expected.date).ok();
if actual.date.as_deref() != expected_date.as_deref() {
diffs.push(json!("date"));
}
// A bank voucher is written with EFFECTIVEDATE equal to DATE (§9.13), and
// the verification read returns it (§9.8 scoped correction). Only a value
// that came back and differs is a diff: a response without the element
// is reported as not observed, never refused.
if expected.voucher_type != VoucherType::Journal
&& actual
.effective_date
.as_deref()
.is_some_and(|observed| Some(observed) != expected_date.as_deref())
{
diffs.push(json!("effective_date"));
}
if actual.voucher_type.as_deref() != Some(expected.voucher_type.as_str()) {
diffs.push(json!("voucher_type"));
}
Expand Down
13 changes: 11 additions & 2 deletions src-tauri/src/agent_import_amend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,18 @@ impl Lineage {
last_diffs = diffs;
}
match matched {
Some(batch_id) => admitted.push(json!({"bridge_txn_id":txn_id,
Some(batch_id) => {
let mut entry = json!({"bridge_txn_id":txn_id,
"book_matches_batch_id":batch_id,"guid":row.guid,"master_id":row.master_id,
"alter_id":row.alter_id})),
"alter_id":row.alter_id});
// the compare-and-swap could not see an edit to it
if row.effective_date.is_none()
&& row.voucher_type.as_deref() != Some("Journal")
{
entry["not_observed"] = json!(["effective_date"]);
}
admitted.push(entry);
}
None => refused.push(
json!({"bridge_txn_id":txn_id,"reason":"book_voucher_diverged",
"diffs_from_latest_build":last_diffs,"guid":row.guid,"alter_id":row.alter_id}),
Expand Down
27 changes: 27 additions & 0 deletions src-tauri/src/agent_import_amend_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ fn book_row(line: &ImportLedgerLine) -> ReadVoucher {
voucher_number: Some("7".into()),
cancelled: Some(false),
optional: Some(false),
effective_date: None,
// Tally does not promise the order it was sent (§12a.4).
entries: voucher
.entries
Expand Down Expand Up @@ -310,6 +311,32 @@ fn an_edited_missing_or_cancelled_voucher_refuses_the_amendment() {
let mut redated = book_row(&original);
redated.date = Some("20260905".into());
assert_eq!(reason(vec![redated]), "book_voucher_diverged");
// an edited effective date is a change the amendment would overwrite
let mut effective_redated = book_row(&original);
effective_redated.effective_date = Some("20260905".into());
let refused = lineage
.compare_and_swap(&proposal, &book(vec![effective_redated]))
.unwrap()
.expect_err("refused");
assert_eq!(refused[0]["reason"], "book_voucher_diverged");
assert_eq!(
refused[0]["diffs_from_latest_build"],
json!(["effective_date"])
);
// one that came back unchanged is admitted without a caveat; one that did
// not come back is admitted with it
let mut effective_kept = book_row(&original);
effective_kept.effective_date = effective_kept.date.clone();
let admitted = lineage
.compare_and_swap(&proposal, &book(vec![effective_kept]))
.unwrap()
.unwrap();
assert!(admitted[0].get("not_observed").is_none());
let admitted = lineage
.compare_and_swap(&proposal, &book(vec![book_row(&original)]))
.unwrap()
.unwrap();
assert_eq!(admitted[0]["not_observed"], json!(["effective_date"]));

assert_eq!(reason(vec![]), "not_in_book");
// A voucher carrying another batch's marker is not this one.
Expand Down
47 changes: 47 additions & 0 deletions src-tauri/src/agent_import_bank_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1270,12 +1270,44 @@ async fn a_bank_batch_verifies_through_the_rewrites_tally_makes_to_it() {
voucher_number: Some("463".into()),
cancelled: Some(false),
optional: Some(false),
effective_date: None,
entries,
}];
let result = verify_observed_batch(&line, &observed).unwrap();
assert_eq!(result["counts"]["posted_verified"], 1);
assert_eq!(verification_status(&result, 1), "posted_verified");
assert_eq!(result["duplicates"], json!([]));
// A response without EFFECTIVEDATE still verifies, but says the written
// effective date was not compared.
assert_eq!(
result["vouchers"][0]["not_observed"],
json!(["effective_date"])
);
// Returned equal to DATE, as measured on 7.1 (§9.8 scoped correction).
let mut returned = observed.clone();
returned[0].effective_date = returned[0].date.clone();
let result = verify_observed_batch(&line, &returned).unwrap();
assert_eq!(verification_status(&result, 1), "posted_verified");
assert!(result["vouchers"][0].get("not_observed").is_none());
// Returned and different: Tally rewrote it or someone edited it.
let mut rewritten = observed.clone();
rewritten[0].effective_date = Some("20260902".into());
let result = verify_observed_batch(&line, &rewritten).unwrap();
assert_eq!(result["vouchers"][0]["status"], "posted_divergent");
assert_eq!(result["vouchers"][0]["diffs"], json!(["effective_date"]));
assert!(result["vouchers"][0].get("not_observed").is_none());

// A Journal is written without EFFECTIVEDATE, so it is neither compared
// nor reported missing.
let mut journal_line = line.clone();
journal_line.vouchers[0].voucher_type = VoucherType::Journal;
let mut journal = rewritten.clone();
journal[0].voucher_type = Some("Journal".into());
let result = verify_observed_batch(&journal_line, &journal).unwrap();
assert_eq!(verification_status(&result, 1), "posted_verified");
journal[0].effective_date = None;
let result = verify_observed_batch(&journal_line, &journal).unwrap();
assert!(result["vouchers"][0].get("not_observed").is_none());
// A readback that disagrees on the type is a different voucher, and
// says so rather than passing on matching amounts alone.
let mut mistyped = observed.clone();
Expand Down Expand Up @@ -1585,3 +1617,18 @@ async fn a_parsed_statement_builds_an_import_file_by_proposals_id() {
);
assert_eq!(simulator.finish().expect("requests").len(), 44);
}

#[test]
fn the_verification_read_fetches_the_effective_date_and_not_the_party() {
let request = render_import_verification_read("Synthetic Book", "20260901", "20260901");
let fetch = request
.split_once("<FETCH>")
.and_then(|(_, rest)| rest.split_once("</FETCH>"))
.map(|(fetch, _)| fetch.split(',').collect::<Vec<_>>())
.unwrap();
assert!(fetch.contains(&"EFFECTIVEDATE"));
// Read back on 7.1, PARTYLEDGERNAME held a bank ledger rather than the
// written counterparty (§9.8 scoped correction): comparing it would refuse
// every legitimate bank voucher.
assert!(!fetch.contains(&"PARTYLEDGERNAME"));
}
22 changes: 22 additions & 0 deletions src-tauri/src/agent_import_boundary_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,25 @@ fn import_boundary_rejects_malformed_accounting_scalars_in_captured_vouchers() {
);
}
}

#[test]
fn import_verification_carries_the_captured_effective_date() {
// The capture returns EFFECTIVEDATE on its first two vouchers only.
let rows = parse_import_vouchers(&captured_vouchers(), CAPTURED_GUID)
.unwrap()
.rows;
let effective: Vec<Option<&str>> = rows
.iter()
.map(|row| row.effective_date.as_deref())
.collect();
assert_eq!(effective, [Some("20260801"), Some("20260801"), None]);
let invalid = captured_vouchers().replacen(
"<EFFECTIVEDATE TYPE=\"Date\">20260801</EFFECTIVEDATE>",
"<EFFECTIVEDATE TYPE=\"Date\">20261345</EFFECTIVEDATE>",
1,
);
assert_eq!(
parse_import_vouchers(&invalid, CAPTURED_GUID),
Err("import_verification_export_invalid".to_string())
);
}
1 change: 1 addition & 0 deletions src-tauri/src/agent_import_multiplicity_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ fn identical_batch() -> (ImportLedgerLine, Vec<ReadVoucher>) {
voucher_number: None,
cancelled: Some(false),
optional: Some(false),
effective_date: None,
entries: voucher
.entries
.iter()
Expand Down
Loading