From 24a5a87db9b35db1ba42e40ce1c9f3d2f8d07c2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jernej=20Kavka=20=28JK=29=20=5BSSW=20=C2=B7=20Microsoft=20?= =?UTF-8?q?MVP=5D?= Date: Sat, 27 Jun 2026 21:11:19 +1000 Subject: [PATCH 1/3] feat(accounting-guides): add invoice/timesheet locking + prepaid/receipt/recurring diagnostics Five new accounting guides driven by recurring real-world diagnostic patterns (scrubbed to Northwind placeholders): - invoice-timesheet-locked: locked invoice vs timesheet-locked-by-invoice vs sync - prepaid-reconciliation-delta: chase a non-zero prepaid delta - credit-note-prepaid-credit: why a credit note didn't reduce the prepaid balance - receipt-allocation: payment applied to the wrong invoice / split - recurring-invoice-gap: stalled/overdue recurring template Cross-link locked timesheets from the dev saved-timesheets-wrong guide so `tp dev guide` surfaces it too. All commands verified against the real tp surface; adds exact-match ranking tests for each new guide. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../accounting/credit-note-prepaid-credit.md | 36 +++++++++ guides/accounting/index.json | 77 +++++++++++++++++++ guides/accounting/invoice-timesheet-locked.md | 42 ++++++++++ .../prepaid-reconciliation-delta.md | 32 ++++++++ guides/accounting/receipt-allocation.md | 30 ++++++++ guides/accounting/recurring-invoice-gap.md | 28 +++++++ guides/dev/index.json | 2 +- guides/dev/saved-timesheets-wrong.md | 5 ++ .../Features/Guides/GuideRankingTests.cs | 15 ++++ 9 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 guides/accounting/credit-note-prepaid-credit.md create mode 100644 guides/accounting/invoice-timesheet-locked.md create mode 100644 guides/accounting/prepaid-reconciliation-delta.md create mode 100644 guides/accounting/receipt-allocation.md create mode 100644 guides/accounting/recurring-invoice-gap.md diff --git a/guides/accounting/credit-note-prepaid-credit.md b/guides/accounting/credit-note-prepaid-credit.md new file mode 100644 index 0000000..49d7148 --- /dev/null +++ b/guides/accounting/credit-note-prepaid-credit.md @@ -0,0 +1,36 @@ +# Credit Note Not Reducing Prepaid Balance + +Use this guide when a credit note was issued for a prepaid client but the +prepaid **Credited** bucket (in `tp prepaid summary`) did not move. + +The prepaid summary only counts a credit note toward `credited` when **both** are +true: + +- `isCreditingInvoice == true` (it credits a specific invoice, not a standalone + credit), and +- `associatedInvoiceId` equals the **prepaid invoice** you are looking at. + +A standalone credit note, or one that credits a *different* invoice, will not +change this invoice's prepaid balance — by design. + +Useful evidence (read-only; needs `tp feature accounting`): + +```bash +tp creditnote list --client NWIND --tenant northwind --env prod --json \ + | jq '.[] | {id, total, isCreditingInvoice, associatedInvoiceId}' +tp prepaid summary 142 --tenant northwind --env prod --json \ + | jq '{creditingCreditNoteCount, credited}' +``` + +Check: + +- **`isCreditingInvoice`** — if `false`, it is a standalone credit and won't + reduce any invoice's prepaid balance. +- **`associatedInvoiceId`** — must equal the prepaid invoice (`142` here). A + credit note pointed at another invoice reduces *that* invoice instead. +- **`creditingCreditNoteCount`** — confirms how many credit notes the summary + actually counted; if it is lower than you expect, one of the above is the + reason. + +If the credit note targets the right invoice but the prepaid total still looks +wrong, move on to the `prepaid-reconciliation-delta` guide. diff --git a/guides/accounting/index.json b/guides/accounting/index.json index d15dc5b..498a51b 100644 --- a/guides/accounting/index.json +++ b/guides/accounting/index.json @@ -89,6 +89,83 @@ ], "mcpTools": ["ListInvoices", "ListPaidReceipts", "ListCreditNotes"], "skills": ["timepro-accounting-cli"] + }, + { + "slug": "invoice-timesheet-locked", + "file": "invoice-timesheet-locked.md", + "title": "Locked invoice or timesheet", + "description": "Diagnose why a timesheet can't be edited or moved, or an invoice can't be updated, posted, or reallocated — invoice lock vs timesheet-locked-by-invoice vs external sync.", + "keywords": ["locked", "lock", "locked timesheet", "locked invoice", "cannot edit invoice", "cannot move timesheet", "reallocate timesheet", "locked period", "posted invoice"], + "commands": [ + "tp accounting guide --use-case \"locked invoice or timesheet\" --json", + "tp invoice get --json", + "tp invoice timesheets --json", + "tp invoice timesheets --writeoff --json", + "tp creditnote list --client --json" + ], + "mcpTools": ["GetInvoice", "GetInvoiceTimesheets", "ListCreditNotes"], + "skills": ["timepro-accounting-cli"] + }, + { + "slug": "prepaid-reconciliation-delta", + "file": "prepaid-reconciliation-delta.md", + "title": "Prepaid reconciliation delta", + "description": "Chase a non-zero prepaid reconciliation delta (original minus drawn-down minus credited not equal to remaining) across drawdown rows, write-offs, and credit notes.", + "keywords": ["reconciliation delta", "prepaid delta", "drawdown delta", "prepaid reconciliation", "does not reconcile", "prepaid balance wrong"], + "commands": [ + "tp accounting guide --use-case \"prepaid reconciliation delta\" --json", + "tp prepaid summary --json", + "tp invoice timesheets --json", + "tp invoice timesheets --writeoff --json", + "tp creditnote list --client --json" + ], + "mcpTools": ["GetInvoiceTimesheets", "ListCreditNotes"], + "skills": ["timepro-accounting-cli"] + }, + { + "slug": "credit-note-prepaid-credit", + "file": "credit-note-prepaid-credit.md", + "title": "Credit note not reducing prepaid balance", + "description": "Explain why a credit note did not change a prepaid invoice's credited bucket — only crediting credit notes with a matching associated invoice count.", + "keywords": ["credit note prepaid", "credit not applied", "iscreditinginvoice", "standalone credit note", "credit balance unchanged", "associated invoice"], + "commands": [ + "tp accounting guide --use-case \"credit note not reducing prepaid\" --json", + "tp creditnote list --client --json", + "tp prepaid summary --json" + ], + "mcpTools": ["ListCreditNotes"], + "skills": ["timepro-accounting-cli"] + }, + { + "slug": "receipt-allocation", + "file": "receipt-allocation.md", + "title": "Receipt allocated to the wrong invoice", + "description": "Diagnose a payment that left an invoice outstanding or split incorrectly using receipt allocations and the receipt-to-invoice direction.", + "keywords": ["receipt allocation", "payment allocation", "paid but outstanding", "misallocated payment", "split payment", "wrong invoice payment"], + "commands": [ + "tp accounting guide --use-case \"receipt allocated to wrong invoice\" --json", + "tp receipt get --json", + "tp invoice receipts --json", + "tp invoice get --json", + "tp receipt outstanding --json" + ], + "mcpTools": [], + "skills": ["timepro-accounting-cli"] + }, + { + "slug": "recurring-invoice-gap", + "file": "recurring-invoice-gap.md", + "title": "Recurring invoice not generating", + "description": "Diagnose a stalled or overdue recurring invoice template using isActive, lastInvEndDate, nextInvoiceDate, and the invoices it actually generated.", + "keywords": ["recurring invoice", "recurring not generating", "stalled recurring", "missing recurring invoice", "overdue recurring", "recurring template"], + "commands": [ + "tp accounting guide --use-case \"recurring invoice not generating\" --json", + "tp recurring list --client --outdated --json", + "tp recurring get --json", + "tp invoice list --query --recurring --json" + ], + "mcpTools": ["ListInvoices"], + "skills": ["timepro-accounting-cli"] } ] } diff --git a/guides/accounting/invoice-timesheet-locked.md b/guides/accounting/invoice-timesheet-locked.md new file mode 100644 index 0000000..ead5e8e --- /dev/null +++ b/guides/accounting/invoice-timesheet-locked.md @@ -0,0 +1,42 @@ +# Locked Invoice or Timesheet + +Use this guide when a timesheet **cannot be edited or moved**, or an invoice +**cannot be updated, posted, or reallocated** — even though the underlying data +looks correct. The usual cause is a *lock*, not bad data. + +Two distinct locks: + +- **Timesheet locked by an invoice** — once a timesheet is allocated to an + invoice it is locked; only its location and description can change. Trying to + move it to another invoice or edit its hours/rate fails. +- **Invoice locked** — an invoice can be locked by posting, a closed accounting + period, or external (Xero) sync state. A locked invoice rejects updates and + reallocations. + +Useful evidence (read-only; needs `tp feature accounting`): + +```bash +tp invoice get 142 --tenant northwind --env prod --json \ + | jq '{isLocked, isRecurring, sellTotal, paidAmt, osAmt, externalSyncStatus}' +tp invoice timesheets 142 --tenant northwind --env prod --json # rows allocated to (and thus locked by) the invoice +tp invoice timesheets 142 --tenant northwind --env prod --writeoff --json # written-off rows +tp creditnote list --client NWIND --tenant northwind --env prod --json # a credit note may be the only way to adjust a locked invoice +``` + +Check, in order: + +- **Is the invoice `isLocked`?** Posted / period-locked / sync-locked invoices + cannot be edited — the fix is to reverse/unlock the invoice or credit it, not + to edit the row. +- **Is the timesheet locked by an invoice?** If it appears in `tp invoice + timesheets`, it is allocated and locked — to move it you must first take it off + that invoice (or write it off). +- **`externalSyncStatus`** — a row mid-sync to Xero can present as locked; see + the `external-sync-bug` dev guide / `external-comparison` accounting guide. + +Resolving a lock is a **write**: unlocking a period, reversing/crediting an +invoice, or reallocating timesheets all mutate financial data. On production, +gather the evidence read-only and get explicit permission before any change. + +> Developers hit this too — a locked timesheet only allows location/description +> edits. It is also reachable from `tp dev guide` via `saved-timesheets-wrong`. diff --git a/guides/accounting/prepaid-reconciliation-delta.md b/guides/accounting/prepaid-reconciliation-delta.md new file mode 100644 index 0000000..017126a --- /dev/null +++ b/guides/accounting/prepaid-reconciliation-delta.md @@ -0,0 +1,32 @@ +# Prepaid Reconciliation Delta + +Use this guide when `tp prepaid summary` reports a non-zero **reconciliation +delta** — the warning that `original − drawnDown − credited ≠ remaining`. The +delta should always be zero; a non-zero value means a drawdown, write-off, or +credit is missing or double-counted. + +Useful evidence (read-only; needs `tp feature accounting`): + +```bash +tp prepaid summary 142 --tenant northwind --env prod --json \ + | jq '{original, drawnDown, credited, remaining, reconciliationDeltaExGst}' +tp invoice timesheets 142 --tenant northwind --env prod --json # timesheets drawn down against the prepaid invoice +tp invoice timesheets 142 --tenant northwind --env prod --writeoff --json # write-offs that change the drawn-down total +tp creditnote list --client NWIND --tenant northwind --env prod --json # credits that should reduce the balance +``` + +Check: + +- **Sum the parts.** `drawnDown` should equal the allocated timesheet value; + `credited` should equal the crediting credit notes. Recompute and find which + bucket is off. +- **Write-offs** — a written-off row still affects drawdown; confirm `--writeoff` + rows are accounted for. +- **Credits** — only credit notes with `isCreditingInvoice == true` and + `associatedInvoiceId` equal to this invoice count toward `credited`. If a + credit note exists but the delta persists, see the + `credit-note-prepaid-credit` guide. + +The delta is a *defined error condition* (it is asserted to be zero in tests), so +treat any non-zero value as a real discrepancy to chase down, not a rounding +artefact. diff --git a/guides/accounting/receipt-allocation.md b/guides/accounting/receipt-allocation.md new file mode 100644 index 0000000..feadc2b --- /dev/null +++ b/guides/accounting/receipt-allocation.md @@ -0,0 +1,30 @@ +# Receipt Allocated to the Wrong Invoice + +Use this guide when a client has paid but an invoice still shows an outstanding +balance, or a single payment looks split across invoices incorrectly. A receipt +carries **allocations** (one receipt can be applied across many invoices), so a +mis-allocation leaves one invoice short while another is over-paid. + +Useful evidence (read-only; needs `tp feature accounting`): + +```bash +tp receipt get 108 --tenant northwind --env prod --json | jq '.allocations' # where this payment actually landed +tp invoice receipts 142 --tenant northwind --env prod --json # receipts applied to the suspect invoice +tp invoice get 142 --tenant northwind --env prod --json | jq '{sellTotal, paidAmt, osAmt}' +tp receipt outstanding NWIND --tenant northwind --env prod --json # aged view across the client's invoices +``` + +Check: + +- **Receipt allocations vs invoice.** Do the receipt's `allocations` add up to + the receipt total, and is the suspect invoice in the list with the expected + amount? +- **Under-allocation.** If the invoice's `osAmt` is non-zero but the client has + paid, the receipt was likely applied to a different invoice — compare against + `tp receipt outstanding NWIND`. +- **Split correctness.** For a payment spread across invoices, confirm each + allocation matches the intended invoice; a wrong split shows up as one invoice + over-paid and another still outstanding. + +Re-allocating a receipt is a **write**. On production, confirm the mis-allocation +read-only first, then get permission before moving the payment. diff --git a/guides/accounting/recurring-invoice-gap.md b/guides/accounting/recurring-invoice-gap.md new file mode 100644 index 0000000..74ca092 --- /dev/null +++ b/guides/accounting/recurring-invoice-gap.md @@ -0,0 +1,28 @@ +# Recurring Invoice Not Generating + +Use this guide when a client has not received a scheduled invoice, or a recurring +invoice template appears stalled. The template's `isActive`, `lastInvEndDate`, +and `nextInvoiceDate` tell the story. + +Useful evidence (read-only; needs `tp feature accounting`): + +```bash +tp recurring list --client NWIND --tenant northwind --env prod --outdated --json # templates overdue to generate +tp recurring get 7 --tenant northwind --env prod --json \ + | jq '{isActive, lastInvEndDate, nextInvoiceDate}' +tp invoice list --query NWIND --recurring --tenant northwind --env prod --json # invoices actually generated from recurring templates +``` + +Check: + +- **`isActive`** — an inactive template will never generate; that may be + intentional (paused) or an accidental deactivation. +- **`nextInvoiceDate` in the past** with no matching generated invoice → the + scheduler did not run or the template is stuck; confirm by listing recurring + invoices with `tp invoice list --recurring`. +- **`lastInvEndDate`** — compare to the expected cadence; a gap between + `lastInvEndDate` and `nextInvoiceDate` larger than the cycle suggests a missed + run. + +If `--outdated` returns the template but invoices still aren't appearing, the +generation job (not the template data) is the next thing to investigate. diff --git a/guides/dev/index.json b/guides/dev/index.json index 466d4c7..faa8afb 100644 --- a/guides/dev/index.json +++ b/guides/dev/index.json @@ -36,7 +36,7 @@ "file": "saved-timesheets-wrong.md", "title": "Saved timesheets wrong", "description": "Compare saved timesheet rows, wider date ranges, query output, rate enrichment, and create/update/accept boundaries.", - "keywords": ["timesheet", "timesheets", "saved timesheets", "duplicate", "missing rows", "accept", "create", "update"], + "keywords": ["timesheet", "timesheets", "saved timesheets", "duplicate", "missing rows", "accept", "create", "update", "locked timesheet", "cannot edit timesheet"], "commands": [ "tp dev guide --use-case \"saved timesheets wrong\" --json", "tp ts get --tenant --env --emp-id --json", diff --git a/guides/dev/saved-timesheets-wrong.md b/guides/dev/saved-timesheets-wrong.md index 9bb9640..1e42307 100644 --- a/guides/dev/saved-timesheets-wrong.md +++ b/guides/dev/saved-timesheets-wrong.md @@ -12,3 +12,8 @@ tp query --from 2026-03-01 --to 2026-03-31 --tenant northwind --env staging --em Check create/update/accept boundaries, rate enrichment, category/billable type, and whether the UI read model matches the API output. + +If a timesheet **can't be edited or moved at all**, it is probably **locked** by +the invoice it is allocated to (a locked timesheet only allows location and +description edits). That is an invoice/lock problem, not a saved-row problem — see +`tp accounting guide --use-case "locked invoice or timesheet"`. diff --git a/tests/SSW.TimePro.Cli.Tests/Features/Guides/GuideRankingTests.cs b/tests/SSW.TimePro.Cli.Tests/Features/Guides/GuideRankingTests.cs index c601c80..b90f0b2 100644 --- a/tests/SSW.TimePro.Cli.Tests/Features/Guides/GuideRankingTests.cs +++ b/tests/SSW.TimePro.Cli.Tests/Features/Guides/GuideRankingTests.cs @@ -93,6 +93,21 @@ public void AccountingGuide_FindsCommonReportGuides(string useCase, string expec guide.RecommendedSkills.Should().Equal("timepro-accounting-cli"); } + [Theory] + [InlineData("locked invoice", "Locked invoice or timesheet")] + [InlineData("reconciliation delta", "Prepaid reconciliation delta")] + [InlineData("credit note prepaid", "Credit note not reducing prepaid balance")] + [InlineData("receipt allocation", "Receipt allocated to the wrong invoice")] + [InlineData("recurring invoice", "Recurring invoice not generating")] + public void AccountingGuide_FindsNewDiagnosticGuides(string useCase, string expectedTitle) + { + var guide = AccountingGuide.For(useCase); + + guide.MatchingGuides.Should().ContainSingle(match => match.Title == expectedTitle); + guide.MatchingGuides[0].MatchType.Should().Be("exact"); + guide.RecommendedSkills.Should().Equal("timepro-accounting-cli"); + } + [Fact] public void GuideRanking_ReturnsAllTopicsWithoutQuery() { From 034e99887973451f0dec09eb636626295064ee5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jernej=20Kavka=20=28JK=29=20=5BSSW=20=C2=B7=20Microsoft=20?= =?UTF-8?q?MVP=5D?= Date: Sat, 27 Jun 2026 22:53:19 +1000 Subject: [PATCH 2/3] Apply Copilot review: receipt-allocation mcpTools, robust ranking test, wording - receipt-allocation guide: populate mcpTools with the real accounting MCP tools (GetReceiptDetail, GetInvoiceReceipts, GetClientOutstanding). - GuideRankingTests: assert MatchType on the matched element (fold into ContainSingle) instead of indexing [0], so a tie can't validate the wrong guide. - saved-timesheets-wrong: 'invoice/lock problem' -> 'invoice lock problem'. Co-Authored-By: Claude Opus 4.8 (1M context) --- guides/accounting/index.json | 2 +- guides/dev/saved-timesheets-wrong.md | 2 +- .../Features/Guides/GuideRankingTests.cs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/guides/accounting/index.json b/guides/accounting/index.json index 498a51b..3d50914 100644 --- a/guides/accounting/index.json +++ b/guides/accounting/index.json @@ -149,7 +149,7 @@ "tp invoice get --json", "tp receipt outstanding --json" ], - "mcpTools": [], + "mcpTools": ["GetReceiptDetail", "GetInvoiceReceipts", "GetClientOutstanding"], "skills": ["timepro-accounting-cli"] }, { diff --git a/guides/dev/saved-timesheets-wrong.md b/guides/dev/saved-timesheets-wrong.md index 1e42307..dfe5b0f 100644 --- a/guides/dev/saved-timesheets-wrong.md +++ b/guides/dev/saved-timesheets-wrong.md @@ -15,5 +15,5 @@ and whether the UI read model matches the API output. If a timesheet **can't be edited or moved at all**, it is probably **locked** by the invoice it is allocated to (a locked timesheet only allows location and -description edits). That is an invoice/lock problem, not a saved-row problem — see +description edits). That is an invoice lock problem, not a saved-row problem — see `tp accounting guide --use-case "locked invoice or timesheet"`. diff --git a/tests/SSW.TimePro.Cli.Tests/Features/Guides/GuideRankingTests.cs b/tests/SSW.TimePro.Cli.Tests/Features/Guides/GuideRankingTests.cs index b90f0b2..441dd01 100644 --- a/tests/SSW.TimePro.Cli.Tests/Features/Guides/GuideRankingTests.cs +++ b/tests/SSW.TimePro.Cli.Tests/Features/Guides/GuideRankingTests.cs @@ -103,8 +103,8 @@ public void AccountingGuide_FindsNewDiagnosticGuides(string useCase, string expe { var guide = AccountingGuide.For(useCase); - guide.MatchingGuides.Should().ContainSingle(match => match.Title == expectedTitle); - guide.MatchingGuides[0].MatchType.Should().Be("exact"); + guide.MatchingGuides.Should().ContainSingle(match => + match.Title == expectedTitle && match.MatchType == "exact"); guide.RecommendedSkills.Should().Equal("timepro-accounting-cli"); } From 153d2ec71c8619b4e0b0bd083e46a074683332e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jernej=20Kavka=20=28JK=29=20=5BSSW=20=C2=B7=20Microsoft=20?= =?UTF-8?q?MVP=5D?= Date: Sat, 27 Jun 2026 23:18:06 +1000 Subject: [PATCH 3/3] Apply Copilot re-review: align mcpTools with guide commands - prepaid-reconciliation-delta + credit-note-prepaid-credit: add GetPrepaidStatus - recurring-invoice-gap: add ListRecurringInvoices, GetRecurringInvoice All verified to exist in AccountingMcpTools.cs. Co-Authored-By: Claude Opus 4.8 (1M context) --- guides/accounting/index.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/guides/accounting/index.json b/guides/accounting/index.json index 3d50914..0431d5b 100644 --- a/guides/accounting/index.json +++ b/guides/accounting/index.json @@ -119,7 +119,7 @@ "tp invoice timesheets --writeoff --json", "tp creditnote list --client --json" ], - "mcpTools": ["GetInvoiceTimesheets", "ListCreditNotes"], + "mcpTools": ["GetPrepaidStatus", "GetInvoiceTimesheets", "ListCreditNotes"], "skills": ["timepro-accounting-cli"] }, { @@ -133,7 +133,7 @@ "tp creditnote list --client --json", "tp prepaid summary --json" ], - "mcpTools": ["ListCreditNotes"], + "mcpTools": ["ListCreditNotes", "GetPrepaidStatus"], "skills": ["timepro-accounting-cli"] }, { @@ -164,7 +164,7 @@ "tp recurring get --json", "tp invoice list --query --recurring --json" ], - "mcpTools": ["ListInvoices"], + "mcpTools": ["ListRecurringInvoices", "GetRecurringInvoice", "ListInvoices"], "skills": ["timepro-accounting-cli"] } ]