Skip to content

feat(payments): ✨ Payments foundation with a Stripe adapter - #845

Open
aXenDeveloper wants to merge 13 commits into
canaryfrom
claude/vitnode-payments-foundation-ejdu0r
Open

aXenDeveloper wants to merge 13 commits into
canaryfrom
claude/vitnode-payments-foundation-ejdu0r

Conversation

@aXenDeveloper

@aXenDeveloper aXenDeveloper commented Oct 4, 2026 •

Copy link
Copy Markdown
Owner

Description

What?

This PR adds the first complete version of Payments to VitNode.

Core (@vitnode/core/payments)

  • Payments is configured only in vitnode.api.config.ts (payments?: PaymentsConfig) and validated at startup. It is on when at least one provider is registered.
  • With no payments key or an empty providers list, Payments is off:
    • Settings → Billing, AdminCP → Payments and plugin payment pages (/example/payments) answer 404, and their menu entries are hidden.
    • requirePaymentsEnabled(queryClient) lets a plugin page do the same.
    • AdminCP nav items can declare a feature they need.
  • Several providers can run side by side, each with its own id and webhook URL; defaultProvider picks the one checkouts use. The checkout, offers and portal hooks accept an optional provider, and Manage billing opens the portal of the subscription's own provider.
  • The UI reads a public view from GET /payments/settings, which never includes a secret.
  • The money model stores integer minor units plus an ISO 4217 code. Amounts are formatted with Intl.NumberFormat, and the display currency is separate from the UI language.
  • There is a PaymentProvider contract with normalized types and error kinds. Core does not import the Stripe SDK.
  • Plugins register offers with definePaymentOffer on a module's paymentOffers: one-time, or month/year subscriptions with one price per currency.
  • Checkout requires a signed-in user and is idempotent:
    • A per-user, per-offer advisory lock and a partial unique index allow only one open purchase per offer.
    • The purchase and the checkout attempt are saved before the provider call.
    • The provider is called outside any transaction, with a stored idempotency key, so a timed-out call shows as unknown and can be replayed safely.
  • Incoming webhooks keep their raw bytes, have a 512 KB limit and are verified. The inbox row and its queue job are written in one transaction before the 200. Processing reads the current state back from the provider, and statuses only move forward.
  • Fulfillment has one row per effect (grant, refund:<id>, subscription) and runs inside a locked transaction, so it happens exactly once in the database.
  • A reconciliation cron catches webhooks that were missed. Refunds and disputes are synced from the provider.
  • Five payments events are documented in built-in-events.mdx.

@vitnode/stripe (the first provider)

  • A separate package, linted with oxlint like the other packages.
  • Uses hosted Checkout and the Customer Portal, and pins API version 2026-09-30.endive.
  • Keeps test and live mode apart, and allows only currencies whose minor units Stripe handles exactly.
  • The example apps register it only when STRIPE_SECRET_KEY is set.

UI

  • Settings → Billing shows purchases and subscriptions, with Manage billing.
  • AdminCP → Payments has an overview with per-currency totals and worker health, plus purchases, subscriptions, webhook events, and retry. It needs payments: can_view / can_retry.
  • /example/payments in @vitnode/example has a lifetime pass and a monthly/yearly Pro plan. Its features are protected on the server, and it supports several currencies.

Docs

  • docs/dev/payments/*: overview, configuration, Providers (overview with multiple providers, plus Stripe setup), one-time and subscription tutorials, reliability, reference and troubleshooting.
  • Real screenshots, in WebP.

Migrations

  • 20261011150917_add_core_payments and 20261011150922_add_example_payments, regenerated after canary's newest migration.

The self-review commit fixes these problems:

  • A session that has been replaced is now expired at the provider first.
  • A late expired event from an old session is ignored, and a checkout that was paid twice is flagged.
  • A stale subscription read can no longer overwrite newer data or bring an ended subscription back.
  • A duplicate provider subscription is flagged.
  • Locks are always taken purchase first.
  • A succeeded refund can no longer slide back to pending.
  • The refresh throttle is now an atomic database claim, so it works without Redis.

Test setup

  • Canary removed src/tests/postgres.ts and the CI Postgres service. This PR keeps that small helper because the payments tests need real transactions, row locks and advisory locks, which memory-db cannot imitate.
  • The payments tests skip unless VITNODE_TEST_POSTGRES_URL is set, so CI does not run them. Run them with VITNODE_TEST_POSTGRES_URL=postgresql://root:root@localhost:5432/postgres npx vitest run src/api/models/payments src/api/modules/payments in packages/vitnode.

Why?

This is the payments foundation plugins can build on, without the shop, SaaS, coupons, wallet, trials, proration, tax or invoicing (all out of scope).

Validation

  • pnpm test passes: core 8,768 passed and 65 skipped (the Postgres payments tests), stripe 29, example 10, blog 35, create-vitnode-app 134. pnpm test:types passes (405).
  • The payments tests pass against a local Postgres 16 with VITNODE_TEST_POSTGRES_URL set: 136 of 136.
  • pnpm lint:fix, pnpm lint and pnpm build (including the docs) pass.
  • vitnode migrate works on a fresh database. Applying the two payments migrations on top of canary's schema keeps the existing rows. drizzle-kit generate reports no drift.
  • Checked in the running app as the test admin:
    • Without Stripe keys: settings returns "enabled": false; /settings/billing, /example/payments and /admin/core/payments answer 404; and neither Settings nor the AdminCP sidebar shows a payments entry.
    • With placeholder test keys: all three pages answer 200 and both entries are back.
  • Each of the six regression tests from the self-review commit fails on the code before that commit.

Not verified: Stripe's API is blocked by this environment's network and there were no credentials, so no real Stripe checkout, webhook or payment was made. The Stripe adapter is covered by unit tests against recorded API shapes only. Please run the test-mode flow in docs/dev/payments/providers/stripe before release.

🤖 Generated with Claude Code

https://claude.ai/code/session_01S1hrAnTnFPh8ph9hjsPhW5

@github-actions github-actions Bot added the 💡 Feature A new feature label Oct 4, 2026
@github-actions

github-actions Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

React Doctor found 10 new issues in 5 files · 10 warnings · score 90 / 100 (Great) · 0 fixed · vs canary

10 warnings

src/api/models/payments/reconcile.ts

  • ⚠️ L58 await inside a loop async-await-in-loop
  • ⚠️ L119 await inside a loop async-await-in-loop
  • ⚠️ L191 await inside a loop async-await-in-loop
  • ⚠️ L200 await inside a loop async-await-in-loop

src/api/models/payments/sync.ts

  • ⚠️ L569 Sequential independent awaits server-sequential-independent-await
  • ⚠️ L772 await inside a loop async-await-in-loop
  • ⚠️ L853 await inside a loop async-await-in-loop

src/pages/payments/payments-page.tsx

  • ⚠️ L347 React function has high control-flow complexity no-high-complexity-react-function

src/payments/config.test.ts

  • ⚠️ L18 Secret in client code no-secrets-in-client-code

src/views/admin/views/core/payments/purchase-detail-content.tsx

  • ⚠️ L282 Duplicated JSX structure duplicate-jsx-subtree

Reviewed by React Doctor for commit c2e17a2. See inline comments for fixes.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d02c7d14c5

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const provider = providerForRow(c, purchase);
const checkout = await latestCheckout(c, purchase.id);

if (checkout?.externalId && checkout.status === "open") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Serialize cancellation with checkout creation

When cancellation races a provider create call, the latest attempt is still creating and has no externalId, so this branch is skipped and the purchase is marked canceled. The in-flight openCheckout call can then store the newly created session and return its URL without rechecking the purchase; the original tab redirects to a payable checkout after another tab reported a successful cancellation, and a resulting webhook promotes the canceled purchase to paid. Coordinate cancellation with creating/unknown attempts and recheck the purchase before exposing a newly returned checkout URL.

Useful? React with 👍 / 👎.

Comment on lines +190 to +191
const manage = () => {
portal.mutate(undefined, {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Open the portal for the subscription's provider

In a multi-provider installation, each subscription's Manage button is shown based on subscription.provider, but portal.mutate(undefined) omits that provider and the server therefore uses defaultProvider. For subscriptions purchased through another provider—or after changing the default—this either returns no_billing_account or opens an unrelated provider portal, preventing the user from managing the displayed subscription.

Useful? React with 👍 / 👎.

async () =>
await Promise.all([
stripe.paymentIntents.retrieve(paymentId),
stripe.refunds.list({ limit: 100, payment_intent: paymentId }),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Page through all refunds before computing totals

For a payment with more than 100 partial refund records, Stripe's list response is paginated and this reads only the first page. amountRefunded and the normalized refund list are then incomplete, so core can leave a fully refunded purchase marked partial and omit fulfillment callbacks for refunds outside that page. Follow pagination until has_more is false before calculating the total.

Useful? React with 👍 / 👎.

Comment on lines +103 to +104
.from(core_payments_purchases)
.where(eq(core_payments_purchases.disputeStatus, "open")),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include invoice disputes in the attention count

When a renewal payment is disputed, syncPayment stores the dispute status on core_payments_invoices, not on the originating purchase. This overview counts only purchase rows, so disputesOpen can remain zero for every recurring-payment dispute even though those invoice records require administrator attention; include open invoice disputes in the total as well.

Useful? React with 👍 / 👎.

claude added 12 commits October 11, 2026 14:56
…ion and schema

- Integer minor-unit Money with ISO 4217 exponents and Intl formatting
- Separate payment, fulfillment, refund, checkout and subscription states
- Narrow provider contract with normalized results and errors
- Typed offer registration collected from the whole module tree
- Optional, validated `payments` API config and public projection
- Billing tables: customers, purchases, checkouts, subscriptions, invoices,
  adjustments, webhook inbox and fulfillments

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1hrAnTnFPh8ph9hjsPhW5
… Stripe adapter

- @vitnode/stripe: hosted Checkout, subscriptions, Customer Portal and
  raw-body webhook verification, pinned to API version 2026-09-30.endive
- Authenticated, idempotent checkout: server-derived prices, per-offer lock,
  provider calls outside transactions with per-attempt idempotency keys
- Webhook inbox: verify, store and queue in one transaction, ack after that
- Queue-driven processing that re-reads provider state; monotonic statuses
- Fulfillment rows unique per business effect; handlers run in a locked tx
- Refund and dispute sync attributed to purchases or recurring invoices
- Bounded reconciliation cron and re-queueing of stuck work
- Example plugin: lifetime pass and Pro plan offers with an access table
- Conditional Stripe config in both API deployment modes, opt-in in-process
  cron for the embedded API, migrations for core and example tables

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1hrAnTnFPh8ph9hjsPhW5
… and reconciliation

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1hrAnTnFPh8ph9hjsPhW5
…ests

- Overview with per-currency totals, attention counters and worker health
- Purchase and subscription lists with status, currency, provider and period
  filters; purchase detail with checkouts, fulfillments, refunds, disputes
  and invoices; webhook inbox list
- Idempotent retry of failed fulfillments and webhook events (can_retry)
- Tests: ownership, no-store headers, server-derived prices, refresh on
  return, portal isolation, staff permissions, browser-safe boundaries

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1hrAnTnFPh8ph9hjsPhW5
…the example page

- /settings/billing: purchases and subscriptions with server-side paging,
  per-currency formatting and Customer Portal access (shown when enabled)
- AdminCP Payments: overview with per-currency totals and worker health,
  purchases and subscriptions with currency/provider/period filters,
  webhook inbox with retry, purchase detail with fulfillment retry
- Shared money formatter, state badges, checkout and portal hooks and a
  bounded, backing-off return poller in @vitnode/core/tanstack/payments
- /example/payments: lifetime pass and monthly/yearly Pro plan with
  server-checked features, currency choice separate from the UI language
  and graceful handling when Payments is disabled

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1hrAnTnFPh8ph9hjsPhW5
…, tutorials and reference

- Overview of what core, the provider and a plugin own, and current scope
- API-only configuration for both deployment modes, env vars, currencies
  and the queue worker
- Stripe setup: test keys, webhook forwarding, the exact event list,
  Customer Portal settings, test clocks and test/live separation
- One-time and subscription tutorials built from the example plugin
- Reliability explanation, provider/API reference and troubleshooting
- Payment events in the built-in events reference
- Screenshots captured from the running app

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1hrAnTnFPh8ph9hjsPhW5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1hrAnTnFPh8ph9hjsPhW5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1hrAnTnFPh8ph9hjsPhW5
- Close a checkout that is about to expire at the provider before opening a
  new one, and ignore late expired/failed events from a superseded session.
- Keep the original payment when a second checkout for a purchase was also
  paid, and flag it for a refund.
- Scope the billing customer idempotency key to the install's web origin.
- Never let an older subscription read overwrite a newer one, never revive
  an ended subscription, and flag a second provider subscription for the
  same purchase instead of overwriting the first.
- Lock the purchase before the subscription, fulfillment or invoice row in
  every writer.
- Never move a succeeded refund back to pending; only a failed refund may
  lower the refunded total.
- Throttle buyer-triggered refreshes with an atomic row claim
  (refreshCheckedAt) so it holds across instances without Redis.
- Require checkoutExpiresInMinutes of at least 45.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1hrAnTnFPh8ph9hjsPhW5
- Regenerate the core and example payments migrations after canary's
  newest migration, so the snapshot chain stays linear.
- Allow a null queue id from QueueModel.dispatch in the webhook inbox.
- Move @vitnode/stripe from ESLint to oxlint like the other packages.
- Add the payments flag to the new middleware config test fixtures.
- Keep the opt-in Postgres test helper the payments integration tests use;
  they skip unless VITNODE_TEST_POSTGRES_URL is set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1hrAnTnFPh8ph9hjsPhW5
@aXenDeveloper
aXenDeveloper force-pushed the claude/vitnode-payments-foundation-ejdu0r branch from d02c7d1 to d988768 Compare October 11, 2026 15:22
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 11, 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
📝 Code Review ✅ Completed 2026-10-11T17:03:05.394450Z c2e17a2 New commits
ℹ️ 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.

@github-actions github-actions Bot added 💡 Feature A new feature and removed 💡 Feature A new feature labels Oct 11, 2026

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d9887686f7

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +514 to +515
lastError: "Abandoned: the provider never confirmed this checkout.",
status: "failed",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep uncertain attempts exclusive until they expire

When the provider creates a checkout but its response is lost, retrying after replayWindowMs but before the original expiresAt reaches this branch, labels the unknown attempt failed, and creates another attempt with a new idempotency key. The provider's first session can still be open—for the minimum 45-minute lifetime this can happen after 10 minutes—so the buyer receives a second payable session and can be charged twice. Continue replaying the original key, or prevent a replacement until the uncertain session can no longer be paid.

Useful? React with 👍 / 👎.

Comment on lines +394 to +395
externalCustomerId: state.customerId,
externalPriceId: state.priceId ?? before?.externalPriceId ?? null,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Persist subscription terms changed through the portal

When a provider portal allows a subscriber to switch price, interval, or currency, retrieve() returns the new terms but the update path stores only the new price ID; amount, currency, and interval are populated only on insert. Consequently billing keeps displaying the original terms, and because changedForPlugin also ignores these fields, onSubscriptionChanged is not rerun for plugins whose entitlement depends on the selected interval or price.

Useful? React with 👍 / 👎.

code: string,
message: string,
extra: Record<string, unknown> = {},
): HTTPException =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Return translatable payment error keys

Every payment rejection serialized here sends an English message, and PaymentsRequestError surfaces that message directly in checkout and billing toasts. In a non-English UI, failures such as disabled payments, checkout conflicts, or portal errors therefore appear in English; return a stable i18n key/code and translate it with use-intl in the web client instead.

AGENTS.md reference: AGENTS.md:L62-L62

Useful? React with 👍 / 👎.

…ders

- An empty `payments.providers` list turns Payments off instead of failing
  at startup, so apps can register providers only when their keys exist.
- With Payments off, Settings -> Billing, AdminCP -> Payments and plugin
  payment pages answer 404, and their navigation entries are hidden.
  `requirePaymentsEnabled` lets a plugin page do the same.
- Admin navigation items can name a feature they need (`feature`).
- Checkout, offers and billing portal hooks accept an optional `provider`;
  Manage billing opens the portal of the subscription's own provider.
- Docs: new Providers section (overview with multiple providers, Stripe
  setup with install and options), screenshots converted to WebP.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1hrAnTnFPh8ph9hjsPhW5
@github-actions github-actions Bot added 💡 Feature A new feature and removed 💡 Feature A new feature labels Oct 11, 2026
for (const purchase of due) {
try {
const provider = providerForRow(c, purchase);
const refreshed = await refreshPurchase(c, provider, purchase);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-doctor/async-await-in-loop (warning)

This for…of loop waits before starting the next iteration. If iterations perform independent asynchronous work, consider bounded concurrency; await syntax alone does not establish a speedup.

Fix → Consider concurrent calls only for independent asynchronous work. Shared queues or synchronous work may not benefit. Preserve resource limits, transaction ordering, and failure/cancellation semantics; observe all callback promises.

Docs


for (const subscription of due) {
try {
await syncSubscription(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-doctor/async-await-in-loop (warning)

This for…of loop waits before starting the next iteration. If iterations perform independent asynchronous work, consider bounded concurrency; await syntax alone does not establish a speedup.

Fix → Consider concurrent calls only for independent asynchronous work. Shared queues or synchronous work may not benefit. Preserve resource limits, transaction ordering, and failure/cancellation semantics; observe all callback promises.

Docs

.returning({ id: core_payments_webhook_events.id });

for (const { id } of fulfillments) {
await c.get("queue").dispatch({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-doctor/async-await-in-loop (warning)

This for…of loop waits before starting the next iteration. If iterations perform independent asynchronous work, consider bounded concurrency; await syntax alone does not establish a speedup.

Fix → Consider concurrent calls only for independent asynchronous work. Shared queues or synchronous work may not benefit. Preserve resource limits, transaction ordering, and failure/cancellation semantics; observe all callback promises.

Docs

}

for (const { id } of events) {
await c.get("queue").dispatch({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-doctor/async-await-in-loop (warning)

This for…of loop waits before starting the next iteration. If iterations perform independent asynchronous work, consider bounded concurrency; await syntax alone does not establish a speedup.

Fix → Consider concurrent calls only for independent asynchronous work. Shared queues or synchronous work may not benefit. Preserve resource limits, transaction ordering, and failure/cancellation semantics; observe all callback promises.

Docs

.where(eq(core_payments_subscriptions.id, subscription.id))
.for("update");

const [existing] = await tx

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-doctor/server-sequential-independent-await (warning)

This awaited initializer does not read the previous result. If the operations are independent asynchronous work, they may overlap; await syntax alone does not establish a speedup.

Fix → Consider Promise.all([...]) only for independent asynchronous work. Shared queues or synchronous work may not benefit. Preserve resource limits, transaction ordering, and failure/cancellation semantics.

Docs

];

for (const adjustment of adjustments) {
const [before] = await tx

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-doctor/async-await-in-loop (warning)

This for…of loop waits before starting the next iteration. If iterations perform independent asynchronous work, consider bounded concurrency; await syntax alone does not establish a speedup.

Fix → Consider concurrent calls only for independent asynchronous work. Shared queues or synchronous work may not benefit. Preserve resource limits, transaction ordering, and failure/cancellation semantics; observe all callback promises.

Docs


if (purchase.mode === "one_time") {
for (const refund of newRefunds) {
await scheduleFulfillment(c, tx, {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-doctor/async-await-in-loop (warning)

This for…of loop waits before starting the next iteration. If iterations perform independent asynchronous work, consider bounded concurrency; await syntax alone does not establish a speedup.

Fix → Consider concurrent calls only for independent asynchronous work. Shared queues or synchronous work may not benefit. Preserve resource limits, transaction ordering, and failure/cancellation semantics; observe all callback promises.

Docs

} from "./config";
import { definePaymentOffer } from "./offer";

const SECRET = "sk_test_must_never_leave_the_server";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-doctor/no-secrets-in-client-code (warning)

This hardcoded secret is a security vulnerability: it ships to the browser where anyone can read it.

Fix → Move secrets to server-only code. In TanStack Start, only VITE_* env vars are exposed to the browser, and they must not contain secrets

Docs

</CardHeader>
<CardContent className="overflow-x-auto">
<Table>
<TableHeader>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-doctor/duplicate-jsx-subtree (warning)

2 copies of this 6-node JSX tree appear across 2 files, repeating about 8 lines. Composition path: PurchaseDetailContent > div > Card > CardContent > Table > TableHeader.

Fix → Consider extracting a shared component if these trees represent the same UI concept. Keep them separate when the resemblance is incidental or the variants are likely to evolve independently.

Docs

);
};

const PaymentsPage = ({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-doctor/no-high-complexity-react-function (warning)

PaymentsPage has cyclomatic complexity 30, cognitive complexity 31, and maximum nesting depth 2, so its React logic is hard to understand and change. Extract independent branches into components or hooks.

Fix → Extract independent render branches and state logic into focused components or hooks until the control flow is easy to follow.

Docs

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c2e17a2a94

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +115 to +117
status: "failed",
})
.where(eq(core_payments_fulfillments.id, fulfillment.id));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve a concurrently completed fulfillment

When two workers execute the same fulfillment—possible after a queue lease expires or reconciliation requeues work—one handler can fail while the other succeeds after waiting for the row lock. The successful worker commits completedGeneration and status: "completed", but the failing worker then calls this unconditional update outside its rolled-back transaction and overwrites the row and purchase as failed. Since the generation is already complete, the AdminCP retry is no longer eligible, leaving a successfully granted purchase permanently reported as failed; condition the failure write on the generation still being incomplete.

Useful? React with 👍 / 👎.

Comment on lines +525 to +529
portal.mutate(
{
offerId: EXAMPLE_OFFERS.plan,
pluginId: CONFIG_PLUGIN.pluginId,
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Pass the subscription provider to the offer portal

When this plan's existing subscription belongs to a non-default provider, or the installation's default provider changes after purchase, this Manage action still omits provider and createPortalSession consequently selects the current default. The billing screen now passes subscription.provider, but this separate offer-page flow still opens an unrelated portal or returns no_billing_account; carry the provider from the subscription selected by usePlanSubscription into this mutation.

Useful? React with 👍 / 👎.

This branch has not been deployed

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

Labels

💡 Feature A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants