Repository navigation
feat: Add Coupons Migration to Lemon Squeezy Provider - #7
Conversation
|
@CodeRabbit review |
✅ Actions performedReview triggered.
|
|
Caution Review failedThe pull request is closed. WalkthroughThe changes implement on-demand coupon migration from Lemon Squeezy to Dodo Payments, adding interactive checkbox selection for migration types (products/coupons) and refactoring payload construction to use runtime currency resolution per store instead of pre-defined pipelines. Product migration is also restructured to align with the new on-demand pattern. Changes
Sequence DiagramsequenceDiagram
participant User
participant CLI as CLI/Main
participant Selector as Type Selector
participant Store as Store Fetch
participant ProdMgr as Product Migrator
participant CouponMgr as Coupon Migrator
participant DodoAPI as Dodo Payments
participant Report as Reporting
User->>CLI: Invoke with --types
CLI->>Selector: Interactive checkbox prompt
Selector-->>CLI: Selected types (products, coupons)
rect rgb(220, 240, 255)
Note over CLI: Setup Phase
CLI->>Store: Fetch store & cache currency
Store-->>CLI: Store data + currency
end
alt Products selected
rect rgb(240, 255, 240)
Note over CLI: Product Migration
CLI->>ProdMgr: Fetch products at runtime
ProdMgr->>ProdMgr: Resolve currency per store
ProdMgr->>ProdMgr: Build Dodo Payments payloads
ProdMgr-->>CLI: Product payloads
CLI->>CLI: Confirm with user
CLI->>DodoAPI: Migrate products
DodoAPI-->>Report: Success/failure per product
end
end
alt Coupons selected
rect rgb(255, 245, 230)
Note over CLI: Coupon Migration
CLI->>CouponMgr: Fetch discounts
CouponMgr->>CouponMgr: Filter published, percentage-based
CouponMgr->>CouponMgr: Resolve currency, build payloads
CouponMgr-->>CLI: Coupon payloads
CLI->>CLI: Confirm with user
CLI->>DodoAPI: Migrate coupons
DodoAPI-->>Report: Success/failure per coupon
end
end
CLI->>Report: Consolidate results by category
Report-->>User: Final summary (migrated categories)
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~45 minutes The changes introduce substantial new migration pathways (coupon support), add interactive user flows with new dependencies ( Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (6)
docs/stripe/README.md (1)
3-6: Fix Markdown heading and fenced block formatting
markdownlintis flagging the#### Usage:heading jump and the unlabeled code fence right below it. Bumping the heading to##and tagging the snippet asbashresolves both issues without changing meaning.-#### Usage: -``` -dodo-migrate stripe -``` +## Usage +```bash +dodo-migrate stripe +```README.md (1)
57-66: Add fenced code languages to satisfy markdownlint (MD040).Mark the CLI snippets as console/bash for readability and lint compliance.
Apply:
-``` +```console dodo-migrate lemonsqueezy-
+console
dodo-migrate stripe... -``` +```bash dodo-migrate lemonsqueezy \ --provider-api-key=lsq_XXXXXXXXXXXXXXXX \ --dodo-api-key=dp_XXXXXXXXXXXXXXXX \ --mode=test_mode \ --dodo-brand-id=brand_XXXXXX-
+bash
dodo-migrate stripe
--provider-api-key=sk_test_XXXXXXXXXXXXXXXX
--dodo-api-key=dp_XXXXXXXXXXXXXXXX
--mode=test_mode
--dodo-brand-id=brand_XXXXXX
--migrate-types=products,couponsAlso applies to: 100-104, 106-119
src/providers/stripe/index.ts (4)
169-171: Subscription cadence mapping is incomplete.Interval_count (e.g., every 3 months) and weekly/daily intervals are not handled and default to monthly, potentially wrong in target.
Would you like me to add an interval mapper (handles day/week/month/year + interval_count) and confirm what Dodo accepts for billing_period? Based on learnings.
209-215: Rename local variable to avoid shadowing the function name.Improves readability.
Apply:
- const migrateProducts = await select({ + const proceedProducts = await select({ message: 'Proceed to create these products in Dodo Payments?', choices: [ { name: 'Yes', value: 'yes' }, { name: 'No', value: 'no' } ], }); - if (migrateProducts === 'yes') { + if (proceedProducts === 'yes') {Also applies to: 217-230
299-305: Rename local variable to avoid shadowing the function name.Apply:
- const migrateCoupons = await select({ + const proceedCoupons = await select({ message: 'Proceed to create these coupons in Dodo Payments?', choices: [ { name: 'Yes', value: 'yes' }, { name: 'No', value: 'no' } ], }); - if (migrateCoupons === 'yes') { + if (proceedCoupons === 'yes') {Also applies to: 307-321
380-386: Rename local variable to avoid shadowing the function name.Apply:
- const migrateCustomers = await select({ + const proceedCustomers = await select({ message: 'Proceed to create these customers in Dodo Payments?', choices: [ { name: 'Yes', value: 'yes' }, { name: 'No', value: 'no' } ], }); - if (migrateCustomers === 'yes') { + if (proceedCustomers === 'yes') {Also applies to: 388-402
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (7)
README.md(3 hunks)docs/lemonsqueezy/README.md(2 hunks)docs/stripe/README.md(1 hunks)package.json(1 hunks)src/index.ts(2 hunks)src/providers/lemonsqueezy/index.ts(4 hunks)src/providers/stripe/index.ts(1 hunks)
🧰 Additional context used
🪛 markdownlint-cli2 (0.18.1)
docs/stripe/README.md
3-3: Heading levels should only increment by one level at a time
Expected: h2; Actual: h4
(MD001, heading-increment)
4-4: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
14-14: Tables should be surrounded by blank lines
(MD058, blanks-around-tables)
15-15: Table pipe style
Expected: leading_only; Actual: no_leading_or_trailing; Missing leading pipe
(MD055, table-pipe-style)
94-94: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
98-98: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
102-102: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
107-107: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
README.md
63-63: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
96-96: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
101-101: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
106-106: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🔇 Additional comments (3)
docs/lemonsqueezy/README.md (1)
12-24: Clear, actionable docs for --types (LGTM).Examples and defaults are unambiguous and match the described behavior.
src/index.ts (1)
6-7: Approve Stripe provider registration
Dependencies (stripe,@inquirer/prompts,dodopayments) are listed in package.json anddocs/stripe/README.mdanddocs/lemonsqueezy/README.mdexist.src/providers/stripe/index.ts (1)
56-58: Retain explicit API version
‘2025-02-24.acacia’ is a valid Stripe API version string for stripe-node v17; pinning it ensures consistent API behavior across environments.Likely an incorrect or invalid review comment.
|
@CodeRabbit review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/providers/lemonsqueezy/index.ts(4 hunks)src/providers/stripe/index.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/providers/stripe/index.ts
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/providers/stripe/index.ts (3)
3-3: Mask API keys; use password prompts instead of input.Secrets are echoed to the terminal. Switch to password prompts and add validation.
-import { input, select, checkbox } from '@inquirer/prompts'; +import { input, select, checkbox, password } from '@inquirer/prompts'; -const PROVIDER_API_KEY = argv['provider-api-key'] || await input({ - message: 'Enter your Stripe Secret API Key (sk_...):', - required: true -}); +const PROVIDER_API_KEY = argv['provider-api-key'] || await password({ + message: 'Enter your Stripe Secret API Key (sk_...):', + mask: true, + validate: (v) => v?.trim().startsWith('sk_') ? true : 'Please provide a valid Stripe secret key' +}); -const DODO_API_KEY = argv['dodo-api-key'] || await input({ - message: 'Enter your Dodo Payments API Key:', - required: true -}); +const DODO_API_KEY = argv['dodo-api-key'] || await password({ + message: 'Enter your Dodo Payments API Key:', + mask: true, + validate: (v) => v?.trim() ? true : 'API key is required' +});Also consider replacing
required: truewith a validate function across prompts. [Based on learnings]Also applies to: 39-47
168-186: Handle weekly/daily Stripe intervals or skip unsupported ones.Currently defaults to monthly for non-year/month intervals; this mislabels weekly/daily plans.
- price: { - currency: price.currency.toUpperCase(), - price: price.unit_amount as number, - discount: 0, - purchasing_power_parity: false, - type: 'recurring_price', - billing_period: price.recurring?.interval === 'month' ? 'monthly' : - price.recurring?.interval === 'year' ? 'yearly' : 'monthly' - }, + // Map Stripe interval -> Dodo billing_period; skip unsupported. + const interval = price.recurring?.interval; + const billingMap: Record<string, 'daily' | 'weekly' | 'monthly' | 'yearly'> = { + day: 'daily', + week: 'weekly', + month: 'monthly', + year: 'yearly', + }; + const billingPeriod = interval ? billingMap[interval] : undefined; + if (!billingPeriod) { + console.log(`[LOG] Skipping price ${price.id} - unsupported interval: ${interval}`); + continue; + } + price: { + currency: price.currency.toUpperCase(), + price: price.unit_amount as number, + discount: 0, + purchasing_power_parity: false, + type: 'recurring_price', + billing_period: billingPeriod + },If Dodo only accepts monthly/yearly, log and skip day/week instead.
56-58: I'll verify the Stripe API version validity by searching for current Stripe documentation.Let me search for specific information about the Node.js Stripe SDK's apiVersion parameter format.
Let me verify if
2025-02-24was an actual Stripe API version release.Based on my verification, I can now provide the corrected review comment.
Remove the
.acaciasuffix from the apiVersion string; Node.js SDK expects YYYY-MM-DD format.For the Node.js Stripe SDK, the apiVersion parameter should use the format
YYYY-MM-DD(e.g.,2025-02-24), not2025-02-24.acacia. While2025-02-24.acaciais a valid API version supported by webhook endpoints, the.codenamesuffix was historically used for Stripe.js preview features but is no longer supported.The current Stripe API version is 2025-09-30.clover. If you want to pin to a specific stable version, use
2025-02-24alone. Otherwise, omitapiVersionto use your account's default.const stripe = new Stripe(PROVIDER_API_KEY, { - apiVersion: '2025-02-24.acacia', + apiVersion: '2025-02-24', });
♻️ Duplicate comments (2)
src/providers/stripe/index.ts (2)
276-281: Promotion codes: add active filter and paginate; otherwise you’ll miss >100 and include disabled codes.Use
active: trueandautoPagingEach(orautoPagingToArray) to iterate all promotion codes.-// Fetch promotion codes for this coupon -const promotionCodes = await stripe.promotionCodes.list({ - coupon: coupon.id, - limit: 100 -}); +// Fetch all active promotion codes for this coupon +const promoCodes: Stripe.PromotionCode[] = []; +await stripe.promotionCodes + .list({ coupon: coupon.id, active: true, limit: 100 }) + .autoPagingEach((pc) => { promoCodes.push(pc); });And replace subsequent uses of
promotionCodes.datawithpromoCodes.
312-341: Use promotion code–specific fields (expires_at, max_redemptions, name) when mapping; don’t reuse coupon-level values.For each promotion code: prefer
promotionCode.expires_atandpromotionCode.max_redemptions, and default name tocoupon.name || promotionCode.code.- for (const promotionCode of promotionCodes.data) { + for (const promotionCode of promoCodes) { // ... discountType/discountValue derived from coupon (unchanged) - const currency = coupon.amount_off ? coupon.currency?.toUpperCase() : undefined; + const currency = coupon.amount_off ? coupon.currency?.toUpperCase() : undefined; CouponsToMigrate.push({ - code: promotionCode.code, - name: coupon.name || coupon.id, + code: promotionCode.code, + name: coupon.name || promotionCode.code, discount_type: discountType, amount: discountValue, currency: currency, - usage_limit: coupon.max_redemptions || null, - expires_at: coupon.redeem_by ? new Date(coupon.redeem_by * 1000).toISOString() : null, + usage_limit: promotionCode.max_redemptions ?? null, + expires_at: promotionCode.expires_at + ? new Date(promotionCode.expires_at * 1000).toISOString() + : null, brand_id: brand_id }); }
🧹 Nitpick comments (5)
src/providers/lemonsqueezy/index.ts (4)
35-39: Add validation for the--typesoption.The
--typesoption currently accepts any comma-separated string without validation. If a user provides invalid types (e.g.,--types=foo,bar), the script will run but migrate nothing, which may confuse users.Consider validating the types after parsing in the handler:
// After line 67, add validation if (migrationTypes.length === 0) { console.log('[ERROR] No migration types selected. Please select at least one type.'); process.exit(1); } const validTypes = ['products', 'coupons']; const invalidTypes = migrationTypes.filter(type => !validTypes.includes(type)); if (invalidTypes.length > 0) { console.log(`[ERROR] Invalid migration types: ${invalidTypes.join(', ')}. Valid types are: ${validTypes.join(', ')}`); process.exit(1); }
296-308: Enhance error handling for failed coupon migrations.The current error handling stringifies the entire error object, which may not provide clear information to users. If a coupon fails due to validation errors (e.g., missing currency, duplicate code, invalid format), users would benefit from more specific error messages.
Consider parsing and displaying structured error information:
} catch (error) { - console.log(`[ERROR] Failed to migrate coupon: ${coupon.data.name} - ${error}`); + console.log(`[ERROR] Failed to migrate coupon: ${coupon.data.name}`); + if (error && typeof error === 'object' && 'message' in error) { + console.log(`[ERROR] Reason: ${error.message}`); + } else { + console.log(`[ERROR] Reason: ${error}`); + } failureCount++; }
166-166: Address type assertions for currency fields.Multiple
as anytype assertions are used when assigning currency values from Lemon Squeezy to Dodo Payments (Lines 166, 238, 252). These assertions bypass TypeScript's type safety and may hide actual type incompatibilities between the two APIs.Investigate the actual types returned by the Lemon Squeezy SDK and expected by Dodo Payments. Consider:
- Creating a type mapping function if the currency formats differ
- Adding runtime validation to ensure currency values are valid
- Using proper type assertions with validation rather than
as anyExample approach:
type LemonSqueezyCurrency = string; // actual type from SDK type DodoPaymentsCurrency = string; // actual type from Dodo API function mapCurrency(lsCurrency: LemonSqueezyurrency | undefined): DodoPaymentsCurrency | undefined { // Add validation/mapping logic if needed return lsCurrency; } // Usage currency: mapCurrency(StoreData.data.attributes.currency)Also applies to: 238-238, 252-252
236-254: Remove the unnecessary initial check fordiscount.attributes.currency— the Lemon Squeezy API does not return a currency field on discount objects.The Lemon Squeezy Discount object does not include a currency field. The code at line 238 using
(discount as any).attributes.currencywill always be undefined, making the initial check dead code. Fixed amounts are stored as cents and currency is implied by the store/order, so the store data fallback (lines 240–253) will always be needed.Simplify by removing the unnecessary type assertion and initial check:
let fixedCurrency: string | undefined = undefined; if (discount.attributes.amount_type === 'fixed') { - fixedCurrency = (discount as any).attributes.currency; - if (!fixedCurrency) { // Fallback: use cached store currency or fetch and cache the store if not present if (!StoresData[discount.attributes.store_id]) { // ... fetch store } fixedCurrency = StoresData[discount.attributes.store_id]?.data.attributes.currency as any; - } }src/providers/stripe/index.ts (1)
161-167: unit_amount guard is good; drop fallback “|| 0” to avoid masking issues.You already skip null amounts; prefer using the non-null value directly to prevent accidental zeros.
- price: price.unit_amount || 0, + price: price.unit_amount as number,Apply to both one-time and recurring blocks.
Also applies to: 176-177, 196-197
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/providers/lemonsqueezy/index.ts(4 hunks)src/providers/stripe/index.ts(6 hunks)
🔇 Additional comments (9)
src/providers/lemonsqueezy/index.ts (6)
112-120: Good addition of completion tracking flags.The completion tracking flags (
completedProductsandcompletedCoupons) properly address the previous review concern about misleading completion summaries. The final message will now accurately reflect what was actually migrated.
122-211: Products migration implementation looks solid.The products migration flow is well-structured with proper error handling, store data caching to prevent rate limiting, detailed logging, and user confirmation before execution. The completion flag is set only after successful migration.
255-265: Amount handling is correct.The code correctly passes the raw
amountvalue for both percentage and fixed-amount coupons without dividing by 100, which aligns with Dodo Payments API expectations (minor-unit values). The division by 100 on Line 275 is only for display purposes, which is appropriate.
290-318: Completion tracking logic is strict but appropriate.The code only sets
completedCoupons = truewhen all coupons migrate successfully (failureCount === 0). This strict approach ensures that the final completion message accurately reflects a fully successful migration, while partial successes are clearly reported separately. This design is appropriate for a migration tool where users need to know if any items failed.
327-334: Accurate completion message implementation.The final completion message correctly uses the
completedProductsandcompletedCouponsflags to report only what was actually migrated, addressing the previous review concern about misleading summaries.
59-66: ****The
requiredproperty is a valid and supported option in @inquirer/checkbox that ensures at least one choice must be selected. The code correctly prevents empty selections withrequired: true. No validation code is needed.Likely an incorrect or invalid review comment.
src/providers/stripe/index.ts (3)
128-135: Nice: full pagination for products and prices.Using
autoPagingEachto collect all products and per-product prices addresses missed pages.Also applies to: 146-155
252-259: Good: coupon pagination implemented.Collecting all coupons avoids truncation at 100.
404-410: Customers: pagination and address cleanup look solid.Good handling of deleted customers and omission of empty address objects.
Also applies to: 427-457
|
Please only accept discounts that are percentage only. Ignore discounts that are not percentage based. This is because the Dodo Payments SDK only supports creating percentage discounts at the moment. |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
src/providers/lemonsqueezy/index.ts (3)
1-1: Remove unused import.
getProductis imported but never referenced in the code.Apply this diff:
-import { listProducts, lemonSqueezySetup, getProduct, getStore, Store, listDiscounts } from '@lemonsqueezy/lemonsqueezy.js'; +import { listProducts, lemonSqueezySetup, getStore, Store, listDiscounts } from '@lemonsqueezy/lemonsqueezy.js';
235-254: Currency resolution logic is solid.The fallback to store currency when
discount.attributes.currencyis unavailable ensures fixed-amount coupons always have currency defined. The caching prevents redundant API calls.Consider removing the type assertion on Line 238 for better type safety:
- fixedCurrency = (discount as any).attributes.currency; + fixedCurrency = discount.attributes.currency;If TypeScript complains, verify the
@lemonsqueezy/lemonsqueezy.jstypes includecurrencyon discount attributes for fixed amounts, or add a proper type guard.
259-260: Clarify the normalization comment.The comment implies all amounts are multiplied by 100, but only percentage discounts are transformed (to basis points for Dodo). Fixed amounts remain in minor units.
Apply this diff:
- // * 100 to normalize it to Dodo Payments expected format - amount: discount.attributes.amount_type === 'percent' ? discount.attributes.amount * 100 : discount.attributes.amount, + // For percentage: convert to basis points (e.g., 10% → 1000). For fixed: keep minor units as-is. + amount: discount.attributes.amount_type === 'percent' ? discount.attributes.amount * 100 : discount.attributes.amount,
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/providers/lemonsqueezy/index.ts(5 hunks)
🔇 Additional comments (4)
src/providers/lemonsqueezy/index.ts (4)
54-67: LGTM!The migration type selection logic elegantly handles both CLI and interactive modes with sensible defaults.
293-324: Excellent error handling and reporting.The per-coupon try-catch ensures one failure doesn't abort the entire migration, and the success/failure tallies provide clear feedback. Setting
completedCouponsonly when all succeed is the right choice for the final summary.
330-337: LGTM!The completion summary now correctly reflects actual migration outcomes by checking the boolean flags set during each successful migration branch.
126-130: Based on my research, I found that the Lemon Squeezy API uses page-based pagination for all "list" endpoints with a default page size of 10 items.While the official Lemon Squeezy JavaScript SDK (@lemonsqueezy/lemonsqueezy.js) documentation does not clearly specify whether
listProducts()andlistDiscounts()auto-paginate or only return the first page, the SDK examples and tutorials consistently show single function calls without pagination loops. The underlying API architecture requires manual pagination handling—developers must check forlinks.nextto fetch additional pages.Your original review comment is valid. The code at lines 126-130 calls
listProducts()without any pagination parameters, which means if the account has more than 10 products (the default page size), only the first page will be migrated. The same concern applies tolistDiscounts()at line 218.Add pagination handling to fetch all products and discounts.
The Lemon Squeezy API uses page-based pagination for all "list" endpoints. Implement pagination loops using the response's
links.nextor manually specifypage[number]andpage[size]parameters to ensure all products and discounts are migrated, not just the first page.
134e999 to
1252e62
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
src/providers/lemonsqueezy/index.ts (1)
1-1: Remove unusedgetProductimport.The
getProductfunction is imported but never used in the code.Apply this diff:
-import { listProducts, lemonSqueezySetup, getProduct, getStore, Store, listDiscounts } from '@lemonsqueezy/lemonsqueezy.js'; +import { listProducts, lemonSqueezySetup, getStore, Store, listDiscounts } from '@lemonsqueezy/lemonsqueezy.js';
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
docs/lemonsqueezy/README.md(1 hunks)src/providers/lemonsqueezy/index.ts(5 hunks)
✅ Files skipped from review due to trivial changes (1)
- docs/lemonsqueezy/README.md
🔇 Additional comments (3)
src/providers/lemonsqueezy/index.ts (3)
35-39: LGTM! Well-designed CLI option and interactive flow.The
--typesoption with fallback to interactive checkbox selection provides good UX flexibility. The defaults (both checked) and required validation are sensible.Also applies to: 54-67
122-211: LGTM! Products migration flow is well-structured.The on-demand fetching with store caching, clear preview formatting, user confirmation, and per-item logging provide a solid migration experience. Price handling correctly preserves minor units.
236-236: LGTM! Percentage-only filtering correctly implements requirements.The filter to process only
amount_type === 'percent'discounts and skip fixed-amount discounts with a clear warning aligns with the stakeholder requirement (issue #4 discussion) that Dodo Payments SDK currently supports percentage discounts only.Also applies to: 249-251
|
Thank you for your contribution! |


fixes #4
Added Lemon Squeezy migration support for products, coupons, and customers
Implemented interactive CLI with progress tracking
Added comprehensive documentation in /docs/lemonsqueezy
Updated README with Lemon Squeezy support status
Summary by CodeRabbit
New Features
--typesCLI option to specify migration scopeDocumentation