Skip to content

fix(quickbooks): don't demote fully corroborated Spreedly matches on name gap - #238

Merged
spizeck merged 2 commits into
mainfrom
feat/spreedly-name-policy
Oct 15, 2026
Merged

spizeck merged 2 commits into
mainfrom
feat/spreedly-name-policy

Conversation

@spizeck

@spizeck spizeck commented Oct 15, 2026 •

Copy link
Copy Markdown
Owner

Summary

The October 7 payout exposed a matching-policy bug: the Stripe charge (txn_3UNHoNC2zsUcVaNLl3PhIrt9, $230.00) carried valid Spreedly metadata 00326-1623638 and both legs uniquely corroborated QBO Payment 1786 (PaymentRefNum 1623638 + linked invoice DocNumber 00326), yet the match demoted to suggested solely 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 stays verified and surfaces cardholder_customer_name_differs evidence. The customer_mismatch demotion 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 for cardholder_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.
  • Tests — Oct-7 Tropics Cafe fixture verifies despite the name gap; a name gap with incomplete linkage still demotes with both markers.

Verification

  • npx tsc --noEmit
  • npm run lint
  • npm test — 1241 pass (+2)
  • npm run test:rules — 49 pass
  • npm run check:md-links
  • npm run check:react-versions

Risk / deployment notes

  • No secrets, credentials, or private data were committed.
  • No QBO writes, no env changes, production stays in Approval mode. Historical transactions remain manual-only; previously applied proposals (including Payment 1786's manual approval) are terminal and unchanged — durable reconciliation evidence still preserves the original stored confidence.
  • Post-cutover, a fully corroborated Spreedly match with a name gap now qualifies for the existing auto-eligibility evaluation under unchanged rules — intended per the task; no policy relaxation.

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:

  • Preserve verified confidence for fully corroborated Spreedly matches when the cardholder and QBO customer names differ, while retaining conservative demotion for incomplete or non-Spreedly identity matches.

Enhancements:

  • Expose clear evidence labels for Spreedly identity, linkage status, and cardholder/customer name differences.
  • Document the evidence hierarchy and the exception for fully corroborated Spreedly two-leg matches.

Documentation:

  • Update QuickBooks operations guidance to clarify that identifiers establish fully corroborated Spreedly identity and names provide corroboration only.

Tests:

  • Add coverage for the production name-gap scenario, memo-only and mismatched Spreedly linkage, and incomplete linkage with a customer mismatch.

Summary by CodeRabbit

  • Bug Fixes
    • Improved payout matching so a Spreedly identity is verified only when both the claimed payment reference and linked order are corroborated.
    • A customer-name difference no longer blocks verification when both identity checks match; incomplete or conflicting evidence remains review-required.
  • Documentation
    • Clarified how name differences, duplicate candidates, and corroborating identity checks affect payout review status.

…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>
@sourcery-ai

sourcery-ai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

The 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 matching

sequenceDiagram
    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
Loading

Flow diagram for Spreedly identity match confidence

flowchart 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]
Loading

File-Level Changes

Change Details Files
Preserve verified confidence for fully corroborated two-leg Spreedly identity matches despite a cardholder/customer name difference, while retaining conservative demotion for incomplete or non-Spreedly identity matches.
  • Detect customer-name conflicts without immediately demoting the match.
  • Require both Spreedly payment-reference and linked-invoice legs to corroborate the same payment before allowing the exception.
  • Emit informational cardholder-name evidence for the verified exception and mismatch plus unverified-linkage evidence for incomplete claims.
  • Continue emitting customer-consistent evidence only when both names are present and equal.
lib/stripe-payouts-common.ts
Make the new reconciliation evidence states understandable in the QuickBooks payout administration UI.
  • Add human-readable labels for cardholder/customer name differences and Spreedly identity/linkage evidence.
components/admin-quickbooks-payouts.tsx
Document the evidence hierarchy and the narrowly scoped Spreedly exception in QuickBooks operations guidance.
  • Clarify that names corroborate but do not establish identity.
  • Document that fully corroborated two-leg Spreedly matches remain verified despite a name gap, while incomplete linkage remains review-only.
docs/operations/quickbooks.md
Cover both the intended verified production scenario and the conservative incomplete-linkage behavior with focused matcher tests.
  • Verify the Tropics Cafe fixture remains verified and records informational name-difference evidence.
  • Verify incomplete Spreedly linkage with a name gap remains suggested and records both relevant evidence markers.
tests/lib/stripe-payouts-common.test.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@vercel

vercel Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated
deepdivebrewing-web Ready Ready Preview Oct 15, 2026 12:57am UTC

Request Review

@coderabbitai

coderabbitai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: daae7162-b30c-4ab9-889f-ea90bdadcef0
📥 Commits

Reviewing files that changed from the base of the PR and between a573bec and 4e45993.

📒 Files selected for processing: 4
  • components/admin-quickbooks-payouts.tsx
  • docs/operations/quickbooks.md
  • lib/stripe-payouts-common.ts
  • tests/lib/stripe-payouts-common.test.ts

Included review availability: This review used your included allowance. 0 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 3 reviews per hour.

📝 Walkthrough

The matcher now requires a claimed payment reference in QBO PaymentRefNum and a matching linked-invoice order reference for Spreedly verification. Fully corroborated matches can remain verified despite differing customer names; other name mismatches remain suggested.

Changes: Spreedly payout matching

Layer / File(s) Summary
Payment reference and identity verification
lib/stripe-payouts-common.ts, tests/lib/stripe-payouts-common.test.ts
The matcher extracts references from PaymentRefNum and requires both claimed Spreedly identity legs to match. Tests cover verified matches with differing names and suggested matches when a claimed reference or linkage is missing.
Evidence labels and matching guidance
components/admin-quickbooks-payouts.tsx, docs/operations/quickbooks.md
The interface adds labels for Spreedly identity evidence and cardholder-name differences. The guide describes name-mismatch outcomes and duplicate corroborating candidates.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~15 minutes

Change: Bug fix

Merge Risk: ⚪ Minimal · up to 4e459

The changed matching behavior has no identified merge-blocking risk after normal checks.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 3 files. (1 skipped: 1… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly and concisely describes the primary fix: preserving verified Spreedly matches when cardholder and customer names differ.
Description check Passed The description includes the required Summary, Changes, Verification, and Risk / deployment notes sections. It explains the bug, implementation, tests, and deployment impact. It omits some template ve…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 3 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review ✅ Completed 2026-10-15T00:55:20.449738Z 9daa5f1 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 is free for open source - if you like our reviews please consider sharing them ✨

Comment thread lib/stripe-payouts-common.ts
Comment thread lib/stripe-payouts-common.ts
Comment thread lib/stripe-payouts-common.ts
Comment thread lib/stripe-payouts-common.ts
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>
@spizeck
spizeck merged commit 5fd8844 into main Oct 15, 2026
5 checks passed
@spizeck
spizeck deleted the feat/spreedly-name-policy branch October 15, 2026 01:16

This branch was successfully deployed

1 active deployment
Preview — 4e459935 Deployed Oct 15, 2026 by vercel[bot]
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