-
Notifications
You must be signed in to change notification settings - Fork 3k
Stripe integration - account for multiple line items for a given invoice #3824
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
eac11b1
2ad8449
263c6d2
30588a6
168a09f
fa93ce8
a262613
65d6119
6a888ad
0b01a27
360b19f
7b4abfc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| import { stripeAppClient } from "@/lib/stripe"; | ||
| import { StripeMode } from "@/lib/types"; | ||
| import type Stripe from "stripe"; | ||
|
|
||
| export function productIdFromLineItemPrice( | ||
| price: Stripe.Price | string | null | undefined, | ||
| ): string | null { | ||
| if (!price || typeof price === "string") { | ||
| return null; | ||
| } | ||
|
|
||
| if (!price.product) { | ||
| return null; | ||
| } | ||
|
|
||
| const product = price.product; | ||
|
|
||
| if (typeof product === "string") { | ||
| return product; | ||
| } | ||
|
|
||
| if ("deleted" in product && product.deleted) { | ||
| return null; | ||
| } | ||
|
|
||
| return product.id; | ||
| } | ||
|
|
||
| export async function getCheckoutSessionProducts({ | ||
| checkoutSessionId, | ||
| stripeAccountId, | ||
| mode, | ||
| }: { | ||
| checkoutSessionId: string; | ||
| stripeAccountId?: string | null; | ||
| mode: StripeMode; | ||
| }) { | ||
| if (!stripeAccountId) { | ||
| return []; | ||
| } | ||
|
|
||
| try { | ||
| const stripeApp = stripeAppClient({ | ||
| mode, | ||
| }); | ||
|
|
||
| const lineItems = await stripeApp.checkout.sessions.listLineItems( | ||
| checkoutSessionId, | ||
| { | ||
| expand: ["data.price.product"], | ||
| limit: 10, | ||
| }, | ||
|
devkiran marked this conversation as resolved.
|
||
| { | ||
| stripeAccount: stripeAccountId, | ||
| }, | ||
| ); | ||
|
|
||
| if (lineItems.data.length === 0) { | ||
| console.log( | ||
| `[getCheckoutSessionProducts] No line items found for checkout session ${checkoutSessionId}.`, | ||
| ); | ||
| return []; | ||
| } | ||
|
|
||
| const products = lineItems.data | ||
| .map((line) => { | ||
| const productId = productIdFromLineItemPrice(line.price); | ||
|
|
||
| if (!productId) return null; | ||
|
|
||
| return { | ||
| id: productId, | ||
| amount: line.amount_total, | ||
| quantity: line.quantity, | ||
| }; | ||
| }) | ||
| .filter( | ||
| (p): p is { id: string; amount: number; quantity: number } => | ||
| p !== null, | ||
|
Comment on lines
+73
to
+79
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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:
💡 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:
🏁 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 Line 74 passes Suggested fix return {
id: productId,
amount: line.amount_total,
- quantity: line.quantity,
+ quantity: line.quantity ?? 1,
};🤖 Prompt for AI Agents |
||
| ); | ||
|
|
||
| if (products.length === 0) { | ||
| console.log( | ||
| `[getCheckoutSessionProducts] No valid products found for checkout session ${checkoutSessionId}.`, | ||
| ); | ||
| return []; | ||
| } | ||
|
|
||
| return products; | ||
| } catch (error) { | ||
| console.log( | ||
| "[getCheckoutSessionProducts] Failed to get checkout session products:", | ||
| error, | ||
| ); | ||
| return []; | ||
|
Comment on lines
+90
to
+95
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't collapse Stripe fetch failures into an empty product list. Returning 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 🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.