e-arveldaja-mcp 0.36.0 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/.claude/commands/classify-unmatched.md +10 -1
  2. package/.claude/commands/import-wise.md +1 -1
  3. package/.claude/commands/reconcile-bank.md +3 -3
  4. package/CHANGELOG.md +16 -0
  5. package/dist/banking/reconciliation/executor.d.ts.map +1 -1
  6. package/dist/banking/reconciliation/executor.js +138 -32
  7. package/dist/banking/reconciliation/executor.js.map +1 -1
  8. package/dist/banking/reconciliation/presenter.d.ts +2 -1
  9. package/dist/banking/reconciliation/presenter.d.ts.map +1 -1
  10. package/dist/banking/reconciliation/presenter.js +36 -14
  11. package/dist/banking/reconciliation/presenter.js.map +1 -1
  12. package/dist/banking/reconciliation/projection.d.ts +2 -2
  13. package/dist/banking/reconciliation/projection.d.ts.map +1 -1
  14. package/dist/banking/reconciliation/projection.js +24 -14
  15. package/dist/banking/reconciliation/projection.js.map +1 -1
  16. package/dist/banking/reconciliation/types.d.ts +25 -4
  17. package/dist/banking/reconciliation/types.d.ts.map +1 -1
  18. package/dist/camt/executor.d.ts +15 -1
  19. package/dist/camt/executor.d.ts.map +1 -1
  20. package/dist/camt/executor.js +33 -19
  21. package/dist/camt/executor.js.map +1 -1
  22. package/dist/documents/operations.d.ts.map +1 -1
  23. package/dist/documents/operations.js +72 -2
  24. package/dist/documents/operations.js.map +1 -1
  25. package/dist/documents/types.d.ts +14 -3
  26. package/dist/documents/types.d.ts.map +1 -1
  27. package/dist/guided/process-accounting-document.d.ts.map +1 -1
  28. package/dist/guided/process-accounting-document.js +3 -1
  29. package/dist/guided/process-accounting-document.js.map +1 -1
  30. package/dist/guided/run-accounting-report.d.ts +3 -1
  31. package/dist/guided/run-accounting-report.d.ts.map +1 -1
  32. package/dist/guided/run-accounting-report.js +10 -2
  33. package/dist/guided/run-accounting-report.js.map +1 -1
  34. package/dist/receipts/classification-operations.d.ts +8 -2
  35. package/dist/receipts/classification-operations.d.ts.map +1 -1
  36. package/dist/receipts/classification-operations.js +246 -22
  37. package/dist/receipts/classification-operations.js.map +1 -1
  38. package/dist/receipts/classification-presenter.d.ts.map +1 -1
  39. package/dist/receipts/classification-presenter.js +42 -7
  40. package/dist/receipts/classification-presenter.js.map +1 -1
  41. package/dist/receipts/counterparty-booking-history.d.ts +13 -0
  42. package/dist/receipts/counterparty-booking-history.d.ts.map +1 -1
  43. package/dist/receipts/counterparty-booking-history.js +68 -24
  44. package/dist/receipts/counterparty-booking-history.js.map +1 -1
  45. package/dist/server/create-server.d.ts.map +1 -1
  46. package/dist/server/create-server.js +5 -0
  47. package/dist/server/create-server.js.map +1 -1
  48. package/dist/server/register-domain-tools.d.ts +2 -0
  49. package/dist/server/register-domain-tools.d.ts.map +1 -1
  50. package/dist/server/register-domain-tools.js +2 -2
  51. package/dist/server/register-domain-tools.js.map +1 -1
  52. package/dist/tools/accounting-inbox-autopilot-service.d.ts.map +1 -1
  53. package/dist/tools/accounting-inbox-autopilot-service.js +3 -1
  54. package/dist/tools/accounting-inbox-autopilot-service.js.map +1 -1
  55. package/dist/tools/accounting-inbox.js +1 -1
  56. package/dist/tools/accounting-inbox.js.map +1 -1
  57. package/dist/tools/bank-reconciliation-plan.d.ts +2 -0
  58. package/dist/tools/bank-reconciliation-plan.d.ts.map +1 -1
  59. package/dist/tools/bank-reconciliation-plan.js +4 -0
  60. package/dist/tools/bank-reconciliation-plan.js.map +1 -1
  61. package/dist/tools/bank-reconciliation.d.ts.map +1 -1
  62. package/dist/tools/bank-reconciliation.js +13 -9
  63. package/dist/tools/bank-reconciliation.js.map +1 -1
  64. package/dist/tools/receipt-extraction.d.ts +2 -1
  65. package/dist/tools/receipt-extraction.d.ts.map +1 -1
  66. package/dist/tools/receipt-extraction.js.map +1 -1
  67. package/dist/tools/receipt-inbox.d.ts +13 -0
  68. package/dist/tools/receipt-inbox.d.ts.map +1 -1
  69. package/dist/tools/receipt-inbox.js +22 -11
  70. package/dist/tools/receipt-inbox.js.map +1 -1
  71. package/dist/wise/executor.d.ts.map +1 -1
  72. package/dist/wise/executor.js +43 -3
  73. package/dist/wise/executor.js.map +1 -1
  74. package/dist/wise/presenter.d.ts +4 -1
  75. package/dist/wise/presenter.d.ts.map +1 -1
  76. package/dist/wise/presenter.js +18 -2
  77. package/dist/wise/presenter.js.map +1 -1
  78. package/dist/wise/projection.d.ts +9 -1
  79. package/dist/wise/projection.d.ts.map +1 -1
  80. package/dist/wise/projection.js +16 -1
  81. package/dist/wise/projection.js.map +1 -1
  82. package/dist/wise/types.d.ts +16 -0
  83. package/dist/wise/types.d.ts.map +1 -1
  84. package/dist/wise/types.js.map +1 -1
  85. package/package.json +1 -1
  86. package/server.json +2 -2
  87. package/workflows/classify-unmatched.md +2 -1
  88. package/workflows/import-wise.md +1 -1
  89. package/workflows/reconcile-bank.md +3 -3
@@ -76,7 +76,15 @@ Show the compact `result.summary`: `counts` (unmatched transactions, groups, aut
76
76
 
77
77
  ### Step 2: Explain what can be applied and choose the groups
78
78
 
79
- Rows that may pay an open invoice never reach the groups: a possible invoice match, a residual on a partially paid purchase invoice, or a document-reference candidate (exact amount plus the purchase invoice's number or `Ostukorvi/tellimuse nr:` order/cart number in the description). The purchase invoice already exists, so booking such a row here would double the expense: reconcile it with the **Reconcile Bank** workflow (`reconcile_bank_transactions` `mode: "suggest"` lists it), and drop any such transaction ID a user asks to add. Apply re-checks and skips it ("matches an open purchase invoice; skipped — reconcile instead").
79
+ Rows that may pay an open invoice never reach the groups: a possible invoice match, a residual on a partially paid purchase invoice, or a document-reference candidate (exact amount plus the purchase invoice's number or `Ostukorvi/tellimuse nr:` order/cart number in the description). They are listed, never dropped: <!-- E_ARVELDAJA_CAPABILITY_CONDITION_START:standard -->
80
+ Capability condition for `standard`: inspect the connected MCP server's advertised tool list before this section. Run this section only when every named tool is advertised: `list_account_dimensions`. Otherwise skip this section and continue with the surrounding workflow. Never call a missing tool to probe capability.
81
+
82
+ `result.invoice_matched` gives each row's transaction id, amount, date, the invoice (`invoice_type`, `invoice_id`, `invoice_number`) and what matched (`matched_by`, `matched_reference` or `match_reasons`)
83
+ <!-- E_ARVELDAJA_CAPABILITY_CONDITION_END:standard --><!-- E_ARVELDAJA_CAPABILITY_CONDITION_START:guided -->
84
+ Capability condition for `guided`: inspect the connected MCP server's advertised tool list before this section. Run this section only when every named tool is advertised: `classify_bank_transactions`, and none of these is advertised: `list_account_dimensions`. Otherwise skip this section and continue with the surrounding workflow. Never call a missing tool to probe capability.
85
+
86
+ the `invoice_matched` count and warning in `result.summary` name each row and its invoice
87
+ <!-- E_ARVELDAJA_CAPABILITY_CONDITION_END:guided -->. The invoice already exists, so booking such a row here would double the expense: reconcile it with the **Reconcile Bank** workflow (`reconcile_bank_transactions` `mode: "suggest"` lists it), and drop any such transaction ID a user asks to add. Apply re-checks and skips it ("matches an open purchase invoice; skipped — reconcile instead").
80
88
 
81
89
  Rows the ledger check took out of the groups are never applied here; report each with its `next_action`:
82
90
  <!-- E_ARVELDAJA_CAPABILITY_CONDITION_START:standard -->
@@ -95,6 +103,7 @@ Capability condition for `guided`: inspect the connected MCP server's advertised
95
103
  <!-- E_ARVELDAJA_CAPABILITY_CONDITION_END:guided -->
96
104
 
97
105
  - `apply_mode="purchase_invoice"` groups are auto-bookable
106
+ - `apply_mode="account_distribution"` groups are auto-bookable from booking history: every earlier confirmed row of this counterparty (at least two, matched by its client, IBAN or legal form) went straight to one account and dimension with no invoice. Apply confirms each row to that account with its own amount, setting the row's client first when it has none. Show the proposed account, dimension and the evidence (`suggested_booking.transaction_history.history_total` and `last_journal_ids`, or the `history_distribution` warning) on the approval card. Apply re-derives the booking from the fresh ledger, so a history that changed since the dry run is `plan_drift`
98
107
  - review-only categories are reported back as skipped
99
108
  <!-- E_ARVELDAJA_CAPABILITY_CONDITION_START:guided -->
100
109
  Capability condition for `guided`: inspect the connected MCP server's advertised tool list before this section. Run this section only when every named tool is advertised: `classify_bank_transactions`, and none of these is advertised: `list_account_dimensions`. Otherwise skip this section and continue with the surrounding workflow. Never call a missing tool to probe capability.
@@ -110,7 +110,7 @@ The compact preview returns a `summary`: `summary.counts` (CSV rows, eligible, f
110
110
  <!-- E_ARVELDAJA_CAPABILITY_CONDITION_START:standard -->
111
111
  Capability condition for `standard`: inspect the connected MCP server's advertised tool list before this section. Run this section only when every named tool is advertised: `import_wise_transactions`. Otherwise skip this section and continue with the surrounding workflow. Never call a missing tool to probe capability.
112
112
 
113
- The dry run returns top-level `plan_handle` and `approved_command_digest`, a `summary` (`total_csv_rows`, `eligible`, `filtered_out`, `created` = would-create, `skipped`, `error_count`, `inter_account_total`, `needs_review`, `blocking_review_count` = the reviews that block approval; every review but `wise_transfer_ownership_unverified` blocks), `ownership_reviews` (transfers that could not be auto-verified as own-account transfers; never dropped), `transfer_counterpart_unresolved` (only when transfer rows are held: `message`, `unresolved_row_count`, `conflict_row_count`, `candidates[]` with `accounts_dimensions_id` + `label`; the per-row reviews are in `ownership_reviews` with `counterpart_candidate_dimension_ids` and, for a conflict, `evidence_transaction_ids`), `inter_account_reconciliation`, advisory `invoice_currency_fixes`, and `skipped_details`: skipped rows grouped by reason, each group with `count` and up to 5 `sample_ids`; for already-booked rows the ledger id is replaced in the reason by `journal (see records)` / `transaction (see records)` and the group's `records` lists every row's `wise_id` + `record_id`. In a dry run, `execution.skipped` itemises only skips that are not already-booked rows (unsupported directions, dependency skips); `execution.skipped_omitted_already_booked` counts the rest, which are in `skipped_details`. Transfers already booked by a journal are not commands: the dry run lists them in `execution.already_journalized_rows` (`wise_id`, `existing_journal_id`, date, amount, currency, direction; count in `execution.already_journalized_count`, of which `execution.already_journalized_auto_matched_count` are legacy transfers matched automatically). `execution.commands` and `command_count` cover only commands that write. An executed run keeps every write command and every skipped row itemised; the already-journalized re-check results are in `inter_account_reconciliation.details`, after the confirmed transfers.
113
+ The dry run returns top-level `plan_handle` and `approved_command_digest`, a `summary` (`total_csv_rows`, `eligible`, `filtered_out`, `created` = would-create, `skipped`, `error_count`, `inter_account_total`, `needs_review`, `blocking_review_count` = the reviews that block approval; every review but `wise_transfer_ownership_unverified` blocks), `ownership_reviews` (transfers that could not be auto-verified as own-account transfers; never dropped), `transfer_counterpart_unresolved` (only when transfer rows are held: `message`, `unresolved_row_count`, `conflict_row_count`, `candidates[]` with `accounts_dimensions_id` + `label`; the per-row reviews are in `ownership_reviews` with `counterpart_candidate_dimension_ids` and, for a conflict, `evidence_transaction_ids`), `inter_account_reconciliation`, advisory `invoice_currency_fixes`, and `skipped_details`: skipped rows grouped by reason, each group with `count` and up to 5 `sample_ids`; for already-booked rows the ledger id is replaced in the reason by `journal (see records)` / `transaction (see records)` and the group's `records` lists every row's `wise_id` + `record_id`. In a dry run, `execution.skipped` itemises only skips that are not already-booked rows (unsupported directions, dependency skips); `execution.skipped_omitted_already_booked` counts the rest, which are in `skipped_details`. Transfers already booked by a journal are not commands: the dry run lists them in `execution.already_journalized_rows` (`wise_id`, `existing_journal_id`, date, amount, currency, direction; count in `execution.already_journalized_count`, of which `execution.already_journalized_auto_matched_count` are legacy transfers matched automatically). `execution.commands` and `command_count` cover only commands that write. An executed run keeps every write command and every skipped row itemised; the already-journalized re-check results are in `inter_account_reconciliation.details`, after the confirmed transfers. A row booked as an ordinary payment (not an own transfer) carries the client whose normalized name equals its counterparty name, ignoring case (CAMT's exact-name rule; a partial name match is not used): `clients_id`, `client_match`, also in its `create_payload`; a row several clients match exactly has `client_ambiguous` and a `client_note`, is created without a client and needs one (`clients_id`) before it can be confirmed.
114
114
  <!-- E_ARVELDAJA_CAPABILITY_CONDITION_END:standard -->
115
115
 
116
116
  Show main transactions and fee rows that would be created, exact duplicate / skip reasons, whether fees will be auto-confirmed to the chosen dimension, inter-account transfer confirmations or skips, and advisory invoice FX corrections (reported only — the import never changes confirmed purchase invoices).
@@ -80,7 +80,7 @@ Capability condition for `standard`: inspect the connected MCP server's advertis
80
80
  Capability condition for `guided`: inspect the connected MCP server's advertised tool list before this section. Run this section only when every named tool is advertised: `reconcile_bank_transactions`, and none of these is advertised: `confirm_transaction`. Otherwise skip this section and continue with the surrounding workflow. Never call a missing tool to probe capability.
81
81
 
82
82
  `document_reference_candidate` warnings
83
- <!-- E_ARVELDAJA_CAPABILITY_CONDITION_END:guided -->): the exact amount plus the purchase invoice's number or its `Ostukorvi/tellimuse nr:` order/cart number in the bank description, scored below the threshold. They are NOT unmatched rows and are never auto-confirmed: show each one (transaction, invoice, matched reference) and ask the owner whether it is that invoice's payment.<!-- E_ARVELDAJA_CAPABILITY_CONDITION_START:standard -->
83
+ <!-- E_ARVELDAJA_CAPABILITY_CONDITION_END:guided -->): a match whose bank description carries the exact-amount purchase invoice's number or its `Ostukorvi/tellimuse nr:` order/cart number gets the `document_reference` reason and ranks above amount/name-only candidates, capped at confidence 85, so it is never auto-confirmed — ask before confirming it. A document-reference invoice that another candidate outranked is listed in this group. They are NOT unmatched rows and are never auto-confirmed: show each one (transaction, invoice, matched reference) and ask the owner whether it is that invoice's payment.<!-- E_ARVELDAJA_CAPABILITY_CONDITION_START:standard -->
84
84
  Capability condition for `standard`: inspect the connected MCP server's advertised tool list before this section. Run this section only when every named tool is advertised: `confirm_transaction`. Otherwise skip this section and continue with the surrounding workflow. Never call a missing tool to probe capability.
85
85
 
86
86
  If approved, call `confirm_transaction` with `distributions: [{ related_table: "purchase_invoices", related_id: <invoice.id>, amount: <open balance> }]` (no `distribution` key is provided; check a partially paid invoice's open balance first).
@@ -258,7 +258,7 @@ Review the results:
258
258
  Capability condition for `standard`: inspect the connected MCP server's advertised tool list before this section. Run this section only when every named tool is advertised: `confirm_transaction`. Otherwise skip this section and continue with the surrounding workflow. Never call a missing tool to probe capability.
259
259
 
260
260
  - Treat `result.execution.summary` as the canonical source for counts, and use `result.pairs`, `result.one_sided`, `result.already_handled`, and `result.ambiguous_pairs` for the detailed breakdown.
261
- - `already_handled`: a journal already books the transfer. Rows WITHOUT `mirror_proposal` are only listed for the owner: they are not proven mirrors, so never offer to delete them. Propose deleting a row only when it carries `mirror_proposal` (it is then the sole candidate): show its `evidence`, `journal_id` and `counterpart_transaction_id`, and delete it through its `next_action` (`delete_transaction`) only after the owner explicitly approves that row
261
+ - `already_handled`: a journal already books the transfer. Rows WITHOUT `mirror_proposal` are only listed for the owner: they are not proven mirrors, so never offer to delete them. Propose deleting a row only when it carries `mirror_proposal` (it is then the sole candidate): show its `evidence`, `journal_id` and `counterpart_transaction_id`, and delete it through its `next_action` (`delete_transaction`) only after the owner explicitly approves that row, or, once the owner approves all of them, re-run the dry run with `delete_proposed_mirrors: true` to put their deletions in the plan
262
262
  - `one_sided`: would confirm against the other bank account
263
263
  - `pairs`: would confirm the outgoing side and delete the duplicate incoming `PROJECT` (draft/unconfirmed) row (`incoming_action: "would_delete_duplicate"`)
264
264
  - `result.execution.errors`: any confirmation failures or other blocking issues
@@ -267,7 +267,7 @@ Capability condition for `standard`: inspect the connected MCP server's advertis
267
267
  Capability condition for `guided`: inspect the connected MCP server's advertised tool list before this section. Run this section only when every named tool is advertised: `reconcile_bank_transactions`, and none of these is advertised: `confirm_transaction`. Otherwise skip this section and continue with the surrounding workflow. Never call a missing tool to probe capability.
268
268
 
269
269
  - `summary.counts` gives the pairs, one-sided confirms, already-handled rows (`duplicates`), proposed mirror deletions (`proposed_mirrors`), and transfers needing review; `summary.samples`, `summary.warnings`, and `summary.blockers` carry the detail. Warnings show at most 3 rows per category; when a category has more, a `<category>_more` warning reports how many are not shown, and when the response size limit trims further a `warnings_truncated` warning counts the dropped rows per code: tell the owner those counts and never present the shown rows as the complete list.
270
- - Already-handled rows are only listed. A `mirror_proposal` warning names a row that is the sole proven mirror of a journal-booked transfer; deleting it needs the `standard` or `full` profile (`EARVELDAJA_PROFILE`) and the owner's explicit approval. Never offer to delete an already-handled row that has no mirror proposal.
270
+ - Already-handled rows are only listed. A `mirror_proposal` warning names a row that is the sole proven mirror of a journal-booked transfer; to delete them, re-run the dry run with `delete_proposed_mirrors: true` (the summary's `next_action` says so when the plan is otherwise empty), review `proposed_mirror_deletes`, and execute with the same argument only after the owner's explicit approval; execute deletes only rows still matching the reviewed row and journal. Never offer to delete an already-handled row that has no mirror proposal.
271
271
  - `summary.next_action` is the ready-to-send execute call (`mode: "execute_inter_account"` with the `plan_handle` and the dry run's `max_date_gap` / `target_accounts_dimensions_id`); run it only after approval.
272
272
  <!-- E_ARVELDAJA_CAPABILITY_CONDITION_END:guided -->
273
273
  - Ambiguous transfers (including `direction_unresolved`) are never auto-confirmed — present them for a decision.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.37.0] - 2026-10-30
6
+
7
+ ### Fixed
8
+
9
+ - **A bank payment that names an invoice's order or cart number is now suggested for that invoice, even when another invoice has the same amount.** Before, two same-amount ticket payments were both suggested for the older ticket and the right invoice was hidden. Details: reconcile suggest adds the match reason `document_reference` when the description carries an exact-amount purchase invoice's number or its notes order/cart reference, raising confidence by 30 and capping it at 85 (a document reference alone never reaches the 90 auto-confirm threshold; the auto-confirm path is unchanged); `document_reference_candidates` also lists such an invoice when a different invoice won the row. Suggest now re-reads the transaction and invoice lists instead of using the short list cache, so changes made elsewhere (web UI, another client) are seen — three extra list reads per suggest.
10
+ - **Wise payments to someone already in your client list now get that client, so they can be confirmed.** Before, a transfer to the owner imported as an ordinary payment had no client and confirming it failed with "buyer or supplier is missing". Details: the Wise import sets `clients_id` on payment rows and transfers treated as payments only on an exact (case-insensitive) name match, using the CAMT name rule (`selectClientByName`, shared); a partial name match leaves the client empty; several exact matches leave it empty with a note. The client is part of the reviewed import plan.
11
+ - **The Wise dry run now reports how many own-account transfers it will book.** Details: the dry-run summary `inter_account_total` counts the planned `inter_account` commands (it showed 0 before execute).
12
+ - **The guided profile can now delete the duplicate half of a transfer between your own accounts after you approve it.** Before, guided showed these rows but could not remove them. Details: `reconcile_bank_transactions` / `reconcile_inter_account_transfers` take opt-in `delete_proposed_mirrors: true` on the dry run; the deletions become plan commands run after all confirms, bound into the plan fingerprint (resend the flag at execute); before each delete the row, its confirmed counterpart and the booking journal are re-read, and any change stops that delete with nothing written.
13
+ - **The guided invoice booking can now book a ticket that names both the seller and the carrier, once you choose the supplier.** Before, a Tpilet ticket (seller T grupp, carrier Tulisilm) could only be booked on the standard profile. Details: on a booking-binding prepare, an explicit `supplier_client_id` whose registry code (digits) or VAT number equals one of the document's printed supplier identifiers turns `supplier_identifier_ambiguous` into the warning `supplier_identifier_acknowledged`, recorded in `booking_review`; the company's own identifiers never acknowledge, and the blocker stays when they cannot be read; create re-reads the client and refuses with `plan_drift` if its identifier changed.
14
+
15
+ ### Added
16
+
17
+ - **Classification proposes the account a counterparty has always been booked to, and you can apply it with one approval.** Details: when at least two earlier confirmed payments of the same client (or IBAN / legal form — never a name alone), in the same direction, were all booked to one account and dimension with no invoice links, the group gets apply mode `account_distribution` with the evidence (count, journal ids); apply runs only through `dry_run_apply` → `execute_apply`, re-derives the booking from the fresh ledger (a change is `plan_drift`, nothing written), sets the client first and confirms with `related_sub_id`. Mixed history stays review-only and shows the split; own bank accounts, foreign currency, saved rules and statutory categories are excluded.
18
+ - **Classification now lists the payments it leaves out because they may pay an open invoice.** Details: new `invoice_matched` section (invoice, matched reference or score) counted in `total_partitioned_out`, a guided warning, and accounting-inbox follow-ups pointing to reconcile.
19
+ - **The guided accounting report can force a fresh read.** Details: `run_accounting_report` takes `fresh: true`, clearing the active connection's cached data before computing.
20
+
5
21
  ## [0.36.0] - 2026-10-30
6
22
 
7
23
  ### Fixed
@@ -1 +1 @@
1
- {"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../../src/banking/reconciliation/executor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAqB7D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AAmE5E,OAAO,KAAK,EAEV,qBAAqB,EACrB,0BAA0B,EAC1B,iBAAiB,EAGjB,mBAAmB,EAGnB,qBAAqB,EACrB,0BAA0B,EAC1B,iBAAiB,EACjB,uBAAuB,EACvB,mBAAmB,EAInB,yBAAyB,EAOzB,mBAAmB,EACpB,MAAM,YAAY,CAAC;AAkBpB,eAAO,MAAM,+BAA+B,KAAK,CAAC;AAUlD,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,sBAAsB,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACnE;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAC/D;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9D,MAAM,MAAM,eAAe,CAAC,CAAC,IACzB;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAA;CAAE,GACvC;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAA;CAAE,CAAC;AAE3D;;;;GAIG;AACH,qBAAa,kCAAmC,SAAQ,KAAK;IAC/C,QAAQ,CAAC,OAAO,EAAE,YAAY;IAA1C,YAAqB,OAAO,EAAE,YAAY,EAGzC;CACF;AAYD,wDAAwD;AACxD,eAAO,MAAM,8BAA8B,KAAK,CAAC;AAEjD,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,UAAU,EACf,KAAK,EAAE,mBAAmB,GACzB,OAAO,CAAC,yBAAyB,CAAC,CAkOpC;AAoYD,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,UAAU,EACf,oBAAoB,EAAE,oBAAoB,EAC1C,KAAK,EAAE,iBAAiB,GACvB,OAAO,CAAC,mBAAmB,CAAC,CAI9B;AAED,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,UAAU,EACf,oBAAoB,EAAE,oBAAoB,EAC1C,KAAK,EAAE,0BAA0B,GAChC,OAAO,CAAC,eAAe,CAAC,qBAAqB,CAAC,CAAC,CA0BjD;AAID,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,UAAU,EACf,KAAK,EAAE,iBAAiB,EACxB,IAAI,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GACzB,OAAO,CAAC,uBAAuB,CAAC,CAw5BlC;AAuBD,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,UAAU,EACf,oBAAoB,EAAE,oBAAoB,EAC1C,KAAK,EAAE,iBAAiB,GACvB,OAAO,CAAC,mBAAmB,CAAC,CA6B9B;AAED,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,UAAU,EACf,oBAAoB,EAAE,oBAAoB,EAC1C,KAAK,EAAE,0BAA0B,GAChC,OAAO,CAAC,eAAe,CAAC,qBAAqB,CAAC,CAAC,CAmNjD"}
1
+ {"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../../src/banking/reconciliation/executor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAqB7D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AAqE5E,OAAO,KAAK,EAEV,qBAAqB,EACrB,0BAA0B,EAC1B,iBAAiB,EAGjB,mBAAmB,EAGnB,qBAAqB,EACrB,0BAA0B,EAC1B,iBAAiB,EACjB,uBAAuB,EAEvB,mBAAmB,EAInB,yBAAyB,EAOzB,mBAAmB,EACpB,MAAM,YAAY,CAAC;AAkBpB,eAAO,MAAM,+BAA+B,KAAK,CAAC;AAUlD,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,sBAAsB,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACnE;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAC/D;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9D,MAAM,MAAM,eAAe,CAAC,CAAC,IACzB;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAA;CAAE,GACvC;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAA;CAAE,CAAC;AAE3D;;;;GAIG;AACH,qBAAa,kCAAmC,SAAQ,KAAK;IAC/C,QAAQ,CAAC,OAAO,EAAE,YAAY;IAA1C,YAAqB,OAAO,EAAE,YAAY,EAGzC;CACF;AAYD,wDAAwD;AACxD,eAAO,MAAM,8BAA8B,KAAK,CAAC;AAcjD,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,UAAU,EACf,KAAK,EAAE,mBAAmB,GACzB,OAAO,CAAC,yBAAyB,CAAC,CAoPpC;AAoYD,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,UAAU,EACf,oBAAoB,EAAE,oBAAoB,EAC1C,KAAK,EAAE,iBAAiB,GACvB,OAAO,CAAC,mBAAmB,CAAC,CAI9B;AAED,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,UAAU,EACf,oBAAoB,EAAE,oBAAoB,EAC1C,KAAK,EAAE,0BAA0B,GAChC,OAAO,CAAC,eAAe,CAAC,qBAAqB,CAAC,CAAC,CA0BjD;AAID,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,UAAU,EACf,KAAK,EAAE,iBAAiB,EACxB,IAAI,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GACzB,OAAO,CAAC,uBAAuB,CAAC,CAw6BlC;AAuBD,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,UAAU,EACf,oBAAoB,EAAE,oBAAoB,EAC1C,KAAK,EAAE,iBAAiB,GACvB,OAAO,CAAC,mBAAmB,CAAC,CA8B9B;AAED,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,UAAU,EACf,oBAAoB,EAAE,oBAAoB,EAC1C,KAAK,EAAE,0BAA0B,GAChC,OAAO,CAAC,eAAe,CAAC,qBAAqB,CAAC,CAAC,CAwQjD"}
@@ -17,7 +17,7 @@ import { BookingGuard } from "../../booking-guard.js";
17
17
  import { buildBankAccountLookups, normalizeReference } from "../../tools/inter-account-utils.js";
18
18
  import { toUtcDay } from "../../date-utils.js";
19
19
  import { annotateDistinctSourceReferences, findDuplicateBankPostings, resolveBankDimensionsSafe, transactionSourceReferenceIdentity, } from "../../bank-posting-duplicate-guard.js";
20
- import { BANK_RECONCILIATION_PLAN_DOMAIN, buildReconciliationExecutionPlanInput, canonicalPlanJson, executeReconciliationCommands, reconClientUpdateCommandId, reconInvoiceConfirmCommandId, reconTransferConfirmCommandId, reconDeleteDuplicateCommandId, RECON_CONFIRM_INVOICE_CATEGORY, RECON_CONFIRM_TRANSFER_CATEGORY, RECON_DELETE_DUPLICATE_CATEGORY, RECON_UPDATE_CLIENT_CATEGORY, stripUndefinedDeep, } from "../../tools/bank-reconciliation-plan.js";
20
+ import { BANK_RECONCILIATION_PLAN_DOMAIN, buildReconciliationExecutionPlanInput, canonicalPlanJson, executeReconciliationCommands, reconClientUpdateCommandId, reconInvoiceConfirmCommandId, reconTransferConfirmCommandId, reconDeleteDuplicateCommandId, reconDeleteProposedMirrorCommandId, RECON_CONFIRM_INVOICE_CATEGORY, RECON_CONFIRM_TRANSFER_CATEGORY, RECON_DELETE_DUPLICATE_CATEGORY, RECON_DELETE_PROPOSED_MIRROR_CATEGORY, RECON_UPDATE_CLIENT_CATEGORY, stripUndefinedDeep, } from "../../tools/bank-reconciliation-plan.js";
21
21
  import { buildInvoiceIndex, getIndexedCandidates } from "./invoice-index.js";
22
22
  import { documentReferenceOnTransaction } from "./document-reference.js";
23
23
  import { legacyTruncatedCamtMarkerHint } from "../../camt/duplicate-identity.js";
@@ -68,13 +68,28 @@ function validateInterAccountDateGap(maxDateGap) {
68
68
  // === Suggest =================================================================
69
69
  /** The suggestion path's default minimum confidence. */
70
70
  export const SUGGEST_DEFAULT_MIN_CONFIDENCE = 50;
71
+ /**
72
+ * Suggest-only bonus for an exact-amount purchase invoice whose number or notes
73
+ * order / cart reference the bank description carries as a whole token.
74
+ * exact_amount (40) + 30 = 70 outranks every amount/name-only candidate
75
+ * (max 40 + 15 = 55). The cap 85 keeps it strictly below the 90 auto-confirm
76
+ * threshold (owner decision: a document reference alone never auto-confirms)
77
+ * and equals the Tier A cap. The exact-confirm path scores with matchScore
78
+ * alone, so ref_number_exact and >= 90 admission are untouched.
79
+ */
80
+ const DOCUMENT_REFERENCE_POINTS = 30;
81
+ const DOCUMENT_REFERENCE_CONFIDENCE_CAP = 85;
71
82
  export async function runSuggestMatches(api, input) {
72
83
  const threshold = input.minConfidence ?? SUGGEST_DEFAULT_MIN_CONFIDENCE;
73
- // A caller asking for a stricter threshold than the default gets no
74
- // below-threshold document-reference group (okt 2026 finding 5).
75
- const documentReferenceGroupEnabled = input.minConfidence === undefined || input.minConfidence <= SUGGEST_DEFAULT_MIN_CONFIDENCE;
76
84
  const documentReferenceCandidates = [];
77
85
  const block_on_duplicate = input.blockOnDuplicate;
86
+ // Read uncached: the 120 s list cache does not see invoices created or paid
87
+ // by another process (a second MCP server, the e-arveldaja UI), so two
88
+ // surfaces over the same books could report different matches (live
89
+ // 2026-10-30: an invoice confirmed 9 s earlier elsewhere was missing).
90
+ api.transactions.invalidateListCache();
91
+ api.saleInvoices.invalidateListCache();
92
+ api.purchaseInvoices.invalidateListCache();
78
93
  // Get all unconfirmed transactions
79
94
  const allTx = await api.transactions.listAll();
80
95
  const unconfirmed = allTx.filter(isProjectTransaction);
@@ -104,6 +119,7 @@ export async function runSuggestMatches(api, input) {
104
119
  let matchedDateTo;
105
120
  for (const tx of unconfirmed) {
106
121
  const candidates = [];
122
+ const documentReferenceByInvoiceId = new Map();
107
123
  const { allowSaleInvoices, allowPurchaseInvoices } = getInvoiceMatchEligibility(tx);
108
124
  if (allowSaleInvoices) {
109
125
  for (const inv of getIndexedCandidates(saleIndex, tx.ref_number, tx.amount, tx.base_amount)) {
@@ -127,8 +143,19 @@ export async function runSuggestMatches(api, input) {
127
143
  }
128
144
  if (allowPurchaseInvoices) {
129
145
  for (const inv of getIndexedCandidates(purchaseIndex, tx.ref_number, tx.amount, tx.base_amount)) {
130
- const { confidence, reasons, partiallyPaidWarning } = matchScore(tx, inv, tx.amount);
146
+ const scored = matchScore(tx, inv, tx.amount);
147
+ const { reasons, partiallyPaidWarning } = scored;
148
+ let confidence = scored.confidence;
149
+ const matchedReference = reasons.includes("exact_amount")
150
+ ? documentReferenceOnTransaction(tx.description, inv)
151
+ : undefined;
152
+ if (matchedReference !== undefined) {
153
+ reasons.push("document_reference");
154
+ confidence = Math.max(confidence, Math.min(confidence + DOCUMENT_REFERENCE_POINTS, DOCUMENT_REFERENCE_CONFIDENCE_CAP));
155
+ }
131
156
  if (confidence >= threshold) {
157
+ if (matchedReference !== undefined)
158
+ documentReferenceByInvoiceId.set(inv.id, { inv, matchedReference });
132
159
  candidates.push({
133
160
  type: "purchase_invoice",
134
161
  id: inv.id,
@@ -143,34 +170,36 @@ export async function runSuggestMatches(api, input) {
143
170
  match_reasons: reasons,
144
171
  });
145
172
  }
146
- else if (documentReferenceGroupEnabled && reasons.includes("exact_amount")) {
147
- const matchedReference = documentReferenceOnTransaction(tx.description, inv);
148
- if (matchedReference !== undefined) {
149
- // RAW domain strings — the presenter wraps the free-text fields.
150
- documentReferenceCandidates.push({
151
- transaction_id: tx.id,
152
- date: tx.date,
153
- amount: tx.amount,
154
- description: tx.description ?? undefined,
155
- bank_account_name: tx.bank_account_name ?? undefined,
156
- invoice: {
157
- type: "purchase_invoice",
158
- id: inv.id,
159
- number: inv.number,
160
- client_name: inv.client_name,
161
- gross_price: inv.gross_price ?? 0,
162
- payment_status: inv.payment_status ?? "NOT_PAID",
163
- },
164
- matched_reference: matchedReference,
165
- match_reasons: [...reasons, "document_reference"],
166
- });
167
- }
168
- }
169
173
  }
170
174
  }
171
175
  if (candidates.length > 0) {
172
176
  candidates.sort((a, b) => b.confidence - a.confidence);
173
177
  const bestMatch = candidates[0];
178
+ // A document-reference invoice that did not win this row is listed
179
+ // separately so it is not hidden behind the chosen best match.
180
+ for (const candidate of candidates.slice(1)) {
181
+ const docRef = candidate.type === "purchase_invoice" ? documentReferenceByInvoiceId.get(candidate.id) : undefined;
182
+ if (docRef === undefined)
183
+ continue;
184
+ // RAW domain strings — the presenter wraps the free-text fields.
185
+ documentReferenceCandidates.push({
186
+ transaction_id: tx.id,
187
+ date: tx.date,
188
+ amount: tx.amount,
189
+ description: tx.description ?? undefined,
190
+ bank_account_name: tx.bank_account_name ?? undefined,
191
+ invoice: {
192
+ type: "purchase_invoice",
193
+ id: docRef.inv.id,
194
+ number: docRef.inv.number,
195
+ client_name: docRef.inv.client_name,
196
+ gross_price: docRef.inv.gross_price ?? 0,
197
+ payment_status: docRef.inv.payment_status ?? "NOT_PAID",
198
+ },
199
+ matched_reference: docRef.matchedReference,
200
+ match_reasons: candidate.match_reasons,
201
+ });
202
+ }
174
203
  const crossCurrency = (bestMatch.match_reasons.includes("exact_base_amount") ||
175
204
  bestMatch.match_reasons.includes("cross_currency_conflict")) &&
176
205
  !bestMatch.match_reasons.includes("exact_amount");
@@ -718,9 +747,12 @@ export async function runInterAccountMatching(api, input, opts) {
718
747
  const errors = [];
719
748
  const consumedTxIds = new Set();
720
749
  const blockedOneSidedTxIds = new Set();
750
+ const deleteProposedMirrors = input.deleteProposedMirrors === true;
751
+ const mirrorDeleteActions = [];
721
752
  function finish(companyClientsId) {
722
- const planCommandProjections = buildInterAccountPlanCommandProjections(confirmActions, companyClientsId);
723
- const normalizedArgs = stripUndefinedDeep({ max_date_gap: maxGap, target_accounts_dimensions_id });
753
+ const planCommandProjections = buildInterAccountPlanCommandProjections(confirmActions, companyClientsId, mirrorDeleteActions);
754
+ const normalizedArgs = stripUndefinedDeep({ max_date_gap: maxGap, target_accounts_dimensions_id,
755
+ ...(deleteProposedMirrors ? { delete_proposed_mirrors: true } : {}) });
724
756
  const fingerprint = interAccountFingerprint({ normalizedArgs, planCommandProjections, skippedAlreadyHandled,
725
757
  ambiguousPairs, ambiguousRefless, crossCurrencyPairs });
726
758
  const companyLegalName = (invoiceInfo.invoice_company_name ?? "").trim();
@@ -728,7 +760,8 @@ export async function runInterAccountMatching(api, input, opts) {
728
760
  ? ["Company name has no legal form; own-transfer detection relies on IBANs only."] : [];
729
761
  return { totalUnconfirmed: unconfirmed.length, invoiceInfo, dimensionToIban, dimensionToTitle,
730
762
  dimensionToAccountsId, matchedPairs, matchedOneSided, ambiguousPairs, skippedAlreadyHandled,
731
- ambiguousRefless, crossCurrencyPairs, errors, confirmActions, companyClientsId, normalizedArgs,
763
+ ambiguousRefless, crossCurrencyPairs, errors, confirmActions,
764
+ ...(deleteProposedMirrors ? { mirrorDeleteActions } : {}), companyClientsId, normalizedArgs,
732
765
  fingerprint, ...(warnings.length > 0 ? { warnings } : {}) };
733
766
  }
734
767
  // With no pending bank rows there is nothing to pair, deduplicate or confirm.
@@ -1495,6 +1528,18 @@ export async function runInterAccountMatching(api, input, opts) {
1495
1528
  row.mirror_proposal = proposal;
1496
1529
  row.next_action = { tool: "delete_transaction", args: { id: row.transaction_id }, approval_required: true };
1497
1530
  row.reason = `Already journalized from the other account side: journal #${proposal.journal_id} books confirmed transaction ${proposal.counterpart_transaction_id}, and this PROJECT row is its likely mirror. Verify against the bank statements, then delete it with delete_transaction only after the owner explicitly approves; reconcile never deletes it automatically.`;
1531
+ if (deleteProposedMirrors) {
1532
+ // Opt-in: the reviewed plan carries this deletion; execute re-verifies it.
1533
+ mirrorDeleteActions.push({
1534
+ transactionId: row.transaction_id,
1535
+ journalId: proposal.journal_id,
1536
+ counterpartTransactionId: proposal.counterpart_transaction_id,
1537
+ targetDimensionId: row.target_dimension_id,
1538
+ material: snapshotBankTransaction(allTx.get(row.transaction_id)),
1539
+ });
1540
+ row.reason = `Already journalized from the other account side: journal #${proposal.journal_id} books confirmed transaction ${proposal.counterpart_transaction_id}, and this PROJECT row is its likely mirror. delete_proposed_mirrors put its deletion in this plan: verify against the bank statements before approving execute.`;
1541
+ delete row.next_action;
1542
+ }
1498
1543
  }
1499
1544
  const companyClientsId = await resolveCompanyClientsId();
1500
1545
  for (const action of confirmActions) {
@@ -1535,7 +1580,7 @@ export async function prepareInterAccount(api, runtimeSafetyContext, input) {
1535
1580
  const match = await runInterAccountMatching(api, input);
1536
1581
  if (input.mintPlanHandles === false)
1537
1582
  return { match };
1538
- const planCommandProjections = buildInterAccountPlanCommandProjections(match.confirmActions, match.companyClientsId);
1583
+ const planCommandProjections = buildInterAccountPlanCommandProjections(match.confirmActions, match.companyClientsId, match.mirrorDeleteActions);
1539
1584
  const planHandle = runtimeSafetyContext.planStore.issue(BANK_RECONCILIATION_PLAN_DOMAIN, buildReconciliationExecutionPlanInput({
1540
1585
  normalizedArgs: match.normalizedArgs,
1541
1586
  sourceIdentities: [],
@@ -1550,6 +1595,7 @@ export async function prepareInterAccount(api, runtimeSafetyContext, input) {
1550
1595
  total_unconfirmed: match.totalUnconfirmed,
1551
1596
  matched_pairs: match.matchedPairs.length,
1552
1597
  matched_one_sided: match.matchedOneSided.length,
1598
+ ...(match.mirrorDeleteActions !== undefined ? { proposed_mirror_deletes: match.mirrorDeleteActions.length } : {}),
1553
1599
  },
1554
1600
  totals: {},
1555
1601
  exclusions: [...match.skippedAlreadyHandled, ...match.ambiguousRefless, ...match.crossCurrencyPairs].map(row => stripUndefinedDeep(row)),
@@ -1780,8 +1826,68 @@ export async function executeInterAccount(api, runtimeSafetyContext, input) {
1780
1826
  });
1781
1827
  }
1782
1828
  }
1829
+ for (const action of match.mirrorDeleteActions ?? []) {
1830
+ // Every read gates the delete: the row must still be the reviewed PROJECT
1831
+ // row, and the journal must still book its confirmed counterpart.
1832
+ const verifyMirror = async () => {
1833
+ api.transactions.invalidateListCache();
1834
+ const row = await api.transactions.get(action.transactionId);
1835
+ if (!row || !isProjectTransaction(row))
1836
+ return "mirror_not_project";
1837
+ if (!compareBankTransactionMaterial(action.material, row).ok)
1838
+ return "mirror_material_drift";
1839
+ const counterpart = await api.transactions.get(action.counterpartTransactionId);
1840
+ if (!counterpart || counterpart.is_deleted || counterpart.status !== "CONFIRMED"
1841
+ || counterpart.accounts_dimensions_id !== action.targetDimensionId
1842
+ || !sameAmountToTheCent(counterpart.amount, action.material.amount ?? Number.NaN))
1843
+ return "mirror_counterpart_drift";
1844
+ api.journals.invalidateListCache();
1845
+ const journal = (await api.journals.listAllWithPostings()).find(j => j.id === action.journalId);
1846
+ if (!journal || journal.is_deleted || journal.registered !== true || journal.operation_type !== "TRANSACTION"
1847
+ || journal.operations_id !== action.counterpartTransactionId)
1848
+ return "mirror_journal_drift";
1849
+ return undefined;
1850
+ };
1851
+ commands.push({
1852
+ id: reconDeleteProposedMirrorCommandId(action.transactionId),
1853
+ category: RECON_DELETE_PROPOSED_MIRROR_CATEGORY,
1854
+ prepare: async () => {
1855
+ const drift = await verifyMirror();
1856
+ if (drift)
1857
+ return { outcome: "drift", error_code: drift };
1858
+ // The journal read may await another actor: make the row the final read.
1859
+ api.transactions.invalidateListCache();
1860
+ const row = await api.transactions.get(action.transactionId);
1861
+ if (!row || !isProjectTransaction(row) || !compareBankTransactionMaterial(action.material, row).ok) {
1862
+ return { outcome: "drift", error_code: "mirror_material_drift" };
1863
+ }
1864
+ return { outcome: "ready" };
1865
+ },
1866
+ mutate: async () => {
1867
+ try {
1868
+ await api.transactions.delete(action.transactionId);
1869
+ logAudit({
1870
+ tool: "reconcile_inter_account_transfers", action: "DELETED", entity_type: "transaction",
1871
+ entity_id: action.transactionId,
1872
+ summary: `Deleted proposed mirror row ${action.transactionId} of confirmed transaction ${action.counterpartTransactionId} (journal ${action.journalId})`,
1873
+ details: { journal_id: action.journalId, counterpart_transaction_id: action.counterpartTransactionId, delete_proposed_mirrors: true },
1874
+ });
1875
+ return { outcome: "completed", known_objects: [{ entity_type: "transaction", entity_id: action.transactionId, outcome: "deleted" }] };
1876
+ }
1877
+ catch (err) {
1878
+ if (err instanceof MutationIndeterminateError)
1879
+ return { outcome: "indeterminate", error_code: "mutation_outcome_unknown" };
1880
+ return { outcome: "failed", error_code: "delete_failed", mutation_occurred: false };
1881
+ }
1882
+ },
1883
+ });
1884
+ }
1783
1885
  const executionReport = await executeReconciliationCommands(commands);
1784
1886
  const completedIds = new Set(executionReport.command_partitions.completed.map(item => item.command_id));
1887
+ for (const row of match.skippedAlreadyHandled) {
1888
+ if (completedIds.has(reconDeleteProposedMirrorCommandId(row.transaction_id)))
1889
+ row.mirror_deleted = true;
1890
+ }
1785
1891
  for (const pair of match.matchedPairs) {
1786
1892
  const action = match.confirmActions.find(action => action.confirmedTxId === pair.outgoing_transaction_id);
1787
1893
  if (action?.registrationJournalId != null)