Repository navigation
fix(quickbooks): don't demote fully corroborated Spreedly matches on name gap - #238
Conversation
…name gap Wholesale buyers routinely pay company invoices on personal cards, so the Stripe cardholder and QBO invoice customer legitimately differ (Oct-7 production: Wilhemus Schutten paying the Tropics Cafe invoice). Once both Spreedly legs corroborate — PaymentRefNum plus a linked-invoice DocNumber — the identifiers alone establish which payment this is; the name gap is surfaced as informational cardholder_customer_name_differs evidence instead of demoting to suggested. Non-Spreedly hits and incomplete linkage keep the conservative customer_mismatch demotion. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Reviewer's GuideThe PR narrows a matching-policy bug by allowing only fully corroborated two-leg Spreedly identifiers to override a cardholder/customer name mismatch; all other identity conflicts remain conservatively demoted, with new UI labels, operational documentation, and regression tests. Sequence diagram for fully corroborated Spreedly matchingsequenceDiagram
participant Stripe as Stripe charge
participant Matcher as matchOlliePayment
participant Spreedly as Spreedly identity claim
participant QBO as QBO payment
Stripe->>Matcher: Match identity hit
Matcher->>Spreedly: Validate composite order_id legs
Spreedly-->>Matcher: PaymentRefNum and invoice DocNumber corroborate
Matcher->>QBO: Confirm both legs reference same payment
QBO-->>Matcher: Unique payment match
Matcher->>Matcher: Add spreedly_identity
Matcher->>Matcher: Add cardholder_customer_name_differs if names differ
Matcher-->>Stripe: verified match
Flow diagram for Spreedly identity match confidenceflowchart TD
A[Stripe charge with identity hit] --> B{Spreedly composite claim?}
B -- No --> C{Customer names differ?}
C -- Yes --> D[Suggested: customer_mismatch]
C -- No --> E[Verified]
B -- Yes --> F{Both payment and invoice legs corroborate same QBO payment?}
F -- No --> G[Suggested: spreedly_linkage_unverified]
G --> H{Customer names differ?}
H -- Yes --> I[Add customer_mismatch]
F -- Yes --> J[Verified: spreedly_identity]
J --> K{Customer names differ?}
K -- Yes --> L[Add cardholder_customer_name_differs as informational evidence]
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
Hey - I've found 4 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="lib/stripe-payouts-common.ts" line_range="637-638" />
<code_context>
const stripeName = identity.customerName?.trim().toLowerCase();
const qboName = hit.payment.customerName?.trim().toLowerCase();
const customerConflict = Boolean(stripeName && qboName && stripeName !== qboName);
- if (customerConflict) {
- evidence.push("customer_mismatch");
- return {
</code_context>
<issue_to_address>
**Unrelated references verify payments**
When a charge contains additional labeled refs matching a candidate’s payment and invoice refs, but its composite Spreedly order or payment values do not, `compareOllieRefs` and the namespace-only leg check accept those hits as corroboration, so the name mismatch remains informational and the wrong QBO payment stays verified and eligible for automatic reclassification.
Compare each leg against the corresponding values in `identity.spreedly` instead of accepting any shared ref in its namespace.
</issue_to_address>
### Comment 2
<location path="lib/stripe-payouts-common.ts" line_range="638" />
<code_context>
}
evidence.push("spreedly_identity");
+ if (customerConflict) {
+ evidence.push("cardholder_customer_name_differs");
+ }
+ } else if (customerConflict) {
</code_context>
<issue_to_address>
**Two payouts can claim one payment**
When two overlapping payout builds both see the same QBO candidate before either has persisted a claiming proposal, and each charge carries identifiers that match it, both builds query `claimedByOthers` before either persists its proposal, so each can match and mark the same QBO payment verified. Each then creates a distinct payout proposal and the automatic pass can execute both; the payment is reclassified only once, while both Stripe transactions are treated as reconciled to it.
Atomically reserve the QBO payment across payout proposals before allowing either proposal to become auto-eligible, or make the executor reject a payment already bound to another payout.
</issue_to_address>
### Comment 3
<location path="lib/stripe-payouts-common.ts" line_range="638" />
<code_context>
}
evidence.push("spreedly_identity");
+ if (customerConflict) {
+ evidence.push("cardholder_customer_name_differs");
+ }
+ } else if (customerConflict) {
</code_context>
<issue_to_address>
**Stale match can auto-apply**
When two builds of the same payout overlap, a linkage change or lookup failure makes the newer build incomplete, and the older fully corroborated build writes its proposal afterward, a slower build with fully corroborated Spreedly data can overwrite a newer same-payout build that found incomplete linkage, because proposal transactions write each build’s computed fields without checking build freshness. Its automatic pass can then execute the stale verified proposal; the executor rechecks the QBO payment’s account and amount, not the Spreedly linkage, so the payment is reclassified using outdated evidence.
Serialize all ledger builds for a payout or attach a monotonic build version and reject proposal writes and automatic execution from stale builds; revalidate the required Spreedly legs before execution.
</issue_to_address>
### Comment 4
<location path="lib/stripe-payouts-common.ts" line_range="637-639" />
<code_context>
const stripeName = identity.customerName?.trim().toLowerCase();
const qboName = hit.payment.customerName?.trim().toLowerCase();
const customerConflict = Boolean(stripeName && qboName && stripeName !== qboName);
- if (customerConflict) {
- evidence.push("customer_mismatch");
- return {
- confidence: "suggested",
- payment: hit.payment,
- candidateIds: [hit.payment.id],
- evidence,
- identityRefs: hit.identityRefs,
- };
- }
// A Spreedly composite claim is held to a stricter standard than a
// lone namespaced ref: BOTH legs must positively corroborate on
</code_context>
<issue_to_address>
**Memo-only references qualify for automatic matching**
When the QBO PaymentRefNum is absent or does not match, but the QBO memo contains the claimed labeled payment reference and a linked invoice carries the claimed order DocNumber, `ollieRefsFromQboPayment` also extracts `paymentNumbers` from the QBO memo, so a memo-only payment reference plus a linked invoice can satisfy the two-leg check. With a cardholder/customer name gap, this branch now keeps the match `verified`, allowing the existing auto-eligibility path to act on a payment whose `PaymentRefNum` did not corroborate the claimed reference.
Require the Spreedly payment leg to match QBO `PaymentRefNum` specifically before treating a name gap as informational; keep memo-derived matches review-only.
</issue_to_address>Sourcery assessment
Needs a human reviewer. 4 findings to address first, and a Spreedly match with both identifier legs corroborated can now remain verified despite a cardholder/customer name mismatch, making it eligible for the executor’s automatic path. If those identifiers do not establish the intended payment, an incorrect QBO reclassification can persist after reverting; this changes the eligibility rule for every such match.
Blocking findings: lib/stripe-payouts-common.ts:638, lib/stripe-payouts-common.ts:638, lib/stripe-payouts-common.ts:638, lib/stripe-payouts-common.ts:639
Sourcery findings on #238: the leg check accepted any namespace hit, so an unrelated labeled ref sharing the order/payment space could satisfy a leg the claim never made; and paymentNumbers merged memo-derived refs with PaymentRefNum, so a memo-only "Ollie Payment #…" mention satisfied the payment leg without PaymentRefNum corroborating. Legs now require the claimed paymentRef in the PaymentRefNum field itself and the claimed orderNumber on a linked-invoice DocNumber. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Summary
The October 7 payout exposed a matching-policy bug: the Stripe charge (
txn_3UNHoNC2zsUcVaNLl3PhIrt9, $230.00) carried valid Spreedly metadata00326-1623638and both legs uniquely corroborated QBO Payment 1786 (PaymentRefNum1623638+ linked invoice DocNumber00326), yet the match demoted tosuggestedsolely because cardholder Wilhemus Schutten differs from invoice customer Tropics Cafe — a routine wholesale pattern (owner pays the company invoice on a personal card).Once both Spreedly legs independently corroborate the same payment, the transaction identifiers alone establish identity; the cardholder↔customer name difference is informational, not disqualifying.
Changes
lib/stripe-payouts-common.ts— in the single-identity-hit branch, a fully corroborated Spreedly two-leg match now staysverifiedand surfacescardholder_customer_name_differsevidence. Thecustomer_mismatchdemotion still applies to non-Spreedly identity hits and to Spreedly claims whose linkage is incomplete — names never substitute for identifiers.components/admin-quickbooks-payouts.tsx— labels forcardholder_customer_name_differs,spreedly_identity,spreedly_linkage_unverified(the latter two previously rendered as raw codes).docs/operations/quickbooks.md— documents the evidence hierarchy: names corroborate but never prove identity, and fully corroborated Spreedly identity makes the name gap informational only.Verification
npx tsc --noEmitnpm run lintnpm test— 1241 pass (+2)npm run test:rules— 49 passnpm run check:md-linksnpm run check:react-versionsRisk / deployment notes
Generated with Devin
Summary by Sourcery
Keep fully corroborated Spreedly matches verified despite cardholder-to-customer name differences while preserving strict identifier and linkage validation.
Bug Fixes:
Enhancements:
Documentation:
Tests:
Summary by CodeRabbit