OUT-3874: use invoice address to designate tax jurisdiction#264
OUT-3874: use invoice address to designate tax jurisdiction#264SandipBajracharya wants to merge 8 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis 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.
Confidence Score: 5/5Safe 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; No files require special attention. Important Files Changed
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
%%{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
Reviews (2): Last reviewed commit: "refactor(OUT-3874): extract shared Addre..." | Re-trigger Greptile |
|
@greptileai re-review whole pr changes again |
c22fd36 to
24575b2
Compare
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>
…ction for report review
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>
24575b2 to
a2797c9
Compare
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
TxnTaxDetail.TotalTaxis 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.BillAddrandShipAddr, only when the two fields QBO needs for a jurisdiction — state and postal code — are present.getUsStateCode(viaus-state-converter) maps a full state name to the two-letterCountrySubDivisionCode.AddressSchema) so a partial address on the REST fetch paths (_getInvoice/_getInvoices, which use.parse()) can't abort a bulk resync.InvoiceDeleted/InvoiceVoidedschemas intoInvoiceDestructiveResponseSchema.Testing
yarn test);tscand lint clean.getUsStateCode; integration coverage for the address payload (BillAddr/ShipAddr), the always-sentTxnTaxDetail, the missing-postal-code skip, and the paid-invoice payment total.Open item (needs sandbox confirmation)
On realms with Automated Sales Tax enabled, sending
ShipAddrmay cause QBO to recompute tax from that jurisdiction and ignore theTxnTaxDetail.TotalTaxwe 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 sendBillAddronly (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