Stripe integration - account for multiple line items for a given invoice#3824
Stripe integration - account for multiple line items for a given invoice#3824devkiran wants to merge 12 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughWebhook handlers, product-extraction utils, reward schemas, and partner commission logic were updated to extract and propagate multiple sale products (id, amount, quantity) and compute partner commissions per-product when reward modifiers reference sale.productId. Log prefixes for webhook early-return paths were adjusted. Changes
Sequence DiagramsequenceDiagram
participant Stripe as Stripe Webhook
participant Handler as Webhook Handler
participant Utils as Product Utils
participant Commission as Partner Commission
participant Rewards as Reward Evaluator
Stripe->>Handler: checkout.session.completed / invoice.paid event
Handler->>Utils: getCheckoutSessionProducts(checkoutSessionId, stripeAccountId)
Utils-->>Handler: [{id, amount, quantity}, ...] or []
Handler->>Commission: createPartnerCommission(context with sale.products)
alt reward conditions reference sale.productId
Commission->>Rewards: evaluateConditions(productId) per product
Rewards-->>Commission: condition results per product
Commission->>Commission: compute per-product rewards & earnings
Commission->>Commission: aggregate earnings across products
else no product-dependent conditions
Commission->>Rewards: evaluateConditions(single sale/context)
Rewards-->>Commission: condition result
Commission->>Commission: compute single reward & earnings
end
Commission-->>Handler: commission recorded (total earnings)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 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. Review rate limit: 6/8 reviews remaining, refill in 13 minutes and 19 seconds.Comment |
…tured product objects
There was a problem hiding this comment.
🧹 Nitpick comments (1)
apps/web/app/(ee)/api/stripe/integration/webhook/utils/get-checkout-session-product-id.ts (1)
47-56: Consider pagination for checkout sessions with many line items.The
limit: 10may miss products when a checkout session has more than 10 line items. For high-volume e-commerce scenarios with many products per checkout, this could lead to incomplete reward calculations.Consider either increasing the limit or implementing pagination via
auto_pagination_each:💡 Optional: Support pagination for larger checkouts
- const lineItems = await stripeApp.checkout.sessions.listLineItems( - checkoutSessionId, - { - expand: ["data.price.product"], - limit: 10, - }, - { - stripeAccount: stripeAccountId, - }, - ); + const lineItems: Stripe.LineItem[] = []; + for await (const item of stripeApp.checkout.sessions.listLineItems( + checkoutSessionId, + { + expand: ["data.price.product"], + limit: 100, + }, + { + stripeAccount: stripeAccountId, + }, + )) { + lineItems.push(item); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/`(ee)/api/stripe/integration/webhook/utils/get-checkout-session-product-id.ts around lines 47 - 56, The current call to stripeApp.checkout.sessions.listLineItems using limit: 10 can miss items for sessions with >10 line items; update the implementation in get-checkout-session-product-id to paginate through all line items (e.g., use Stripe's auto-pagination or repeatedly call listLineItems with starting_after) against stripeApp.checkout.sessions.listLineItems using checkoutSessionId and stripeAccountId, aggregating expanded "data.price.product" results before extracting product IDs so reward calculations include every line item rather than only the first 10.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In
`@apps/web/app/`(ee)/api/stripe/integration/webhook/utils/get-checkout-session-product-id.ts:
- Around line 47-56: The current call to
stripeApp.checkout.sessions.listLineItems using limit: 10 can miss items for
sessions with >10 line items; update the implementation in
get-checkout-session-product-id to paginate through all line items (e.g., use
Stripe's auto-pagination or repeatedly call listLineItems with starting_after)
against stripeApp.checkout.sessions.listLineItems using checkoutSessionId and
stripeAccountId, aggregating expanded "data.price.product" results before
extracting product IDs so reward calculations include every line item rather
than only the first 10.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: fd25aa3a-dcf1-4f2b-b49e-afd596ec95eb
📒 Files selected for processing (6)
apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.tsapps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.tsapps/web/app/(ee)/api/stripe/integration/webhook/utils/get-checkout-session-product-id.tsapps/web/lib/partners/create-partner-commission.tsapps/web/lib/partners/evaluate-reward-conditions.tsapps/web/lib/zod/schemas/rewards.ts
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/web/app/`(ee)/api/stripe/integration/webhook/invoice-paid.ts:
- Around line 247-262: The code that builds products from invoice.lines.data
must handle Stripe's pagination and nullable quantities: if
invoice.lines.has_more is true, page through the invoice lines (using the Stripe
SDK invoice line list API for the given invoice id) to collect all lines before
mapping; when mapping each line in the products creation (the block that
references invoice.lines.data and returns { id: productId, amount, quantity }),
coerce nullable line.quantity to a safe numeric value (e.g., const qty =
line.quantity ?? 1) so the created product objects always have a number for
quantity and will be accepted by createPartnerCommission; update the type guard
to reflect quantity: number and ensure you iterate the fully-aggregated lines
array rather than the possibly partial invoice.lines.data.
In
`@apps/web/app/`(ee)/api/stripe/integration/webhook/utils/get-checkout-session-products.ts:
- Around line 73-79: The mapped product creation is passing nullable Stripe
line.quantity directly (in the map that returns { id, amount, quantity }) and
the subsequent type predicate only checks for null objects, not for missing
quantity; update the mapping logic in get-checkout-session-products so it either
filters out line items with null/undefined quantity or converts nullable
quantities to a safe number (e.g., default 1) and then update the type predicate
used after .filter to assert quantity is a number (e.g., p is { id: string;
amount: number; quantity: number }) so callers of the resulting array can safely
rely on a non-null quantity without changing other code paths.
- Around line 47-52: The current call in get-checkout-session-products.ts to
stripeApp.checkout.sessions.listLineItems uses a hard limit of 10 and stops,
truncating results; update the logic to paginate and collect all line items for
the given checkoutSessionId (e.g., use Stripe's auto-pagination or iterate pages
from stripeApp.checkout.sessions.listLineItems while following
has_more/starting_after) and keep expand: ["data.price.product"] so every page
expands product details; ensure the final returned products array aggregates
items from all pages instead of only the first 10.
🪄 Autofix (Beta)
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
Run ID: 8a83afca-816a-4917-858e-50edfbc58939
📒 Files selected for processing (3)
apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.tsapps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.tsapps/web/app/(ee)/api/stripe/integration/webhook/utils/get-checkout-session-products.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts
| amount: line.amount_total, | ||
| quantity: line.quantity, | ||
| }; | ||
| }) | ||
| .filter( | ||
| (p): p is { id: string; amount: number; quantity: number } => | ||
| p !== null, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n apps/web/app/\(ee\)/api/stripe/integration/webhook/utils/get-checkout-session-products.ts | head -100Repository: dubinc/dub
Length of output: 2800
🌐 Web query:
Stripe LineItem quantity field type nullable definition
💡 Result:
In Stripe's API, "LineItem" refers to different objects depending on context, but the query likely targets the Checkout Session line item (object type "item" in checkout.sessions.line_items.data), as it's commonly used for checkout line items. For Checkout Session LineItem: - quantity: integer, nullable As per official Stripe API docs for recent API versions (e.g., 2026-03-25.dahlia, 2025-03-31.basil, 2024-06-20): "line_items.data.quantity (integer, nullable) - The quantity of products being purchased." Examples always show a positive integer value (e.g., "quantity": 2), but docs explicitly mark it nullable. For comparison, Invoice LineItem (invoice.line_items.data): - quantity: integer, nullable (deprecated in favor of quantity_decimal: decimal string, nullable) When creating Checkout Sessions, line_items[].quantity is "integer, required conditionally" (required except for metered recurring usage_type). Sources confirm consistency across API versions up to 2026.
Citations:
- 1: https://docs.stripe.com/api/invoice-line-item/object?api-version=2024-12-18.acacia
- 2: https://stripe.com/docs/api/invoices/line_item
- 3: https://docs.stripe.com/api/invoice-line-item/object
- 4: https://docs.stripe.com/api/checkout/sessions/line_items?api-version=2024-06-20
- 5: https://docs.stripe.com/api/checkout/sessions/object?api-version=2026-03-25.dahlia
- 6: https://docs.stripe.com/api/checkout/sessions/object?api-version=2025-03-31.basil
- 7: https://docs.stripe.com/api/checkout/sessions/object?api-version=2024-06-20
🏁 Script executed:
# Check Stripe types definition in the project or node_modules
rg "LineItem|quantity" --type ts -A 3 -B 1 | head -100Repository: dubinc/dub
Length of output: 7977
🏁 Script executed:
# Look for stripe types definition
find node_modules -name "*.d.ts" -path "*stripe*" -type f 2>/dev/null | head -20Repository: dubinc/dub
Length of output: 36
🏁 Script executed:
# Check package.json for stripe version
cat package.json | grep -A 5 -B 5 stripeRepository: dubinc/dub
Length of output: 36
🏁 Script executed:
# Find package.json files
find . -maxdepth 2 -name "package.json" -type f | head -5Repository: dubinc/dub
Length of output: 69
🏁 Script executed:
grep -A 2 -B 2 '"stripe"' package.jsonRepository: dubinc/dub
Length of output: 36
🏁 Script executed:
rg "stripe" package.json -iRepository: dubinc/dub
Length of output: 36
🏁 Script executed:
find . -name "package.json" -type f -exec grep -l stripe {} \;Repository: dubinc/dub
Length of output: 113
🏁 Script executed:
grep "stripe" apps/web/package.json | head -5Repository: dubinc/dub
Length of output: 114
Ensure quantity field is non-null before returning products.
Line 74 passes line.quantity directly, but Stripe line-item quantity is nullable. The type predicate on lines 78-79 only checks if the object is null, not if individual fields are null, creating a type safety gap.
Suggested fix
return {
id: productId,
amount: line.amount_total,
- quantity: line.quantity,
+ quantity: line.quantity ?? 1,
};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@apps/web/app/`(ee)/api/stripe/integration/webhook/utils/get-checkout-session-products.ts
around lines 73 - 79, The mapped product creation is passing nullable Stripe
line.quantity directly (in the map that returns { id, amount, quantity }) and
the subsequent type predicate only checks for null objects, not for missing
quantity; update the mapping logic in get-checkout-session-products so it either
filters out line items with null/undefined quantity or converts nullable
quantities to a safe number (e.g., default 1) and then update the type predicate
used after .filter to assert quantity is a number (e.g., p is { id: string;
amount: number; quantity: number }) so callers of the resulting array can safely
rely on a non-null quantity without changing other code paths.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/web/lib/partners/create-partner-commission.ts`:
- Around line 193-195: The code currently assumes rewards[0] is the canonical
reward which makes eligibility checks (e.g., firstCommission.rewardId and
maxDuration) order-dependent; instead, iterate each computed reward from rewards
(do not assign a single `reward = rewards[0]`) and validate eligibility for each
reward individually, applying the `firstCommission.rewardId` and `maxDuration`
checks per reward (or persist a separate commission per reward/product) before
aggregating earnings or setting commission fields so downstream logic is based
on per-reward validation rather than list order.
🪄 Autofix (Beta)
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
Run ID: 62c948f9-23ba-45a2-8993-af287118bcfa
📒 Files selected for processing (1)
apps/web/lib/partners/create-partner-commission.ts
|
@coderabbitai full review please |
|
🧠 Learnings used✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (2)
apps/web/app/(ee)/api/stripe/integration/webhook/utils/get-checkout-session-products.ts (1)
71-79:⚠️ Potential issue | 🟠 Major | ⚡ Quick winCoerce checkout line-item quantity to a number.
line.quantityis nullable, so the current filter can still return a product with a non-numeric quantity while assertingquantity: number. Flat per-unit rewards then compute against bad data.Suggested fix
return { id: productId, amount: line.amount_total, - quantity: line.quantity, + quantity: line.quantity ?? 1, };Stripe Checkout Session line item `quantity` field nullable official docs🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/`(ee)/api/stripe/integration/webhook/utils/get-checkout-session-products.ts around lines 71 - 79, The returned product objects currently use line.quantity which is nullable; update the mapping in getCheckoutSessionProducts (the map that returns { id: productId, amount: line.amount_total, quantity: line.quantity }) to coerce quantity to a number (e.g., Number(line.quantity) or a default like 1) and then tighten the subsequent filter to only allow products where quantity is a finite positive number (use Number.isFinite and > 0) so the type predicate (p is { id: string; amount: number; quantity: number }) is correct and downstream reward calculations receive a valid numeric quantity.apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts (1)
247-262:⚠️ Potential issue | 🟠 Major | ⚡ Quick winNormalize nullable Stripe invoice quantities before building
products.
line.quantitycan be null here, but the type predicate only filters out null objects. That letsproductsclaimquantity: numberwhile still carrying a null at runtime, which underpays flat per-unit commissions for matched line items.Suggested fix
return { id: productId, amount: line.amount, - quantity: line.quantity, + quantity: line.quantity ?? 1, };Stripe Invoice Line Item `quantity` field nullable official docs🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/`(ee)/api/stripe/integration/webhook/invoice-paid.ts around lines 247 - 262, When building products from invoice.lines.data, normalize nullable Stripe line.quantity before asserting the product shape: update the mapping in the products creation (the block referencing invoice.lines.data, line.pricing?.price_details?.product, and line.quantity) to coerce null/undefined quantities to a safe numeric default (e.g., use line.quantity ?? 1 or Number(line.quantity ?? 1)) so the resulting objects truly satisfy { id: string; amount: number; quantity: number } and avoid underpaying per-unit commissions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@apps/web/app/`(ee)/api/stripe/integration/webhook/utils/get-checkout-session-products.ts:
- Around line 90-95: The catch in getCheckoutSessionProducts currently swallows
Stripe errors and returns an empty array, which conflates transient failures
with a legitimately empty cart; change the error handling in
getCheckoutSessionProducts so it preserves failure signals (either rethrow the
caught error or return null instead of []) and update callers (e.g.,
checkout-session-completed.ts and create-partner-commission logic that checks
products.length) to treat null as "fetch failed" and skip or retry commission
creation rather than falling back to the generic sale path.
---
Duplicate comments:
In `@apps/web/app/`(ee)/api/stripe/integration/webhook/invoice-paid.ts:
- Around line 247-262: When building products from invoice.lines.data, normalize
nullable Stripe line.quantity before asserting the product shape: update the
mapping in the products creation (the block referencing invoice.lines.data,
line.pricing?.price_details?.product, and line.quantity) to coerce
null/undefined quantities to a safe numeric default (e.g., use line.quantity ??
1 or Number(line.quantity ?? 1)) so the resulting objects truly satisfy { id:
string; amount: number; quantity: number } and avoid underpaying per-unit
commissions.
In
`@apps/web/app/`(ee)/api/stripe/integration/webhook/utils/get-checkout-session-products.ts:
- Around line 71-79: The returned product objects currently use line.quantity
which is nullable; update the mapping in getCheckoutSessionProducts (the map
that returns { id: productId, amount: line.amount_total, quantity: line.quantity
}) to coerce quantity to a number (e.g., Number(line.quantity) or a default like
1) and then tighten the subsequent filter to only allow products where quantity
is a finite positive number (use Number.isFinite and > 0) so the type predicate
(p is { id: string; amount: number; quantity: number }) is correct and
downstream reward calculations receive a valid numeric quantity.
🪄 Autofix (Beta)
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
Run ID: 41fbbb45-7a63-4064-b21f-800ddd3ba7f7
📒 Files selected for processing (7)
apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.tsapps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.tsapps/web/app/(ee)/api/stripe/integration/webhook/utils/get-checkout-session-product-id.tsapps/web/app/(ee)/api/stripe/integration/webhook/utils/get-checkout-session-products.tsapps/web/lib/partners/create-partner-commission.tsapps/web/lib/partners/evaluate-reward-conditions.tsapps/web/lib/zod/schemas/rewards.ts
💤 Files with no reviewable changes (1)
- apps/web/app/(ee)/api/stripe/integration/webhook/utils/get-checkout-session-product-id.ts
| } catch (error) { | ||
| console.log( | ||
| "[getCheckoutSessionProducts] Failed to get checkout session products:", | ||
| error, | ||
| ); | ||
| return []; |
There was a problem hiding this comment.
Don't collapse Stripe fetch failures into an empty product list.
Returning [] here makes a transient Stripe failure indistinguishable from a real empty cart. In apps/web/lib/partners/create-partner-commission.ts, products.length === 0 falls back to the generic sale path, so programs that use sale.productId modifiers can get the wrong commission instead of a safe skip/retry.
One way to preserve the failure signal
-export async function getCheckoutSessionProducts({
+export async function getCheckoutSessionProducts({
checkoutSessionId,
stripeAccountId,
mode,
}: {
checkoutSessionId: string;
stripeAccountId?: string | null;
mode: StripeMode;
-}) {
+}): Promise<{ id: string; amount: number; quantity: number }[] | null> {
@@
} catch (error) {
console.log(
"[getCheckoutSessionProducts] Failed to get checkout session products:",
error,
);
- return [];
+ return null;
}
}Then have checkout-session-completed.ts skip or retry commission creation when the result is null.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@apps/web/app/`(ee)/api/stripe/integration/webhook/utils/get-checkout-session-products.ts
around lines 90 - 95, The catch in getCheckoutSessionProducts currently swallows
Stripe errors and returns an empty array, which conflates transient failures
with a legitimately empty cart; change the error handling in
getCheckoutSessionProducts so it preserves failure signals (either rethrow the
caught error or return null instead of []) and update callers (e.g.,
checkout-session-completed.ts and create-partner-commission logic that checks
products.length) to treat null as "fetch failed" and skip or retry commission
creation rather than falling back to the generic sale path.
Summary by CodeRabbit
New Features
Bug Fixes