Xero invoice path: document push + Finish Job — job-xero-invoice green (13 of 40) - #47
Conversation
…ods, endpoints Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…donly fabrication Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…urfacing - Clamp provider-driven failure statuses to the declared response map (429/503 from live Xero crashed ninja with ConfigError; 401 collided with the XeroAuthRequiredOut shape) - Never store Xero's zero-UUID sentinel on a PO; page through the listing when recovering the real id - apiErrorMessage reads the Xero document endpoints' error key, so calc and configuration guidance reaches the user - Invoice list load failure no longer renders as the empty state; refetch failure signal moved onto the resolved result (TanStack v5 never rejects) - job-xero-invoice spec creates its own job (sharedEditJobUrl is read-only by contract); readonly PO tripwire restored; ledger entries for the PO-delete 500 fix and same-request fully_invoiced recalc Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Warning Review limit reached
Next review available in:24 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (7)
📝 WalkthroughWalkthroughThis PR adds Finish Job financial and checklist APIs, Xero invoice and purchase-order synchronization, provider contracts, read-only behavior, and frontend Finish Job and invoice-management components with unit, API, and end-to-end tests. ChangesFinish Job and Xero workflows
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant JobFinishTab
participant JobAPI
participant FinishJobSummary
participant JobService
JobFinishTab->>JobAPI: GET finish summary and checklist
JobAPI->>FinishJobSummary: calculate financial totals
FinishJobSummary-->>JobAPI: return summary
JobAPI-->>JobFinishTab: return JobFinishResponse
JobFinishTab->>JobAPI: PATCH checklist fields
JobAPI->>JobService: update checklist
JobService-->>JobAPI: return updated job
sequenceDiagram
participant JobInvoiceCard
participant XeroAPI
participant XeroInvoiceManager
participant XeroAccountingProvider
JobInvoiceCard->>XeroAPI: submit invoice request
XeroAPI->>XeroInvoiceManager: create document
XeroInvoiceManager->>XeroAccountingProvider: create InvoicePayload
XeroAccountingProvider-->>XeroInvoiceManager: return DocumentResult
XeroInvoiceManager-->>XeroAPI: return persisted invoice result
XeroAPI-->>JobInvoiceCard: return success or error
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 14
🧹 Nitpick comments (5)
frontend/src/api/index.ts (1)
81-81: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRemove code-narration comments or document their constraint.
Both comments describe nearby code without stating the rejected alternative and the factual constraint.
frontend/src/api/index.ts#L81-L81: Remove the export-group narration, or explain why feature code must import these generated operations throughfrontend/src/api/index.ts.frontend/src/features/job/JobFinishTab.tsx#L157-L157: Remove the ticket-heading comment. It does not document a runtime or business constraint.As per coding guidelines, “Comments must document the rejected obvious alternative and the factual constraint that rejected it; remove code narration and review-feedback echoes.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/src/api/index.ts` at line 81, Remove the code-narration comments at frontend/src/api/index.ts:81-81 and frontend/src/features/job/JobFinishTab.tsx:157-157; no replacement documentation is needed because neither comment states a factual constraint or rejected alternative.Source: Coding guidelines
apps/xero/tests/test_invoice_manager.py (1)
86-138: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a delete happy-path test.
TestErrorContractcovers the three failure paths ofdelete_document. No test covers the success path, so nothing asserts that the localInvoicerow is removed, thatrecalculate_job_invoicing_stateruns, and that theinvoice_deletedjob event records the invoice number. That recalculation is the same same-request effect the create test protects at Line 256-258, and the Finish Job tab depends on it.Do you want me to write this test?
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/xero/tests/test_invoice_manager.py` around lines 86 - 138, Add a success-case test in TestErrorContract for delete_document using a configured provider and Xero invoice ID, then assert the local Invoice is deleted, recalculate_job_invoicing_state is called, and the invoice_deleted job event records the invoice number. Keep the test focused on the same-request effects of the happy path.apps/xero/tests/test_po_manager.py (1)
98-109: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCover the write direction of the zero-UUID sentinel too.
This test covers the read direction: a stored sentinel routes to create. The module docstring at Line 4-5 names "storing Xero's zero-UUID sentinel as a real id" as the expensive silent failure, and that is the write direction, guarded by
_save_po_with_xero_dataatapps/xero/documents/po.pyLine 151.Add a test where the provider returns
DocumentResult(success=True, external_id=ZERO_UUID)and assert thatpo.xero_idstaysNoneaftersync_to_xero. This also pins the behavior discussed onapps/xero/provider.pyLine 280-314.Do you want me to write this test?
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/xero/tests/test_po_manager.py` around lines 98 - 109, Extend test_zero_uuid_is_treated_as_unsynced to cover the write path: configure the provider’s successful DocumentResult with external_id=ZERO_UUID, run manager.sync_to_xero(), then refresh or inspect po.xero_id and assert it remains None. Preserve the existing create-path assertion while exercising _save_po_with_xero_data’s zero-UUID handling.apps/xero/api.py (2)
317-328: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winType the clamp parameter as the contract, not as
object.
XeroDocumentResponsedeclaresstatus. Annotating the parameterobjectdiscards that contract and forces a runtime comparison against an untyped value. Declareint | Noneso mypy checks every call site.♻️ Proposed signature change
-def _document_error_status(status: object) -> int:+def _document_error_status(status: int | None) -> int:As per coding guidelines: "Treat type annotations as data contracts: use named types such as dataclasses, TypedDicts, or Protocols for complex inline types."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/xero/api.py` around lines 317 - 328, Update the _document_error_status parameter annotation from object to int | None, matching the status contract declared by XeroDocumentResponse. Keep the existing 404 handling and 400 fallback unchanged, and let mypy validate all call sites against the narrowed type.Source: Coding guidelines
358-366: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract the repeated Xero-auth 401 block.
The same
get_valid_token()guard andXeroAuthRequiredOutpayload appear in all four new endpoints (lines 358-366, 458-466, 543-551, 612-620). The two purchase-order endpoints also repeat the identical lookup and supplier guard. One helper per repeated block keeps the message and status in step.♻️ Proposed helper
def_xero_auth_required() ->Status[XeroAuthRequiredOut]: """Build the one 401 body every Xero document endpoint returns."""returnStatus( 401, XeroAuthRequiredOut( success=False, redirect_to_auth=True, message="Your Xero session has expired. Please log in again.", ), )As per coding guidelines: "Use one implementation per concept; search before implementing and extend a near-match instead of creating a parallel implementation."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/xero/api.py` around lines 358 - 366, Extract the repeated get_valid_token() failure response into a shared _xero_auth_required() helper returning the existing 401 Status[XeroAuthRequiredOut] payload, then replace the guards in all four Xero document endpoints with that helper. Also extract and reuse a helper for the identical purchase-order lookup and supplier guard, preserving the current validation behavior and messages.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/job/tests/test_finish_api.py`:
- Around line 105-115: Correct the inline comment in test_lists_job_invoices to
state that the second invoice belongs to the same company but has no job, so it
must not appear in the job-scoped results. Do not change the test setup or
assertions.
In `@apps/xero/api.py`:
- Around line 64-72: Consolidate the duplicated `_staff` helpers by moving the
implementation from `apps/job/api.py` into `apps/core` and importing that shared
helper in `apps/xero/api.py`, `apps/purchasing/api.py`, and the applicable
callers. Remove redundant local definitions, while preserving any
Timesheet-specific narrowing behavior only if it cannot use the shared
implementation.
- Around line 402-405: Update the invoice creation flow around
XeroInvoiceManager.create_document to make retries safe at the Xero API
boundary: either supply a stable idempotency key for the same job/document
across retry attempts, or disable/prohibit RateLimitedRESTClient retries for
this non-idempotent POST. Preserve retries for safe operations and ensure one
logical create cannot produce duplicate Xero documents.
In `@apps/xero/documents/base.py`:
- Around line 82-99: Move the document-type validation before the best-effort
exception handler in both _add_xero_history_note (apps/xero/documents/base.py,
lines 82-99) and _add_history_note (apps/xero/provider.py, lines 407-421):
reject values outside ("invoice", "quote") before entering try, while leaving
provider calls inside the existing handler so only provider failures are
absorbed.
In `@apps/xero/documents/invoice.py`:
- Around line 232-241: Update create_document to validate that billing_metadata
contains target_total, prior_invoiced_total, and calculated_amount before any
direct reads. After validation, access all three keys directly and remove the
"0" defaults from the Decimal conversions so missing contract fields fail rather
than producing an uncomputed remaining_to_invoice value.
- Around line 198-216: Update the invoice creation flow after the existing
result.external_id and result.number validation to validate that raw contains
_sub_total, _total_tax, _total, and _amount_due before constructing Invoice.
Fail early using the existing validation pattern when any key is missing, then
read the validated values without zero fallbacks so invoice totals cannot be
silently stored as 0.00.
In `@apps/xero/documents/po.py`:
- Around line 23-25: Move ZERO_UUID to a shared module, remove the duplicate
definitions from documents/po.py and XeroAccountingProvider, and import the
shared constant in both consumers. Preserve the existing sentinel value and all
checks that use it.
- Around line 55-59: The Xero sync validation path must enforce the draft-only
rule for first-time creation. Update validate_for_xero_sync() to invoke
state_valid_for_xero() before allowing creation, while preserving updates for
POs with an existing Xero ID; keep the implementation and docstring of
state_valid_for_xero() consistent with this behavior.
In `@apps/xero/provider.py`:
- Around line 227-237: The invoice pre-read in the deletion flow must validate
that the API response contains an invoice and that its contact is present before
constructing the DELETED Invoice. Mirror the guards and user-facing error
messages used by delete_purchase_order, referencing the existing
invoice-deletion method and its existing/api/contact symbols while preserving
the normal construction path for valid data.
- Around line 280-314: Update the zero-UUID recovery flow around
_find_po_by_number so that when recovery returns None and po_id remains
ZERO_UUID, return a failed DocumentResult with no external_id instead of
continuing to the success path. Preserve the existing validation-error handling
and only construct the online_url and successful result after a real purchase
order ID is available.
- Around line 316-335: Update _find_po_by_number to enforce a finite maximum
page count while scanning purchase orders. Stop paging and return None once the
limit is reached, while preserving the existing match return and empty-page
termination behavior.
In `@frontend/src/features/job/JobInvoiceCard.tsx`:
- Around line 344-359: Give both numeric inputs in JobInvoiceCard accessible
names: add a visible label or aria-label to the percentage input at
frontend/src/features/job/JobInvoiceCard.tsx:344-359 identifying it as “Invoice
percentage of quote,” and to the amount input at
frontend/src/features/job/JobInvoiceCard.tsx:396-410 identifying it as “Invoice
amount.”
- Around line 233-239: Update the invoice link handler’s window.open call to
pass noopener,noreferrer in its third argument when opening invoice.online_url,
while preserving the existing URL check and toast behavior.
In `@frontend/src/lib/format.ts`:
- Around line 26-34: Update the NZ_DATE formatter used by formatDate to set
timeZone to Pacific/Auckland, ensuring invoice dates render consistently across
environments. Revise the formatter comment to state that per-page formatters are
rejected because invoice dates must display identical text across the workspace.
---
Nitpick comments:
In `@apps/xero/api.py`:
- Around line 317-328: Update the _document_error_status parameter annotation
from object to int | None, matching the status contract declared by
XeroDocumentResponse. Keep the existing 404 handling and 400 fallback unchanged,
and let mypy validate all call sites against the narrowed type.
- Around line 358-366: Extract the repeated get_valid_token() failure response
into a shared _xero_auth_required() helper returning the existing 401
Status[XeroAuthRequiredOut] payload, then replace the guards in all four Xero
document endpoints with that helper. Also extract and reuse a helper for the
identical purchase-order lookup and supplier guard, preserving the current
validation behavior and messages.
In `@apps/xero/tests/test_invoice_manager.py`:
- Around line 86-138: Add a success-case test in TestErrorContract for
delete_document using a configured provider and Xero invoice ID, then assert the
local Invoice is deleted, recalculate_job_invoicing_state is called, and the
invoice_deleted job event records the invoice number. Keep the test focused on
the same-request effects of the happy path.
In `@apps/xero/tests/test_po_manager.py`:
- Around line 98-109: Extend test_zero_uuid_is_treated_as_unsynced to cover the
write path: configure the provider’s successful DocumentResult with
external_id=ZERO_UUID, run manager.sync_to_xero(), then refresh or inspect
po.xero_id and assert it remains None. Preserve the existing create-path
assertion while exercising _save_po_with_xero_data’s zero-UUID handling.
In `@frontend/src/api/index.ts`:
- Line 81: Remove the code-narration comments at frontend/src/api/index.ts:81-81
and frontend/src/features/job/JobFinishTab.tsx:157-157; no replacement
documentation is needed because neither comment states a factual constraint or
rejected alternative.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: ea55edb0-3b70-4970-ae0f-3dafb7b206f3
⛔ Files ignored due to path filters (5)
frontend/src/api/generated/@tanstack/react-query.gen.tsis excluded by!**/generated/**frontend/src/api/generated/index.tsis excluded by!**/generated/**frontend/src/api/generated/sdk.gen.tsis excluded by!**/generated/**frontend/src/api/generated/types.gen.tsis excluded by!**/generated/**frontend/src/api/generated/zod.gen.tsis excluded by!**/generated/**
📒 Files selected for processing (32)
apps/accounting/provider.pyapps/accounting/services/finish_job_summary.pyapps/accounting/tests/test_finish_job_summary.pyapps/accounting/types.pyapps/company/tests/job_fixtures.pyapps/job/api.pyapps/job/schemas.pyapps/job/services/job_service.pyapps/job/tests/test_finish_api.pyapps/xero/api.pyapps/xero/documents/base.pyapps/xero/documents/invoice.pyapps/xero/documents/po.pyapps/xero/helpers.pyapps/xero/provider.pyapps/xero/readonly_provider.pyapps/xero/tests/test_invoice_manager.pyapps/xero/tests/test_po_manager.pydocs/accepted-api-differences.ymldocs/code-quality.mddocs/rewrite-status.mdfrontend/schema.v2.ymlfrontend/src/api/error-message.tsfrontend/src/api/index.tsfrontend/src/features/job/JobDetailPage.tsxfrontend/src/features/job/JobFinishTab.test.tsxfrontend/src/features/job/JobFinishTab.tsxfrontend/src/features/job/JobInvoiceCard.tsxfrontend/src/lib/format.tsfrontend/tests/e2e/job/job-xero-invoice.spec.tsscripts/v1-frontend-operations.ymlstubs/xero_python/accounting/__init__.pyi
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
…erage The endpoint lookup handlers gained their contract markers; the provider's document methods, readonly stubs, and the document HTTP surface (auth 401, 404s, calc 400, status clamp) gained direct tests — every other test mocks the provider, so these paths shipped unexercised. Coverage 88.35%. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Invoice totals and billing_metadata read their keys directly — a payload missing totals fails instead of storing a $0.00 invoice (ADR 0015) - ZERO_UUID single home in apps/xero/constants; unrecovered zero-UUID create is a failure result, never a sentinel success; recovery paging bounded - Invoice delete pre-read guarded like the PO pre-read; history-note kind validated outside the best-effort try - Frontend: noopener on the Xero tab, accessible names on the invoice inputs, UTC date formatting for date-only strings Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
docs/code-quality.md (1)
24-34: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winRegenerate the suppression total.
Line 24 reports 431, but the visible rows sum to 430: fixed suppressions total 13 and rule-coded suppressions total 417.
scripts/checks/code_quality.pyderives the total from the counted entries, so this report is internally inconsistent.Regenerate the file, then run
uv run python -m scripts.checks.code_quality --check.Expected table correction
-| TOTAL suppressions | 431 |+| TOTAL suppressions | 430 |The canonical metric generator in
scripts/checks/code_quality.pyis the source of truth.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/code-quality.md` around lines 24 - 34, Regenerate the suppression table in docs/code-quality.md using scripts/checks/code_quality.py so TOTAL suppressions matches the visible counted entries (430). Then run uv run python -m scripts.checks.code_quality --check and ensure validation passes.
♻️ Duplicate comments (1)
apps/xero/documents/invoice.py (1)
246-251: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winValidate
billing_metadatabefore invoice creation.Lines 249-251 parse required values after
provider.create_invoice()andInvoice.objects.create(). If a required key is missing or non-decimal, the request fails after the financial operation has completed.Parse and validate these fields at the start of
create_document, before provider submission. Reuse the validated values when creating the audit event.As per coding guidelines: “Fail early: check invalid cases first, validate required inputs up front.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/xero/documents/invoice.py` around lines 246 - 251, Update create_document to parse and validate billing_metadata["target_total"], billing_metadata["prior_invoiced_total"], and billing_metadata["calculated_amount"] before calling provider.create_invoice() or Invoice.objects.create(). Reuse these validated Decimal values when constructing the audit event, removing the later direct parsing while preserving the existing required-key and invalid-decimal failures.Source: Coding guidelines
🧹 Nitpick comments (1)
apps/xero/tests/test_provider_documents.py (1)
216-236: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAssert the recovery pagination request.
_find_po_by_numberiteratesget_purchase_orders(tenant_id, page=page), but this test only asserts two calls. A regression that requests page one twice can still pass because the mockedside_effectreturns a different response each time.Assert the
pageargument for bothget_purchase_orderscalls.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/xero/tests/test_provider_documents.py` around lines 216 - 236, Update test_zero_uuid_recovers_real_id_across_pages to inspect api.get_purchase_orders.call_args_list and assert the two calls use page=1 and page=2 respectively, while preserving the existing tenant_id assertions and recovery expectations.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@docs/code-quality.md`:
- Around line 24-34: Regenerate the suppression table in docs/code-quality.md
using scripts/checks/code_quality.py so TOTAL suppressions matches the visible
counted entries (430). Then run uv run python -m scripts.checks.code_quality
--check and ensure validation passes.
---
Duplicate comments:
In `@apps/xero/documents/invoice.py`:
- Around line 246-251: Update create_document to parse and validate
billing_metadata["target_total"], billing_metadata["prior_invoiced_total"], and
billing_metadata["calculated_amount"] before calling provider.create_invoice()
or Invoice.objects.create(). Reuse these validated Decimal values when
constructing the audit event, removing the later direct parsing while preserving
the existing required-key and invalid-decimal failures.
---
Nitpick comments:
In `@apps/xero/tests/test_provider_documents.py`:
- Around line 216-236: Update test_zero_uuid_recovers_real_id_across_pages to
inspect api.get_purchase_orders.call_args_list and assert the two calls use
page=1 and page=2 respectively, while preserving the existing tenant_id
assertions and recovery expectations.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 10f8aa42-f281-4b7d-8f3f-3e62a97f3bec
📒 Files selected for processing (14)
apps/job/tests/test_finish_api.pyapps/xero/api.pyapps/xero/constants.pyapps/xero/documents/base.pyapps/xero/documents/invoice.pyapps/xero/documents/po.pyapps/xero/provider.pyapps/xero/tests/test_document_api.pyapps/xero/tests/test_po_manager.pyapps/xero/tests/test_provider_documents.pydocs/code-quality.mddocs/rewrite-status.mdfrontend/src/features/job/JobInvoiceCard.tsxfrontend/src/lib/format.ts
🚧 Files skipped from review as they are similar to previous changes (9)
- frontend/src/lib/format.ts
- apps/job/tests/test_finish_api.py
- apps/xero/documents/base.py
- apps/xero/tests/test_po_manager.py
- docs/rewrite-status.md
- apps/xero/provider.py
- apps/xero/api.py
- frontend/src/features/job/JobInvoiceCard.tsx
- apps/xero/documents/po.py
Malformed metadata now fails while failing is still free, not after a real Xero invoice exists; the audit event reuses the validated values. Page order asserted in the zero-UUID recovery test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
corrin
commented
Aug 9, 2026
Round-2 items, addressed in 2fb241c:
|
Two CI runs of byte-identical code measured 88.33% and 88.34% — parallel scheduling moves total coverage by ±0.01, so the exact 2-decimal match flapped forever (three CI round-trips on this PR). The coverage row now matches within a Decimal 0.01 band; the surrounding text still compares exactly, every other row stays exact, and the 88 floor is enforced by coverage's own fail_under. Gate behaviour pinned in test_contract_gates. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e test) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Uh oh!
There was an error while loading. Please reload this page.
corrin
commented
Aug 9, 2026
Verification upgraded per the corrected test regime: XERO_READONLY is only for dev sessions pointed at production (hotfixes) — testing runs with writes live. Re-ran the full suite with |
Slice 2b — invoice path: document push + Finish Job (
job-xero-invoicegreen, 13/40)Second of the three slice-2 PRs (2a sync engine #46 · 2b this · 2c quote path next). Ports the Xero document push and the Finish Job workspace from v1.
What ports
Document manager base + invoice push (
apps/xero/documents/{base,invoice}.py,apps/xero/helpers.py)XeroDocumentManagerwith the config guards (branding theme, quote terms), best-effort history notes, and fail-early construction;XeroInvoiceManagerbuilds a provider-agnosticInvoicePayload, callsget_provider().create_invoice(), and owns local persistence: Invoice mirror row from the provider's canonical raw payload,billing_metadataaudit trail, JobEvent, and a same-requestrecalculate_job_invoicing_state(ledgered — v1 leftfully_invoicedto the hourly sync, which underXERO_READONLYnever sees the invoice).POST /api/xero/create_invoice/{job_id}andDELETE /api/xero/delete_invoice/{job_id}keep v1's URL fragments so the spec's request matcher ports unedited. Amount derivation goes through the already-portedcalculate_invoice_amount(modes: full / costs-to-date / percent / amount).Purchase-order push (
apps/xero/documents/po.py) — USER-COMMITTED scopexero_idwith the zero-UUID sentinel treated as absent on read and write (v1 stored it), paged recovery of the real id by PO number, per-linexero_line_item_idbackfill with duplicate-safe description matching, status mapping that raises on unknown statuses. No spec covers PO push;apps/xero/tests/test_po_manager.py(12 tests) is its gate.Readonly works by construction:
XeroReadOnlyProviderfabricates well-formed results —INV-E2E-*numbers, GST-exclusive fake totals from the line items ×CompanyDefaults.gst_rate— so the endpoint path is byte-identical underXERO_READONLYand the E2E balance-settles-to-$0 assertion exercises the real persistence code. Missing write overrides hit tripwires, not the tenant.Finish Job backend (
apps/accounting/services/finish_job_summary.py,apps/job/api.py)Job.save()'s field-change audit machinery (unknown keys 422 viaextra="forbid", schema field lists pinned toJob.COMPLETION_CHECKLIST_FIELDSat import), and the ETagged invoice list. The finish GET is deliberately un-ETagged: the balance moves when invoices sync in without touchingjob.updated_at.Frontend (
JobFinishTab.tsx,JobInvoiceCard.tsx, lazy-loaded into JobDetailPage)apiErrorMessagenow also reads the document endpoints'errorkey so calc/configuration guidance reaches the user.Spec:
job-xero-invoice.spec.tsports near-verbatim, with one deliberate deviation — it creates its own job instead of usingsharedEditJobUrl, because it fully invoices the job and the shared fixture is read-only by contract (v1 mutated it and survived on run ordering).Adversarial review (2 subagents) — notable catches, all fixed
ConfigErrorinstead of returning the error payload. Statuses now clamp to the declared map; the cause stays inerror(ADR 0038).refetch()never rejects, so the "list could not be refreshed" toast was unreachable; an invoices load error rendered as the "No invoices" empty state.calculate_invoice_amountdoesn't quantize, so a sub-cent basis total (e.g. 157.985) can never read fully-invoiced. Real jobs have 2dp values; flagged for 2c or later rather than changing the money path mid-slice.Verified
./scripts/ops/run_e2e.sh: 38 passed —job-xero-invoicegreen underXERO_READONLY(13/40 specs), all prior specs green with the sync beat live.renamed:entries for all four document operation ids; two new behaviour entries indocs/accepted-api-differences.yml.🤖 Generated with Claude Code
Summary by CodeRabbit