Skip to content

Conversation

@Niki2k1
Copy link
Contributor

@Niki2k1 Niki2k1 commented Aug 4, 2025

What kind of change does this PR introduce?

This PR adds sync for the checkout sessions (and line items)

Additional context

I created two new migrations (one for the checkout sessions and one for the line items.

I am not yet using this in production, so I am a little bit cautious. My tests worked fine though.

@jackkru69
Copy link

We also need this feature, can someone watch this PR?

@revoks
Copy link

revoks commented Aug 6, 2025

This feature is something we urgently need on our side as well.
Would love to see this merged - thanks so much for your work!

Copy link
Contributor

@kevcodez kevcodez left a comment

Choose a reason for hiding this comment

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

Thanks for the contribution!

Can you please

  • Add the webhooks to the README as supported
  • Clarify whether line items are present in webhooks
  • Add an appropriate webhook test

)

// Upsert line items into separate table
const lineItemsPromises = checkoutSessions
Copy link
Contributor

@kevcodez kevcodez Aug 7, 2025

Choose a reason for hiding this comment

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

This should be a single upsert and not one upsert per line item ideally

Are line items sent via the webhook by default? Otherwise the checkout session line items would never be there for a regular webhook

@Niki2k1
Copy link
Contributor Author

Niki2k1 commented Aug 7, 2025

Thank you for the Review.

I inspected a checkout session event and indeed there are no line items, so either I would've to fetch them via the api or ignore them and only sync checkout sessions. What do you think?

For my use-case it would be really nice to have the line items directly when syncing

@kevcodez
Copy link
Contributor

kevcodez commented Aug 7, 2025

@Niki2k1 yeah makes sense, i think a check session without persisting the line items themselves would not add too much value.

I think it would be best to call stripe.checkout.sessions.listLineItems (and set a high limit of 100) if no line items is provided - we also have an expansion feature that can be used (see expandEntity).

So

  • If line items is already present, use expandEntity
  • If line items are not present, pull from API using listLineItems

@jackkru69
Copy link

@Niki2k1 Hi, are you going to refine this PR?

@Niki2k1
Copy link
Contributor Author

Niki2k1 commented Aug 19, 2025

@jackkru69 yes, I will implement the changes and update the pr, but at earliest next week. Can't promise anything though.

@Niki2k1
Copy link
Contributor Author

Niki2k1 commented Aug 27, 2025

I updated the PR and added/changed the following items:

  • Updated README
  • Added tests with anonymized webhook payload from my production app
  • Improved LineItems Handling, by adding a custom fill function that retrieves items with listLineItems function and upserts them into the db

feat: handle custom checkout.session tableName
feat: added syncTimestamp logic for checkout sessions
@Niki2k1
Copy link
Contributor Author

Niki2k1 commented Aug 27, 2025

I merged main and resolved some conflicts and also added the new lastSyncedAt logic

@Niki2k1 Niki2k1 marked this pull request as ready for review August 27, 2025 21:32
@jackkru69
Copy link

@kevcodez Could you check it, pls?

@andrey-utkin
Copy link

Hi @Niki2k1 , thanks for your very useful contribution!

I have just tried a container image built from your feature branch, with database containing products of backfill by mainline codebase. I also retried from scratch. I use backfill with POST /sync. I got this error. Maybe the reason is that we might encounter more than one event about the same session, and see the same (by id) line item more than once?

stripe-sync-1  | {"level":"info","time":"2025-08-29T22:12:15.976Z","pid":1,"hostname":"e92d0932b284","msg":"Syncing checkout sessions"}
stripe-sync-1  | {"level":"info","time":"2025-08-29T22:12:15.976Z","pid":1,"hostname":"e92d0932b284","msg":"Fetching items to sync from Stripe"}
stripe-sync-1  | {"level":"info","time":"2025-08-29T22:12:58.862Z","pid":1,"hostname":"e92d0932b284","msg":"Upserting 1819 items"}
postgres-1     | 2025-08-29 22:12:59.458 UTC [54] ERROR:  there is no unique or exclusion constraint matching the ON CONFLICT specification
postgres-1     | 2025-08-29 22:12:59.458 UTC [54] STATEMENT:  INSERT INTO "stripe"."checkout_session_line_items" (
postgres-1     |                "id","object","adjustable_quantity","amount_subtotal","amount_total","currency","description","discounts","price","quantity","taxes","checkout_session", "last_synced_at"
postgres-1     |              )
postgres-1     |              VALUES (
postgres-1     |                $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12, $13
postgres-1     |              )
postgres-1     |              ON CONFLICT (id) DO UPDATE SET
postgres-1     |                "id" = EXCLUDED."id","object" = EXCLUDED."object","adjustable_quantity" = EXCLUDED."adjustable_quantity","amount_subtotal" = EXCLUDED."amount_subtotal","amount_total" = EXCLUDED."amount_total","currency" = EXCLUDED."currency","description" = EXCLUDED."description","discounts" = EXCLUDED."discounts","price" = EXCLUDED."price","quantity" = EXCLUDED."quantity","taxes" = EXCLUDED."taxes","checkout_session" = EXCLUDED."checkout_session",
postgres-1     |                last_synced_at = $14
postgres-1     |              WHERE "checkout_session_line_items"."last_synced_at" IS NULL
postgres-1     |                 OR "checkout_session_line_items"."last_synced_at" < $15;

@Niki2k1
Copy link
Contributor Author

Niki2k1 commented Aug 30, 2025

@andrey-utkin Awesome! Thank you so much for taking the time to test this.
I just tested with my Stripe Test Environment. I will also test it with prod, to try if I can reproduce and fix the error.

@andrey-utkin
Copy link

I guess ON CONFLICT (id) would work if checkout_session_line_items id itself was a primary key.

I am not even sure what this primary key specification means: primary key ("checkout_session", "id")

Indexes/keys end up this way:

Indexes:
    "checkout_session_line_items_pkey" PRIMARY KEY, btree (checkout_session, id)
    "stripe_checkout_session_line_items_price_idx" btree (price)
    "stripe_checkout_session_line_items_session_idx" btree (checkout_session)
Foreign-key constraints:
    "checkout_session_line_items_checkout_session_fkey" FOREIGN KEY (checkout_session) REFERENCES checkout_sessions(id)

@@ -0,0 +1,26 @@
create table if not exists "stripe"."checkout_session_line_items" (
"id" text,
Copy link
Contributor

Choose a reason for hiding this comment

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

These should be unique and therefore this should have a primary key to make the upsert work

@kevcodez
Copy link
Contributor

kevcodez commented Sep 8, 2025

Given checkout line item has a unique id, was there a reason not marking this as primary key in the migration? This is leading to the insert failing

Also, can you please add an integration test that fully covers this including the upsert of the line items? We should catch an error like this during an integration test and not a manual one ideally

@Niki2k1
Copy link
Contributor Author

Niki2k1 commented Sep 8, 2025

I replaced the composite key with a primary key just on the id field.
With my production stripe data it works now. I'll look into the integration tests.

@Niki2k1
Copy link
Contributor Author

Niki2k1 commented Sep 14, 2025

I added a test (was a good idea because I found multiple issues like missing fields)
For now, it's only to test the insert. Do we need other cases as well?

I also added on delete cascade on the lineItems reference fields.
So when a checkout session is deleted, the line items are getting deleted as well. For the prices reference, that's not really needed, as prices and checkout_sessions can't be deleted in stripe. But was useful in testing (for cleanup) and I don't see what would speak against it.

Copy link
Contributor

@kevcodez kevcodez left a comment

Choose a reason for hiding this comment

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

Overall LGTM with the tests, two small things I noticed that would have to be addressed so we can merge this 👍

return this.stripe.refunds.retrieve(stripeId).then((it) => this.upsertRefunds([it]))
} else if (stripeId.startsWith('cs_')) {
return this.stripe.checkout.sessions
.retrieve(stripeId)
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing { expand: ['line_items'] } here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The line_items are getting filled in the upsert itself via their own endpoint. They are not available in the /v1/checkout/sessions/:id endpoint.
correct me if I am wrong

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I will remove it there then as well 😅
was left over from when I started.
I moved over to the fillCheckoutSessionsLineItems which uses the /v1/checkout/sessions/:id/line_items endpoint

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Alright!


async fillCheckoutSessionsLineItems(checkoutSessionIds: string[], syncTimestamp?: string) {
for (const checkoutSessionId of checkoutSessionIds) {
const lineItemResponses = await this.stripe.checkout.sessions.listLineItems(
Copy link
Contributor

Choose a reason for hiding this comment

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

Stripe supports auto-pagination in the node sdk, see https://docs.stripe.com/api/pagination/auto?lang=node

You should likely be using for await to ensure that this iterates over all pages, i.e. if this has 101 items, you wouldn't get the last item

You can use expandEntity - example usage


 await this.expandEntity(charges, 'refunds', (id) =>
      this.stripe.refunds.list({ charge: id, limit: 100 })
    )

This will paginate through all items and replace the property, then you can just do your upsert afterwards

fix: backfill prices before inserting lineItems
feat: auto pagination for lineItems
fix(checkoutSessions test): race condition
@Niki2k1
Copy link
Contributor Author

Niki2k1 commented Sep 15, 2025

  • removed line_items expands
  • backfill prices (needed for the new price table reference) to ensure prices are there before insertion of lineItems
  • using auto pagination feature
  • fix tests

also tested again with my live data

@kevcodez kevcodez merged commit 7fbaab6 into supabase:main Sep 15, 2025
2 checks passed
@kevcodez
Copy link
Contributor

Nice! Thanks for the contribution - will trigger a release now

@Niki2k1
Copy link
Contributor Author

Niki2k1 commented Sep 15, 2025

Thank you!
Also for the reviews.
I learned cool new stuff.

@github-actions
Copy link

🎉 This PR is included in version 0.42.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants