Skip to content
This repository was archived by the owner on Jul 21, 2026. It is now read-only.

feat(dodo): add new provider#38

Closed
almeidazs wants to merge 7 commits into
mainfrom
feat/dodo
Closed

feat(dodo): add new provider#38
almeidazs wants to merge 7 commits into
mainfrom
feat/dodo

Conversation

@almeidazs

@almeidazs almeidazs commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features
    • Added Dodo Payments as an official supported provider with catalog-driven hosted checkout, customer management, catalog operations, webhooks, and subscription-aware dashboard syncing.
    • CLI setup now supports selecting Dodo as a provider.
    • Updated provider catalog/checkout behavior and capabilities, including webhook normalization and Dodo-specific PIX handling (pix: false).
  • Documentation
    • Added/expanded Dodo provider docs and setup examples, plus updated provider support and comparison pages across the website.
  • Tests
    • Added a new Dodo test suite covering catalog, customers, dashboard syncing, provider requests, and webhooks.
  • Chores
    • Applied minor/patch version bumps for the Dodo package and CLI provider selection logic.

@almeidazs almeidazs self-assigned this Jun 13, 2026
@almeidazs almeidazs added the enhancement New feature or request label Jun 13, 2026
@vercel

vercel Bot commented Jun 13, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
paymesh Ready Ready Preview, Comment Jun 15, 2026 1:43am

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0e110202-cb53-4319-841f-dc26e81ab0dd

📥 Commits

Reviewing files that changed from the base of the PR and between f1f0749 and bf2f3ff.

📒 Files selected for processing (1)
  • apps/web/content/docs/providers/dodo.mdx

📝 Walkthrough

Walkthrough

This PR introduces @paymesh/dodo, a new first-party payment provider implementing catalog-driven hosted checkout flows with customer management, catalog reads, subscription-aware dashboard sync, and webhook normalization. The implementation spans type definitions, core provider logic, utilities, tests, comprehensive documentation, and website integration.

Changes

Dodo Payment Provider

Layer / File(s) Summary
Type definitions and capability constants
packages/dodo/src/types.ts, packages/dodo/src/shared/constants.ts
Type contracts for provider options, customer/payment/refund/subscription/catalog entities, webhook payloads, and constants defining supported capabilities, status mappings, event/hook routing, and PIX method types.
Provider factory and core implementations
packages/dodo/src/index.ts
dodo() factory resolving sandbox mode, API key/webhook config, and wiring payment creation, customer operations (get/upsert/delete), catalog listing, dashboard sync delegation, and webhook verification/handling.
Data mappers, utilities, and webhook normalization
packages/dodo/src/shared/mapper.ts, packages/dodo/src/shared/utils.ts, packages/dodo/src/shared/webhooks.ts
Mapping functions converting Dodo entities to Paymesh models, plus utilities for metadata serialization, customer/product validation, webhook signing/verification, status conversion, PIX detection, and webhook event normalization.
Dashboard resource synchronization
packages/dodo/src/shared/sync.ts
Sync functions for customer, payment, and subscription resources with conditional PIX data upsert and event type derivation.
Comprehensive provider test suite
packages/dodo/test/catalog.test.ts, packages/dodo/test/customers.test.ts, packages/dodo/test/dashboard.test.ts, packages/dodo/test/dodo.test.ts, packages/dodo/test/webhooks.test.ts
Tests validating payment creation, customer operations, catalog listing, dashboard sync, webhook verification/normalization, and provider configuration.
Package configuration
packages/dodo/package.json, packages/dodo/tsconfig.json, packages/dodo/tsdown.config.mjs, packages/dodo/README.md
ESM/CJS package manifest, TypeScript config with path aliases, build config, and README with installation and usage examples.
Dedicated provider documentation page
apps/web/content/docs/providers/dodo.mdx
Comprehensive documentation covering setup, capabilities, hosted checkout flow, customer operations, catalog reads, dashboard sync, webhook verification, and guidance on when to choose Dodo.
Documentation updates to existing pages
apps/web/content/docs/installation.mdx, apps/web/content/docs/introduction.mdx, apps/web/content/docs/basic-usage.mdx, apps/web/content/docs/concepts/payment-providers.mdx, apps/web/content/docs/concepts/pix.mdx, apps/web/public/llms.txt, apps/web/src/app/page.tsx, apps/web/src/lib/docs.tsx
Updates to reflect Dodo as a shipped live provider with catalog-driven checkout, clarify PIX support, add provider tabs/comparisons, and update provider status/capability matrices.
Docs site infrastructure
apps/web/.source/browser.ts, apps/web/.source/server.ts, apps/web/src/components/docs/mdx-components.tsx, apps/web/src/lib/docs-navigation.ts
Fumadocs collection registries, MDX component link mappings, and navigation configuration for the Dodo provider documentation.
CLI provider support and release
packages/cli/src/shared/providers.ts, package.json, .changeset/cyan-poems-boil.md, README.md, packages/paymesh/README.md
CLI provider entry for Dodo detection/selection, typecheck script update, changeset documenting minor bump for @paymesh/dodo and patch for @paymesh/cli, and README clarifications on PIX support per provider.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

  • usepaymesh/paymesh#27: Introduced Fumadocs registries that this PR extends by adding new lazy-import mappings for the Dodo provider documentation.
  • usepaymesh/paymesh#28: Added sandbox mode support that Dodo's implementation threads through request options, mapping, dashboard sync, and webhook normalization.
  • usepaymesh/paymesh#31: Established the PROVIDERS configuration and detectProviderFromDeps mechanism that Dodo extends in the CLI provider support layer.

Suggested labels

documentation

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request lacks any description content, failing to meet the repository template requirements for context, rationale, checklist completion, and documentation updates. Add a description following the provided template, including change summary, related issues, completed checklist items, and any relevant implementation notes.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title 'feat(dodo): add new provider' accurately and concisely summarizes the primary change—introducing a new Dodo provider to the Paymesh system.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/dodo

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 12

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/content/docs/providers/dodo.mdx`:
- Line 16: Hyphenate the compound modifier "Dodo hosted checkout" to
"Dodo-hosted checkout" so the sentence reads "...the Dodo-hosted checkout can
still offer Pix inside the redirect flow."; update the phrase wherever "Dodo
hosted checkout" appears (search for the exact string) to use the hyphenated
form.

In `@apps/web/public/llms.txt`:
- Around line 169-187: The Providers quick-link list in llms.txt is missing the
Dodo provider entry; add a link to `/docs/providers/dodo` in the Providers
quick-link section so the newly-live "Dodo" entry (the block starting "### Dodo"
that lists `@paymesh/dodo` features) is discoverable and the file is internally
consistent—update the Providers quick-link list to include
`/docs/providers/dodo` alongside the other provider paths.

In `@apps/web/src/lib/docs.tsx`:
- Around line 3881-3884: The displayed snippet uses an unsupported delete call;
update the customerSnippet used by the DocCodeBlock (the variable
customerSnippet rendered in DocCodeBlock with filename
"src/server/customers.ts") so it does not call customers.delete(); instead
replace that line with a supported operation (e.g., show
customers.get/customers.update or just the creation example) or remove the
delete call entirely so the rendered example matches the docs note that delete
is unsupported.

In `@packages/dodo/src/index.ts`:
- Around line 54-65: The dodo factory currently leaves baseUrl pointing at
DODO_LIVE_BASE_URL when sandbox: true because sandbox only affects
resolveProviderSandbox(); update the initialization so the effective base URL
respects sandbox: change the logic in the dodo function (symbols: dodo,
resolveProviderSandbox) to compute an effectiveBaseUrl (or set baseUrl) using:
if sandbox === true and baseUrl was not explicitly provided, use
DODO_TEST_BASE_URL; otherwise keep the provided baseUrl or existing default;
ensure resolveProviderSandbox continues to return the correct boolean but uses
the same sandbox input and the final network requests/readable metadata use the
effectiveBaseUrl.
- Around line 55-69: The code builds the Authorization header even when apiKey
is undefined, causing "Bearer undefined" to be sent; add a validation at the
start of the provider factory (where apiKey is read from DodoProviderOptions) to
throw a clear error when apiKey is missing or empty, and move/guard the headers
construction so it only occurs after the apiKey check; reference the apiKey
variable, the headers object, and the provider factory function parameters to
locate and fix the issue.

In `@packages/dodo/src/shared/mapper.ts`:
- Around line 156-164: The returned price object can have currency or amount as
undefined (see id `${product.product_id}_price`, currency computed from
price?.currency ?? product.currency, and amount from price?.price ??
product.price), so add a guard in the mapper (before returning) that validates
both currency and amount are present and non-null/undefined (e.g., normalize
currency to string and amount to number); if either is missing, skip emitting
this price (return null/undefined) or filter it out upstream so you never return
an object with undefined currency/amount. Ensure the mapper's callers handle the
nullable return or the mapper filters invalid products out of result arrays.

In `@packages/dodo/src/shared/sync.ts`:
- Around line 96-97: The event object currently uses the caller-provided id
variable for identity; change it to use the provider's canonical ID by replacing
id with subscription.subscription_id (i.e., set event.id =
subscription.subscription_id) so the event identity comes from
subscription.subscription_id rather than the request input id to avoid drift and
maintain canonical identity.

In `@packages/dodo/src/shared/utils.ts`:
- Around line 165-173: The ensureDodoProductIds function currently only checks
array presence/length and lets blank strings through; update
ensureDodoProductIds to iterate over productIds, trim each id and throw the same
PaymeshError if any id is empty after trimming (reject values like '' or '   '),
and replace/augment the later validation site that forwards product_id (the code
around the cart construction) to either call ensureDodoProductIds or perform the
same trim-and-nonempty check so no blank IDs are forwarded.

In `@packages/dodo/src/shared/webhooks.ts`:
- Around line 56-58: The current resolveDodoWebhookType function silently
coerces unknown types to 'payment.created'; change it to fail fast by checking
DODO_EVENTS[type] and if missing throw a descriptive Error (e.g., `Unknown Dodo
webhook type: ${type}`) instead of returning a default, so callers of
resolveDodoWebhookType will receive an explicit failure for unsupported event
types; update resolveDodoWebhookType (and any tests that expect the old
fallback) accordingly.

In `@packages/dodo/test/customers.test.ts`:
- Around line 110-117: The test is asserting that requests[2]?.body contains
keys with undefined values, but JSON won't preserve undefined; update the
assertion on requests[2]?.body to expect only the actual serialized fields —
e.g., assert that requests[2]?.body equals { name: 'Ana Silva', metadata: {
plan: 'business' } } or use expect.objectContaining(...) and explicitly assert
that 'email' and 'phone_number' are not present (e.g.,
expect(requests[2]?.body).not.toHaveProperty('email')). Modify the assertion
around requests[2]?.body in customers.test.ts accordingly so it does not include
undefined-valued keys.

In `@packages/dodo/test/dodo.test.ts`:
- Around line 192-227: The test uses promise rejection assertions on
provider.payments.create but doesn't await them, so update each assertion that
chains .rejects.toMatchObject (the three provider.payments.create calls) to be
awaited (e.g., await
expect(provider.payments.create(...)).rejects.toMatchObject(...)) or return the
promise from the test; specifically change the three occurrences that start with
expect(provider.payments.create({...})).rejects.toMatchObject(...) to use await
expect(...) to ensure the rejections are actually asserted.

In `@packages/dodo/test/webhooks.test.ts`:
- Around line 74-129: The tests call provider.webhooks?.handle with raw unsigned
Request objects while webhook secret verification is enabled; update the two
webhook requests (the PIX case and the subscription case passed to
provider.webhooks?.handle) to be created via dodoWebhookRequest(...) so they
include valid signature headers (or alternatively add the correct signature
headers manually) before asserting successful handling; specifically replace the
new Request(...) bodies in the pix test and the subscription test with calls to
dodoWebhookRequest(payload, { secret: webhookSecret }) or equivalent so
provider.webhooks?.handle receives signed requests.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2b780a11-a4af-4259-b0f9-f9e66f99b0c5

📥 Commits

Reviewing files that changed from the base of the PR and between a148077 and f1f0749.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (34)
  • .changeset/cyan-poems-boil.md
  • README.md
  • apps/web/.source/browser.ts
  • apps/web/.source/server.ts
  • apps/web/content/docs/basic-usage.mdx
  • apps/web/content/docs/concepts/payment-providers.mdx
  • apps/web/content/docs/concepts/pix.mdx
  • apps/web/content/docs/installation.mdx
  • apps/web/content/docs/introduction.mdx
  • apps/web/content/docs/providers/dodo.mdx
  • apps/web/public/llms.txt
  • apps/web/src/app/page.tsx
  • apps/web/src/components/docs/mdx-components.tsx
  • apps/web/src/lib/docs-navigation.ts
  • apps/web/src/lib/docs.tsx
  • package.json
  • packages/cli/src/shared/providers.ts
  • packages/dodo/README.md
  • packages/dodo/package.json
  • packages/dodo/src/index.ts
  • packages/dodo/src/shared/constants.ts
  • packages/dodo/src/shared/mapper.ts
  • packages/dodo/src/shared/sync.ts
  • packages/dodo/src/shared/utils.ts
  • packages/dodo/src/shared/webhooks.ts
  • packages/dodo/src/types.ts
  • packages/dodo/test/catalog.test.ts
  • packages/dodo/test/customers.test.ts
  • packages/dodo/test/dashboard.test.ts
  • packages/dodo/test/dodo.test.ts
  • packages/dodo/test/webhooks.test.ts
  • packages/dodo/tsconfig.json
  • packages/dodo/tsdown.config.mjs
  • packages/paymesh/README.md

Comment thread apps/web/content/docs/providers/dodo.mdx Outdated
Comment thread apps/web/public/llms.txt
Comment on lines +169 to +187
### Dodo

Package: `@paymesh/dodo`

Implemented capabilities:

- checkout payments (catalog-driven with product IDs)
- customers
- webhooks
- subscriptions
- catalog reads
- dashboard sync helpers for customers, payments, and subscriptions

Important limitations:

- `@paymesh/dodo` currently advertises `pix: false`
- Dodo can expose Pix inside BRL hosted checkout, but not through `paymesh.pix`
- customer delete is intentionally unsupported

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add Dodo to the provider entrypoint links to keep llms.txt internally consistent.

Line 169 onward marks Dodo as live, but the Providers quick-link list later still omits /docs/providers/dodo. This weakens discoverability for systems consuming llms.txt.

💡 Proposed fix
 ### Providers
 
 - [Stripe](/docs/providers/stripe)
 - [Polar](/docs/providers/polar)
 - [AbacatePay](/docs/providers/abacatepay)
+- [Dodo](/docs/providers/dodo)
🧰 Tools
🪛 LanguageTool

[grammar] ~185-~185: Use a hyphen to join words.
Context: ... false- Dodo can expose Pix inside BRL hosted checkout, but not throughpaymes...

(QB_NEW_EN_HYPHEN)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/public/llms.txt` around lines 169 - 187, The Providers quick-link
list in llms.txt is missing the Dodo provider entry; add a link to
`/docs/providers/dodo` in the Providers quick-link section so the newly-live
"Dodo" entry (the block starting "### Dodo" that lists `@paymesh/dodo` features)
is discoverable and the file is internally consistent—update the Providers
quick-link list to include `/docs/providers/dodo` alongside the other provider
paths.

Comment thread apps/web/src/lib/docs.tsx
Comment on lines +3881 to +3884
<DocCodeBlock
code={customerSnippet}
filename="src/server/customers.ts"
/>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use a Dodo-specific customer snippet that does not call customers.delete().

Line 3878 states delete is unsupported, but the snippet at Lines 3881-3884 currently shows a delete call. This creates a copy/paste failure path for users.

💡 Proposed fix
+const dodoCustomersSnippet = `const customer = await client.customers.upsert({
+  email: "billing@example.com",
+  externalId: "order_42",
+  name: "Billing Team",
+  metadata: {
+    orderId: "order_42",
+  },
+});
+
+const fetched = await client.customers.get(customer.id);`;
-						<DocCodeBlock
-							code={customerSnippet}
-							filename="src/server/customers.ts"
-						/>
+						<DocCodeBlock
+							code={dodoCustomersSnippet}
+							filename="src/server/customers.ts"
+						/>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/lib/docs.tsx` around lines 3881 - 3884, The displayed snippet
uses an unsupported delete call; update the customerSnippet used by the
DocCodeBlock (the variable customerSnippet rendered in DocCodeBlock with
filename "src/server/customers.ts") so it does not call customers.delete();
instead replace that line with a supported operation (e.g., show
customers.get/customers.update or just the creation example) or remove the
delete call entirely so the rendered example matches the docs note that delete
is unsupported.

Comment on lines +54 to +65
export const dodo = ({
apiKey = process.env.DODO_PAYMENTS_API_KEY,
webhookSecret = process.env.DODO_PAYMENTS_WEBHOOK_KEY,
baseUrl = process.env.DODO_PAYMENTS_BASE_URL ?? DODO_LIVE_BASE_URL,
sandbox,
retry,
timeout,
fetch,
}: DodoProviderOptions = {}) => {
const resolveProviderSandbox = () =>
typeof sandbox === 'boolean' ? sandbox : baseUrl === DODO_TEST_BASE_URL;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Sandbox mode can still send traffic to live Dodo endpoints.

At Line 57 + Line 63-64, sandbox only affects isSandbox() metadata; it does not switch baseUrl. With sandbox: true and no baseUrl, calls still go to https://live.dodopayments.com.

Suggested fix
 export const dodo = ({
 	apiKey = process.env.DODO_PAYMENTS_API_KEY,
 	webhookSecret = process.env.DODO_PAYMENTS_WEBHOOK_KEY,
-	baseUrl = process.env.DODO_PAYMENTS_BASE_URL ?? DODO_LIVE_BASE_URL,
+	baseUrl = process.env.DODO_PAYMENTS_BASE_URL,
 	sandbox,
 	retry,
 	timeout,
 	fetch,
 }: DodoProviderOptions = {}) => {
-	const resolveProviderSandbox = () =>
-		typeof sandbox === 'boolean' ? sandbox : baseUrl === DODO_TEST_BASE_URL;
+	const resolvedSandbox =
+		typeof sandbox === 'boolean' ? sandbox : baseUrl === DODO_TEST_BASE_URL;
+	const resolvedBaseUrl =
+		baseUrl ?? (resolvedSandbox ? DODO_TEST_BASE_URL : DODO_LIVE_BASE_URL);
+
+	const resolveProviderSandbox = () => resolvedSandbox;

 	const headers = {
 		authorization: `Bearer ${apiKey}`,
 		'content-type': 'application/json',
 	};

 	const baseRequestOptions = {
-		baseUrl,
+		baseUrl: resolvedBaseUrl,
 		fetch,
 		headers,
 		retry,
 		timeout,
 	};
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const dodo = ({
apiKey = process.env.DODO_PAYMENTS_API_KEY,
webhookSecret = process.env.DODO_PAYMENTS_WEBHOOK_KEY,
baseUrl = process.env.DODO_PAYMENTS_BASE_URL ?? DODO_LIVE_BASE_URL,
sandbox,
retry,
timeout,
fetch,
}: DodoProviderOptions = {}) => {
const resolveProviderSandbox = () =>
typeof sandbox === 'boolean' ? sandbox : baseUrl === DODO_TEST_BASE_URL;
export const dodo = ({
apiKey = process.env.DODO_PAYMENTS_API_KEY,
webhookSecret = process.env.DODO_PAYMENTS_WEBHOOK_KEY,
baseUrl = process.env.DODO_PAYMENTS_BASE_URL,
sandbox,
retry,
timeout,
fetch,
}: DodoProviderOptions = {}) => {
const resolvedSandbox =
typeof sandbox === 'boolean' ? sandbox : baseUrl === DODO_TEST_BASE_URL;
const resolvedBaseUrl =
baseUrl ?? (resolvedSandbox ? DODO_TEST_BASE_URL : DODO_LIVE_BASE_URL);
const resolveProviderSandbox = () => resolvedSandbox;
const headers = {
authorization: `Bearer ${apiKey}`,
'content-type': 'application/json',
};
const baseRequestOptions = {
baseUrl: resolvedBaseUrl,
fetch,
headers,
retry,
timeout,
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/dodo/src/index.ts` around lines 54 - 65, The dodo factory currently
leaves baseUrl pointing at DODO_LIVE_BASE_URL when sandbox: true because sandbox
only affects resolveProviderSandbox(); update the initialization so the
effective base URL respects sandbox: change the logic in the dodo function
(symbols: dodo, resolveProviderSandbox) to compute an effectiveBaseUrl (or set
baseUrl) using: if sandbox === true and baseUrl was not explicitly provided, use
DODO_TEST_BASE_URL; otherwise keep the provided baseUrl or existing default;
ensure resolveProviderSandbox continues to return the correct boolean but uses
the same sandbox input and the final network requests/readable metadata use the
effectiveBaseUrl.

Comment on lines +55 to +69
apiKey = process.env.DODO_PAYMENTS_API_KEY,
webhookSecret = process.env.DODO_PAYMENTS_WEBHOOK_KEY,
baseUrl = process.env.DODO_PAYMENTS_BASE_URL ?? DODO_LIVE_BASE_URL,
sandbox,
retry,
timeout,
fetch,
}: DodoProviderOptions = {}) => {
const resolveProviderSandbox = () =>
typeof sandbox === 'boolean' ? sandbox : baseUrl === DODO_TEST_BASE_URL;

const headers = {
authorization: `Bearer ${apiKey}`,
'content-type': 'application/json',
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing API key validation yields Authorization: Bearer undefined.

At Line 67, headers are built even when apiKey is unset. This silently sends malformed auth and fails later with less actionable errors.

Suggested fix
 }: DodoProviderOptions = {}) => {
+	if (!apiKey) {
+		throw new PaymeshError({
+			code: 'invalid_request',
+			message:
+				'Provider "dodo" requires an API key. Set "apiKey" or DODO_PAYMENTS_API_KEY.',
+			provider: 'dodo',
+		});
+	}
+
 	const resolveProviderSandbox = () =>
 		typeof sandbox === 'boolean' ? sandbox : baseUrl === DODO_TEST_BASE_URL;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
apiKey = process.env.DODO_PAYMENTS_API_KEY,
webhookSecret = process.env.DODO_PAYMENTS_WEBHOOK_KEY,
baseUrl = process.env.DODO_PAYMENTS_BASE_URL ?? DODO_LIVE_BASE_URL,
sandbox,
retry,
timeout,
fetch,
}: DodoProviderOptions = {}) => {
const resolveProviderSandbox = () =>
typeof sandbox === 'boolean' ? sandbox : baseUrl === DODO_TEST_BASE_URL;
const headers = {
authorization: `Bearer ${apiKey}`,
'content-type': 'application/json',
};
apiKey = process.env.DODO_PAYMENTS_API_KEY,
webhookSecret = process.env.DODO_PAYMENTS_WEBHOOK_KEY,
baseUrl = process.env.DODO_PAYMENTS_BASE_URL ?? DODO_LIVE_BASE_URL,
sandbox,
retry,
timeout,
fetch,
}: DodoProviderOptions = {}) => {
if (!apiKey) {
throw new PaymeshError({
code: 'invalid_request',
message:
'Provider "dodo" requires an API key. Set "apiKey" or DODO_PAYMENTS_API_KEY.',
provider: 'dodo',
});
}
const resolveProviderSandbox = () =>
typeof sandbox === 'boolean' ? sandbox : baseUrl === DODO_TEST_BASE_URL;
const headers = {
authorization: `Bearer ${apiKey}`,
'content-type': 'application/json',
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/dodo/src/index.ts` around lines 55 - 69, The code builds the
Authorization header even when apiKey is undefined, causing "Bearer undefined"
to be sent; add a validation at the start of the provider factory (where apiKey
is read from DodoProviderOptions) to throw a clear error when apiKey is missing
or empty, and move/guard the headers construction so it only occurs after the
apiKey check; reference the apiKey variable, the headers object, and the
provider factory function parameters to locate and fix the issue.

Comment on lines +165 to +173
export function ensureDodoProductIds(productIds?: string[]) {
if (!productIds || productIds.length === 0) {
throw new PaymeshError({
code: 'invalid_request',
message:
'Provider "dodo" requires at least one product id in "productIds"',
provider: 'dodo',
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Reject empty/whitespace productIds values.

Line 166 only validates array presence/length; [''] and [' '] still pass and then Line 192 forwards invalid product_id values. Tighten validation to reject blank IDs before cart construction.

Suggested fix
 export function ensureDodoProductIds(productIds?: string[]) {
-	if (!productIds || productIds.length === 0) {
+	if (
+		!productIds ||
+		productIds.length === 0 ||
+		productIds.some((id) => id.trim().length === 0)
+	) {
 		throw new PaymeshError({
 			code: 'invalid_request',
 			message:
 				'Provider "dodo" requires at least one product id in "productIds"',
 			provider: 'dodo',
 		});
 	}
 }

Also applies to: 191-193

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/dodo/src/shared/utils.ts` around lines 165 - 173, The
ensureDodoProductIds function currently only checks array presence/length and
lets blank strings through; update ensureDodoProductIds to iterate over
productIds, trim each id and throw the same PaymeshError if any id is empty
after trimming (reject values like '' or '   '), and replace/augment the later
validation site that forwards product_id (the code around the cart construction)
to either call ensureDodoProductIds or perform the same trim-and-nonempty check
so no blank IDs are forwarded.

Comment on lines +56 to +58
export function resolveDodoWebhookType(type: string): PaymeshEventType {
return DODO_EVENTS[type] ?? 'payment.created';
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Unknown Dodo webhook types are silently coerced to payment.created.

At Line 57, the fallback masks unsupported event types and can trigger incorrect downstream handlers/events instead of failing fast.

Suggested fix
+import { PaymeshError } from 'paymesh';
 import type { PaymeshEventType } from 'paymesh';
 ...
 export function resolveDodoWebhookType(type: string): PaymeshEventType {
-	return DODO_EVENTS[type] ?? 'payment.created';
+	const resolved = DODO_EVENTS[type];
+	if (!resolved) {
+		throw new PaymeshError({
+			code: 'webhook_parse_error',
+			message: `Unsupported Dodo webhook event type: "${type}"`,
+			provider: 'dodo',
+		});
+	}
+	return resolved;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function resolveDodoWebhookType(type: string): PaymeshEventType {
return DODO_EVENTS[type] ?? 'payment.created';
}
export function resolveDodoWebhookType(type: string): PaymeshEventType {
const resolved = DODO_EVENTS[type];
if (!resolved) {
throw new PaymeshError({
code: 'webhook_parse_error',
message: `Unsupported Dodo webhook event type: "${type}"`,
provider: 'dodo',
});
}
return resolved;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/dodo/src/shared/webhooks.ts` around lines 56 - 58, The current
resolveDodoWebhookType function silently coerces unknown types to
'payment.created'; change it to fail fast by checking DODO_EVENTS[type] and if
missing throw a descriptive Error (e.g., `Unknown Dodo webhook type: ${type}`)
instead of returning a default, so callers of resolveDodoWebhookType will
receive an explicit failure for unsupported event types; update
resolveDodoWebhookType (and any tests that expect the old fallback) accordingly.

Comment on lines +110 to +117
expect(requests[2]?.body).toEqual({
email: undefined,
name: 'Ana Silva',
phone_number: undefined,
metadata: {
plan: 'business',
},
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

PATCH request body expectation includes undefined fields that JSON cannot preserve.

At Line 110-117, parsed JSON body should not contain email or phone_number keys when values are undefined; this assertion can fail despite correct runtime behavior.

Suggested fix
 		expect(requests[2]?.body).toEqual({
-			email: undefined,
 			name: 'Ana Silva',
-			phone_number: undefined,
 			metadata: {
 				plan: 'business',
 			},
 		});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/dodo/test/customers.test.ts` around lines 110 - 117, The test is
asserting that requests[2]?.body contains keys with undefined values, but JSON
won't preserve undefined; update the assertion on requests[2]?.body to expect
only the actual serialized fields — e.g., assert that requests[2]?.body equals {
name: 'Ana Silva', metadata: { plan: 'business' } } or use
expect.objectContaining(...) and explicitly assert that 'email' and
'phone_number' are not present (e.g.,
expect(requests[2]?.body).not.toHaveProperty('email')). Modify the assertion
around requests[2]?.body in customers.test.ts accordingly so it does not include
undefined-valued keys.

Comment on lines +192 to +227
expect(
provider.payments.create({
customer: {
email: 'ana@example.com',
},
}),
).rejects.toMatchObject({
code: 'invalid_request',
message:
'Provider "dodo" requires at least one product id in "productIds"',
});

expect(
provider.payments.create({
productIds: ['prod_1', 'prod_2'],
amount: 1000,
customer: {
email: 'ana@example.com',
},
}),
).rejects.toMatchObject({
code: 'invalid_request',
message:
'Provider "dodo" only accepts "amount" when exactly one product id is provided.',
});

expect(
provider.payments.create({
productIds: ['prod_123'],
}),
).rejects.toMatchObject({
code: 'invalid_request',
message:
'Provider "dodo" requires either "customer.id" or "customer.email" when creating payments.',
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Rejected-promise assertions are not awaited.

At Line 192, Line 204, and Line 218, .rejects chains must be awaited (or returned) or the test can pass without actually asserting rejection.

Suggested fix
-		expect(
+		await expect(
 			provider.payments.create({
 				customer: {
 					email: 'ana@example.com',
 				},
 			}),
 		).rejects.toMatchObject({
 			code: 'invalid_request',
 			message:
 				'Provider "dodo" requires at least one product id in "productIds"',
 		});

-		expect(
+		await expect(
 			provider.payments.create({
 				productIds: ['prod_1', 'prod_2'],
 				amount: 1000,
 				customer: {
 					email: 'ana@example.com',
 				},
 			}),
 		).rejects.toMatchObject({
 			code: 'invalid_request',
 			message:
 				'Provider "dodo" only accepts "amount" when exactly one product id is provided.',
 		});

-		expect(
+		await expect(
 			provider.payments.create({
 				productIds: ['prod_123'],
 			}),
 		).rejects.toMatchObject({
 			code: 'invalid_request',
 			message:
 				'Provider "dodo" requires either "customer.id" or "customer.email" when creating payments.',
 		});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/dodo/test/dodo.test.ts` around lines 192 - 227, The test uses
promise rejection assertions on provider.payments.create but doesn't await them,
so update each assertion that chains .rejects.toMatchObject (the three
provider.payments.create calls) to be awaited (e.g., await
expect(provider.payments.create(...)).rejects.toMatchObject(...)) or return the
promise from the test; specifically change the three occurrences that start with
expect(provider.payments.create({...})).rejects.toMatchObject(...) to use await
expect(...) to ensure the rejections are actually asserted.

Comment on lines +74 to +129
const pixHandled = await provider.webhooks?.handle({
request: new Request('https://app.test/webhooks', {
method: 'POST',
headers: {
'content-type': 'application/json',
'webhook-id': 'msg_pix',
},
body: JSON.stringify({
business_id: 'biz_123',
type: 'payment.processing',
timestamp: new Date().toISOString(),
data: {
payment_id: 'pay_pix_123',
total_amount: 3100,
currency: 'BRL',
customer: {
customer_id: 'cus_123',
email: 'ana@example.com',
name: 'Ana',
},
metadata: {},
payment_link: 'https://pay.dodo.test/pay_pix_123',
payment_method_type: 'pix',
status: 'processing',
},
}),
}),
});
const subscriptionHandled = await provider.webhooks?.handle({
request: new Request('https://app.test/webhooks', {
method: 'POST',
headers: {
'content-type': 'application/json',
'webhook-id': 'msg_sub',
},
body: JSON.stringify({
business_id: 'biz_123',
type: 'subscription.cancelled',
timestamp: new Date().toISOString(),
data: {
subscription_id: 'sub_123',
product_id: 'prod_123',
recurring_pre_tax_amount: 3900,
currency: 'USD',
status: 'cancelled',
cancel_at_next_billing_date: true,
customer: {
customer_id: 'cus_123',
email: 'ana@example.com',
name: 'Ana',
},
metadata: {},
},
}),
}),
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid testing successful webhook handling with unsigned requests when webhookSecret is configured.

Line 74 and Line 102 currently assert successful handling for unsigned requests despite secret-based verification being enabled. This weakens security guarantees in tests and can mask signature-enforcement regressions. Build these requests with dodoWebhookRequest(...) (or include valid signature headers) before expecting successful handle.

Suggested direction
-		const pixHandled = await provider.webhooks?.handle({
-			request: new Request('https://app.test/webhooks', {
-				method: 'POST',
-				headers: {
-					'content-type': 'application/json',
-					'webhook-id': 'msg_pix',
-				},
-				body: JSON.stringify({ ... }),
-			}),
-		});
+		const pixHandled = await provider.webhooks?.handle({
+			request: dodoWebhookRequest(
+				{ ...same payload... },
+				'whsec_c2VjcmV0X2Rlc2lnbg==',
+			),
+		});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/dodo/test/webhooks.test.ts` around lines 74 - 129, The tests call
provider.webhooks?.handle with raw unsigned Request objects while webhook secret
verification is enabled; update the two webhook requests (the PIX case and the
subscription case passed to provider.webhooks?.handle) to be created via
dodoWebhookRequest(...) so they include valid signature headers (or
alternatively add the correct signature headers manually) before asserting
successful handling; specifically replace the new Request(...) bodies in the pix
test and the subscription test with calls to dodoWebhookRequest(payload, {
secret: webhookSecret }) or equivalent so provider.webhooks?.handle receives
signed requests.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@coderabbitai coderabbitai Bot added the documentation Improvements or additions to documentation label Jun 15, 2026
@almeidazs almeidazs closed this Jun 15, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant