Skip to content

OUT-3874: use invoice address to designate tax jurisdiction#264

Open
SandipBajracharya wants to merge 8 commits into
masterfrom
OUT-3874
Open

OUT-3874: use invoice address to designate tax jurisdiction#264
SandipBajracharya wants to merge 8 commits into
masterfrom
OUT-3874

Conversation

@SandipBajracharya

@SandipBajracharya SandipBajracharya commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

What & why

OUT-3874 — attach the customer's address to the QuickBooks invoice so the tax jurisdiction is visible on the QBO record for reporting/review.

Behavior

  • Tax total is always sourced from Assembly. TxnTaxDetail.TotalTax is set to Assembly's computed tax on every invoice; the sync log and any created payment use Assembly's local total. QBO is not relied on to compute tax.
  • Customer address is sent for the QBO record, as both BillAddr and ShipAddr, only when the two fields QBO needs for a jurisdiction — state and postal code — are present. getUsStateCode (via us-state-converter) maps a full state name to the two-letter CountrySubDivisionCode.
  • Address sub-fields are optional (shared AddressSchema) so a partial address on the REST fetch paths (_getInvoice/_getInvoices, which use .parse()) can't abort a bulk resync.
  • Refactor: consolidated the duplicate InvoiceDeleted/InvoiceVoided schemas into InvoiceDestructiveResponseSchema.

Testing

  • Full suite green (yarn test); tsc and lint clean.
  • Unit tests for getUsStateCode; integration coverage for the address payload (BillAddr/ShipAddr), the always-sent TxnTaxDetail, the missing-postal-code skip, and the paid-invoice payment total.

Open item (needs sandbox confirmation)

On realms with Automated Sales Tax enabled, sending ShipAddr may cause QBO to recompute tax from that jurisdiction and ignore the TxnTaxDetail.TotalTax we send — which would contradict the "address is report-only" intent. This should be verified against a QBO sandbox before relying on it; if AST overrides, we should send BillAddr only (lower risk) or accept QBO-computed tax deliberately. The integration tests validate the payload we send, not QBO's tax outcome.

🤖 Generated with Claude Code

@linear-code

linear-code Bot commented Jul 8, 2026

Copy link
Copy Markdown

OUT-3874

@vercel

vercel Bot commented Jul 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
quickbooks-sync Building Building Jul 10, 2026 10:14am
quickbooks-sync (dev) Ready Ready Preview, Comment Jul 10, 2026 10:14am

Request Review

@greptile-apps

greptile-apps Bot commented Jul 8, 2026

Copy link
Copy Markdown

Greptile Summary

This PR wires the customer's Assembly invoice address into QuickBooks' Automated Sales Tax (AST) engine so QBO computes tax by the customer's jurisdiction rather than falling back to the seller's company address, and updates the sync log to record the amounts QBO actually returned rather than locally-computed values.

  • Address-to-QBO flow: getUsStateCode converts a full US state name to a USPS 2-letter code; when both state code and postal code resolve, BillAddr/ShipAddr are sent to QBO on invoice.created. TxnTaxDetail is omitted so QBO's AST can run; it is included only when Assembly reports a non-zero tax or no usable address exists.
  • Response-driven logging: TotalAmt and TxnTaxDetail.TotalTax are now read back from QBO's createInvoice response (with fallback to locally-computed values), so the sync log and any same-request payment creation reflect what QBO actually recorded.
  • Schema cleanup: AddressSchema is extracted as a single shared definition; the duplicate InvoiceDeletedResponseSchema/InvoiceVoidedResponseSchema pair (which had a pre-existing type-inference bug) is collapsed into InvoiceDestructiveResponseSchema.

Confidence Score: 5/5

Safe to merge — all changed paths handle missing/partial address data gracefully, QBO response values are read back with correct null-coalescing fallbacks, and the four new integration tests cover the key branching conditions.

The address construction correctly guards on both state code and postal code before building the payload; overrideTax logic is sound across all four combinations of zero/non-zero Assembly tax and present/absent address; and the sync log now uses QBO's own returned TotalAmt/TxnTaxDetail.TotalTax which is strictly more accurate than before. The acknowledged open item (whether AST honors a co-supplied TxnTaxDetail on enabled realms) does not affect correctness of the logged values since those are always read from the QBO response.

No files require special attention.

Important Files Changed

Filename Overview
src/app/api/quickbooks/invoice/invoice.service.ts Core logic change: builds addressPayload from customer's region/postal code, computes overrideTax flag, conditionally sends TxnTaxDetail, and reads back TotalAmt/TxnTaxDetail.TotalTax from QBO's response for sync-log accuracy. All edge cases (missing address, null postal code, retry on 6140) handled correctly.
src/utils/common.ts Adds getUsStateCode: trims input, calls abbr() from us-state-converter, and uses !== 2 to distinguish a valid 2-char USPS code from the library's error sentences. Logic is correct.
src/type/dto/webhook.dto.ts Exports a shared AddressSchema (all sub-fields optional to tolerate partial REST payloads); adds it to InvoiceCreatedResponseSchema; collapses InvoiceDeletedResponseSchema/InvoiceVoidedResponseSchema into a single InvoiceDestructiveResponseSchema, fixing the pre-existing type-inference bug.
src/type/common.ts Imports and re-uses AddressSchema from webhook.dto.ts to add an optional address field to the REST InvoiceResponseSchema, eliminating the previously duplicated object shape.
src/type/dto/intuitAPI.dto.ts Adds optional TxnTaxDetail.TotalTax to QBInvoiceRowSchema so the QBO-computed tax is captured when the response is parsed.
src/app/api/quickbooks/webhook/webhook.service.ts Updated handleInvoiceDeleted to use the consolidated InvoiceDestructiveResponseSchema — mechanical rename, no behaviour change.
test/integration/quickbooks/invoiceCreated/addressTaxJurisdiction.test.ts New integration tests covering QBO-computes, Assembly-overrides, missing-postal-code, and paid-invoice branches. Good coverage for the four meaningful paths introduced by this PR.
test/unit/utils/common.test.ts Unit tests for getUsStateCode covering valid states, case/whitespace tolerance, undefined/empty input, and non-US regions — complete coverage for the utility function.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant A as Assembly Webhook
    participant W as WebhookService
    participant I as InvoiceService
    participant U as getUsStateCode
    participant Q as QBO (Intuit API)
    participant D as SyncLog DB

    A->>W: invoice.created payload (with address?)
    W->>I: webhookInvoiceCreated(payload)
    I->>U: getUsStateCode(address.region)
    U-->>I: "CA | null"

    alt state code + postalCode present
        I->>I: "addressPayload = {Line1, City, CountrySubDivisionCode, PostalCode, Country}"
    else missing state or postalCode
        I->>I: "addressPayload = null"
    end

    alt "totalTax > 0 OR no addressPayload"
        I->>Q: "createInvoice({BillAddr?, ShipAddr?, TxnTaxDetail:{TotalTax}})"
        note over I,Q: overrideTax=true — send Assembly tax
    else "totalTax == 0 AND addressPayload set"
        I->>Q: "createInvoice({BillAddr, ShipAddr})"
        note over I,Q: overrideTax=false — let AST compute
    end

    Q-->>I: "Invoice{Id, TotalAmt, TxnTaxDetail?.TotalTax}"

    I->>I: "totalWithTax = TotalAmt ?? (subtotal+localTax)"
    I->>I: "taxForLog = TxnTaxDetail.TotalTax ?? localTax"

    I->>D: "logSync(amount=totalWithTax*100, taxAmount=taxForLog*100)"

    opt "invoice.status == PAID"
        I->>Q: "createPayment({TotalAmt: totalWithTax})"
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant A as Assembly Webhook
    participant W as WebhookService
    participant I as InvoiceService
    participant U as getUsStateCode
    participant Q as QBO (Intuit API)
    participant D as SyncLog DB

    A->>W: invoice.created payload (with address?)
    W->>I: webhookInvoiceCreated(payload)
    I->>U: getUsStateCode(address.region)
    U-->>I: "CA | null"

    alt state code + postalCode present
        I->>I: "addressPayload = {Line1, City, CountrySubDivisionCode, PostalCode, Country}"
    else missing state or postalCode
        I->>I: "addressPayload = null"
    end

    alt "totalTax > 0 OR no addressPayload"
        I->>Q: "createInvoice({BillAddr?, ShipAddr?, TxnTaxDetail:{TotalTax}})"
        note over I,Q: overrideTax=true — send Assembly tax
    else "totalTax == 0 AND addressPayload set"
        I->>Q: "createInvoice({BillAddr, ShipAddr})"
        note over I,Q: overrideTax=false — let AST compute
    end

    Q-->>I: "Invoice{Id, TotalAmt, TxnTaxDetail?.TotalTax}"

    I->>I: "totalWithTax = TotalAmt ?? (subtotal+localTax)"
    I->>I: "taxForLog = TxnTaxDetail.TotalTax ?? localTax"

    I->>D: "logSync(amount=totalWithTax*100, taxAmount=taxForLog*100)"

    opt "invoice.status == PAID"
        I->>Q: "createPayment({TotalAmt: totalWithTax})"
    end
Loading

Reviews (2): Last reviewed commit: "refactor(OUT-3874): extract shared Addre..." | Re-trigger Greptile

Comment thread src/utils/common.ts Outdated
Comment thread src/type/dto/webhook.dto.ts Outdated
@SandipBajracharya

Copy link
Copy Markdown
Collaborator Author

@greptileai re-review whole pr changes again

@SandipBajracharya SandipBajracharya changed the title feat(OUT-3874): use invoice address to designate tax jurisdiction OUT-3874: use invoice address to designate tax jurisdiction Jul 8, 2026
SandipBajracharya and others added 8 commits July 10, 2026 15:57
Send the customer's address to QBO as BillAddr + ShipAddr so its
Automated Sales Tax engine computes tax by jurisdiction, and record the
amount/tax QBO actually returned rather than the locally-computed values.

- Add getUsStateCode() (us-state-converter) to map a full state name to
  the two-letter CountrySubDivisionCode QBO needs.
- Only attach the address when both state and postalCode resolve, the two
  fields QBO requires to pin a jurisdiction.
- Gate TxnTaxDetail: send our own tax total when we have one or there is
  no usable address; otherwise omit it so QBO computes from the address.
- Log amount/taxAmount from the QBO response (TotalAmt / TxnTaxDetail),
  falling back to computed values.
- Make address sub-fields optional so partial addresses on the REST fetch
  paths (_getInvoice/_getInvoices .parse) can't abort bulk resync.
- Consolidate InvoiceDeleted/InvoiceVoided response schemas into
  InvoiceDestructiveResponseSchema.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cover full-state-name mapping, case-insensitivity and whitespace
trimming, empty/undefined input, and non-US names returning null.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Assert the invoice.created flow across four branches: QBO computes tax
when state+ZIP are present and Assembly tax is zero; we override with our
own tax when Assembly reports a non-zero amount; the address is omitted
when the postal code is missing; and a paid invoice records the payment
using the tax-inclusive total QBO returned.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Guard on length !== 2 instead of > 2 so an empty or 1-char abbr() result
can't slip through into CountrySubDivisionCode. Addresses Greptile P2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reference a single AddressSchema from both InvoiceCreatedResponseSchema
(webhook push) and InvoiceResponseSchema (REST fetch) so the two parsing
paths can't drift. Addresses Greptile P2. Also strips verbose comments.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tax is now always sent from Assembly and the address is record-only, so
assert TxnTaxDetail is always present and log/payment use the local total
rather than QBO's returned figures.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The create-invoice response no longer reads TxnTaxDetail now that tax is
always sourced from Assembly, so remove the field to stop implying we
consume it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant