@dodopayments/opencode-plugin 0.3.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,464 @@
1
+ ---
2
+ name: discounts-and-promotions
3
+ description: Guide for implementing discount codes and promotional pricing with Dodo Payments, including CRUD operations, eligibility rules, stacking, subscription-cycle limits, and plan-change preservation.
4
+ ---
5
+
6
+ # Discounts and Promotions
7
+
8
+ This skill covers discount codes, coupons, and promotional pricing in Dodo Payments. Use it when building discount management, applying codes at checkout, validating codes before showing prices, or handling discounts during subscription plan changes.
9
+
10
+ ## When to use this skill
11
+
12
+ - Create, update, list, or delete discount codes
13
+ - Apply discount codes at checkout or during plan changes
14
+ - Validate a code before displaying adjusted pricing to the user
15
+ - Configure eligibility rules, usage limits, or subscription-cycle restrictions
16
+ - Handle the subtle semantics of discount preservation during plan upgrades/downgrades
17
+ - Debug why a discount isn't applying or stacking as expected
18
+
19
+ ## Core concepts
20
+
21
+ **Discount types:** Dodo supports both percentage and flat-amount discounts. Percentage amounts are expressed as integers where 1500 means 15% (100 = 1%). Flat amounts are in the smallest currency unit (cents for USD).
22
+
23
+ **Discount code:** A human-readable string (e.g., `SUMMER2025`) that customers enter at checkout. Codes are case-sensitive.
24
+
25
+ **Eligibility:** Discounts can be restricted to specific products, customers, or date ranges. A discount without restrictions applies to any product and any customer.
26
+
27
+ **Stacking:** Multiple discount codes can be applied to a single checkout or subscription. They are applied in the order specified in the `discount_codes` array, up to a maximum of 20 codes.
28
+
29
+ **Subscription-cycle limits:** A discount can be configured to apply only for a specific number of billing cycles (e.g., first 3 months only).
30
+
31
+ **Preservation on plan change:** When a customer upgrades or downgrades their subscription, the `preserve_on_plan_change` setting and the `discount_codes` parameter control whether existing discounts carry forward.
32
+
33
+ ## Discount CRUD
34
+
35
+ ### Create a discount
36
+
37
+ ```typescript
38
+ import DodoPayments from 'dodopayments';
39
+
40
+ const client = new DodoPayments({
41
+ bearerToken: process.env.DODO_PAYMENTS_API_KEY,
42
+ environment: 'test_mode',
43
+ });
44
+
45
+ // Percentage discount: 15% off
46
+ const percentageDiscount = await client.discounts.create({
47
+ type: 'percentage',
48
+ amount: 1500, // 15%
49
+ code: 'SUMMER2025',
50
+ metadata: {
51
+ campaign: 'summer_promo',
52
+ source: 'email_blast'
53
+ }
54
+ });
55
+
56
+ // Flat discount: $10 off (1000 cents in currency_options)
57
+ const flatDiscount = await client.discounts.create({
58
+ type: 'flat',
59
+ amount: 1000,
60
+ currency_options: [{
61
+ currency: 'USD',
62
+ max_amount_possible: 1000,
63
+ is_default: true,
64
+ }],
65
+ code: 'WELCOME10',
66
+ });
67
+ ```
68
+
69
+ ### List discounts
70
+
71
+ ```typescript
72
+ const discounts = await client.discounts.list({
73
+ page_size: 50,
74
+ page_number: 0,
75
+ });
76
+
77
+ // Paginated responses expose `items`.
78
+ for (const discount of discounts.items) {
79
+ console.log(`${discount.code}: ${discount.type} ${discount.amount}`);
80
+ }
81
+
82
+ // Or let the SDK walk every page for you:
83
+ for await (const discount of client.discounts.list()) {
84
+ console.log(discount.code);
85
+ }
86
+ ```
87
+
88
+ ### Retrieve a discount by ID
89
+
90
+ ```typescript
91
+ const discount = await client.discounts.retrieve('discount_id');
92
+ console.log(discount.code, discount.type, discount.amount);
93
+ ```
94
+
95
+ ### Retrieve a discount by code
96
+
97
+ ```typescript
98
+ try {
99
+ const discount = await client.discounts.retrieveByCode('SUMMER2025');
100
+ console.log('Discount found:', discount.amount);
101
+ } catch (error) {
102
+ console.log('Code not found or expired');
103
+ }
104
+ ```
105
+
106
+ ### Update a discount
107
+
108
+ ```typescript
109
+ await client.discounts.update('discount_id', {
110
+ metadata: { updated_at: new Date().toISOString() }
111
+ });
112
+ ```
113
+
114
+ ### Delete a discount
115
+
116
+ ```typescript
117
+ await client.discounts.delete('discount_id');
118
+ ```
119
+
120
+ ## Discount types and configuration
121
+
122
+ **Important:** The feature documentation states that only percentage discounts are currently supported. However, the current OpenAPI schema defines both `flat` and `percentage` types with currency options. Follow the API reference when implementing. If you encounter unexpected behavior with flat discounts, verify with Dodo support whether flat discounts are fully enabled in your account.
123
+
124
+ ### Percentage discounts
125
+
126
+ Expressed as an integer where 100 = 1%. A 15% discount is `1500`.
127
+
128
+ ```typescript
129
+ const discount = await client.discounts.create({
130
+ type: 'percentage',
131
+ amount: 1500, // 15%
132
+ code: 'PERCENT15',
133
+ });
134
+ ```
135
+
136
+ ### Flat discounts
137
+
138
+ For flat discounts, set the deduction per currency through `currency_options`. A $10 USD deduction is `max_amount_possible: 1000` (cents).
139
+
140
+ ```typescript
141
+ const discount = await client.discounts.create({
142
+ type: 'flat',
143
+ amount: 1000, // $10.00
144
+ currency_options: [{
145
+ currency: 'USD',
146
+ max_amount_possible: 1000,
147
+ is_default: true,
148
+ }],
149
+ code: 'FLAT10',
150
+ });
151
+ ```
152
+
153
+ ## Eligibility and restrictions
154
+
155
+ ### Product restrictions
156
+
157
+ Limit a discount to specific products:
158
+
159
+ ```typescript
160
+ const discount = await client.discounts.create({
161
+ type: 'percentage',
162
+ amount: 1500,
163
+ code: 'PREMIUM_ONLY',
164
+ restricted_to: ['pdt_premium', 'pdt_enterprise'],
165
+ });
166
+ ```
167
+
168
+ ### Customer eligibility
169
+
170
+ Restrict a discount to specific customers by creating it with `customer_eligibility: 'specific'`, then attaching customers through the discount-customer endpoint. `customer_ids` is not a `discounts.create` parameter.
171
+
172
+ ```typescript
173
+ const discount = await client.discounts.create({
174
+ type: 'percentage',
175
+ amount: 1500,
176
+ code: 'VIP_ONLY',
177
+ customer_eligibility: 'specific',
178
+ });
179
+
180
+ await fetch(
181
+ `https://test.dodopayments.com/discounts/${discount.discount_id}/customers`,
182
+ {
183
+ method: 'POST',
184
+ headers: {
185
+ Authorization: `Bearer ${process.env.DODO_PAYMENTS_API_KEY}`,
186
+ 'Content-Type': 'application/json',
187
+ },
188
+ body: JSON.stringify({
189
+ customer_ids: ['cus_vip_001', 'cus_vip_002'],
190
+ }),
191
+ },
192
+ );
193
+ ```
194
+
195
+ ### Date range
196
+
197
+ Set activation and expiration dates:
198
+
199
+ ```typescript
200
+ const discount = await client.discounts.create({
201
+ type: 'percentage',
202
+ amount: 1500,
203
+ code: 'SUMMER2025',
204
+ starts_at: '2025-06-01T00:00:00Z',
205
+ expires_at: '2025-08-31T23:59:59Z',
206
+ });
207
+ ```
208
+
209
+ ### Usage limits
210
+
211
+ Limit the total number of times a code can be used:
212
+
213
+ ```typescript
214
+ const discount = await client.discounts.create({
215
+ type: 'percentage',
216
+ amount: 1500,
217
+ code: 'LIMITED_100',
218
+ usage_limit: 100,
219
+ });
220
+ ```
221
+
222
+ ### Subscription-cycle limits
223
+
224
+ Apply a discount only for the first N billing cycles:
225
+
226
+ ```typescript
227
+ const discount = await client.discounts.create({
228
+ type: 'percentage',
229
+ amount: 1500,
230
+ code: 'FIRST_3_MONTHS',
231
+ subscription_cycles: 3, // applies to first 3 billing cycles only
232
+ });
233
+ ```
234
+
235
+ ## Applying codes at checkout
236
+
237
+ ### Single code
238
+
239
+ ```typescript
240
+ const session = await client.checkoutSessions.create({
241
+ product_cart: [{ product_id: 'pdt_abc', quantity: 1 }],
242
+ discount_codes: ['SUMMER2025'],
243
+ customer: { email: 'user@example.com' },
244
+ return_url: 'https://yoursite.com/return'
245
+ });
246
+
247
+ window.location.href = session.checkout_url;
248
+ ```
249
+
250
+ ### Multiple codes (stacking)
251
+
252
+ Codes are applied in the order specified. Maximum 20 codes per checkout:
253
+
254
+ ```typescript
255
+ const session = await client.checkoutSessions.create({
256
+ product_cart: [{ product_id: 'pdt_abc', quantity: 1 }],
257
+ discount_codes: ['WELCOME10', 'BLACKFRIDAY20', 'LOYALTY5'],
258
+ customer: { email: 'user@example.com' },
259
+ return_url: 'https://yoursite.com/return'
260
+ });
261
+ ```
262
+
263
+ ## Validating codes before checkout
264
+
265
+ Validate a code and check its eligibility before showing adjusted pricing:
266
+
267
+ ```typescript
268
+ async function validateDiscount(code, productId) {
269
+ try {
270
+ const discount = await client.discounts.retrieveByCode(code);
271
+
272
+ // Check expiration
273
+ if (discount.expires_at && new Date(discount.expires_at) < new Date()) {
274
+ return { valid: false, reason: 'Code expired' };
275
+ }
276
+
277
+ // Check usage limit
278
+ if (discount.usage_limit && discount.times_used >= discount.usage_limit) {
279
+ return { valid: false, reason: 'Code exhausted' };
280
+ }
281
+
282
+ // Check product eligibility
283
+ if (discount.restricted_to.length > 0) {
284
+ if (!discount.restricted_to.includes(productId)) {
285
+ return { valid: false, reason: 'Code not valid for this product' };
286
+ }
287
+ }
288
+
289
+ return { valid: true, discount };
290
+ } catch (error) {
291
+ return { valid: false, reason: 'Code not found or unavailable' };
292
+ }
293
+ }
294
+
295
+ // Usage
296
+ const result = await validateDiscount('SUMMER2025', 'pdt_abc');
297
+ if (result.valid) {
298
+ console.log('Discount applies:', result.discount.amount);
299
+ } else {
300
+ console.log('Cannot apply:', result.reason);
301
+ }
302
+ ```
303
+
304
+ ## Discounts on plan changes
305
+
306
+ When a customer changes their subscription plan, discounts can be preserved, replaced, or cleared depending on the `discount_codes` parameter and each discount's `preserve_on_plan_change` setting. Set `preserve_on_plan_change` through `discounts.create` or `discounts.update`; it is not a `subscriptions.changePlan` parameter.
307
+
308
+ ### Behavior matrix
309
+
310
+ | Scenario | `discount_codes` | Discount's `preserve_on_plan_change` | Result |
311
+ |---|---|---|---|
312
+ | Upgrade with no change | `undefined` | `true` | Existing discounts carry forward |
313
+ | Upgrade with no change | `undefined` | `false` | Existing discounts are removed |
314
+ | Upgrade with no change | `[]` (empty array) | any | Existing discounts are removed |
315
+ | Upgrade with replacement | `['NEW_CODE']` | any | Only `NEW_CODE` applies; old discounts removed |
316
+
317
+ ### Example: preserve existing discounts
318
+
319
+ ```typescript
320
+ await client.discounts.update('dsc_existing', {
321
+ preserve_on_plan_change: true,
322
+ });
323
+
324
+ await client.subscriptions.changePlan('sub_123', {
325
+ product_id: 'pdt_pro',
326
+ quantity: 1,
327
+ proration_billing_mode: 'prorated_immediately',
328
+ // Omit discount_codes. Existing discounts configured with
329
+ // preserve_on_plan_change: true carry forward.
330
+ });
331
+ ```
332
+
333
+ ### Example: replace discounts on upgrade
334
+
335
+ ```typescript
336
+ await client.subscriptions.changePlan('sub_123', {
337
+ product_id: 'pdt_pro',
338
+ quantity: 1,
339
+ proration_billing_mode: 'prorated_immediately',
340
+ discount_codes: ['UPGRADE20'], // replaces old discounts
341
+ });
342
+ ```
343
+
344
+ ### Example: remove all discounts on downgrade
345
+
346
+ ```typescript
347
+ await client.subscriptions.changePlan('sub_123', {
348
+ product_id: 'pdt_basic',
349
+ quantity: 1,
350
+ proration_billing_mode: 'prorated_immediately',
351
+ discount_codes: [], // empty array removes all discounts
352
+ });
353
+ ```
354
+
355
+ ## Common mistakes
356
+
357
+ ### Validating discounts client-side only
358
+
359
+ Never trust client-side validation. Always validate on the server before applying a discount at checkout. A user can modify the discount code in the browser.
360
+
361
+ ```typescript
362
+ // WRONG: client-side only
363
+ if (code.length > 0) {
364
+ applyDiscount(code);
365
+ }
366
+
367
+ // CORRECT: server-side validation
368
+ try {
369
+ const discount = await client.discounts.retrieveByCode(code);
370
+ if (isEligible(discount)) {
371
+ // proceed with checkout
372
+ }
373
+ } catch (error) {
374
+ // Unknown or unavailable code; reject the checkout request.
375
+ }
376
+ ```
377
+
378
+ ### Assuming codes stack unconditionally
379
+
380
+ Discounts stack in order, but eligibility rules still apply. If a code is restricted to a product not in the cart, it won't apply even if other codes do.
381
+
382
+ ```typescript
383
+ // Both codes may not apply if they have conflicting restrictions
384
+ const session = await client.checkoutSessions.create({
385
+ product_cart: [{ product_id: 'pdt_basic', quantity: 1 }],
386
+ discount_codes: ['PREMIUM_ONLY', 'BASIC_ONLY'], // one or both may fail
387
+ return_url: 'https://yoursite.com/return'
388
+ });
389
+ ```
390
+
391
+ ### Losing a discount on upgrade by passing an empty array
392
+
393
+ Passing `discount_codes: []` explicitly removes all discounts, even if `preserve_on_plan_change` is true. Omit the parameter entirely if you want to preserve existing discounts.
394
+
395
+ ```typescript
396
+ // WRONG: removes all discounts
397
+ await client.subscriptions.changePlan('sub_123', {
398
+ product_id: 'pdt_pro',
399
+ quantity: 1,
400
+ proration_billing_mode: 'prorated_immediately',
401
+ discount_codes: [], // this clears discounts
402
+ });
403
+
404
+ // CORRECT: preserves discounts whose preserve_on_plan_change property is true
405
+ await client.subscriptions.changePlan('sub_123', {
406
+ product_id: 'pdt_pro',
407
+ quantity: 1,
408
+ proration_billing_mode: 'prorated_immediately',
409
+ // omit discount_codes
410
+ });
411
+ ```
412
+
413
+ ### Not handling expired or exhausted codes
414
+
415
+ A code can expire by date or by reaching its redemption limit. Always check both before showing a discount to the user.
416
+
417
+ ```typescript
418
+ // WRONG: assumes code is always valid
419
+ const discount = await client.discounts.retrieveByCode(code);
420
+ applyDiscount(discount);
421
+
422
+ // CORRECT: catch unknown codes, then check expiration and usage
423
+ try {
424
+ const discount = await client.discounts.retrieveByCode(code);
425
+ if (discount.expires_at && new Date(discount.expires_at) < new Date()) {
426
+ showError('Code expired');
427
+ } else if (discount.usage_limit && discount.times_used >= discount.usage_limit) {
428
+ showError('Code exhausted');
429
+ } else {
430
+ applyDiscount(discount);
431
+ }
432
+ } catch (error) {
433
+ showError('Code not found');
434
+ }
435
+ ```
436
+
437
+ ### Confusing subscription-cycle limits with expiration dates
438
+
439
+ `subscription_cycles` applies only to subscriptions and controls how many billing cycles the discount applies. `expires_at` is a hard cutoff for all uses of the code.
440
+
441
+ ```typescript
442
+ // Applies to first 3 billing cycles of any subscription
443
+ const discount = await client.discounts.create({
444
+ type: 'percentage',
445
+ amount: 1500,
446
+ code: 'FIRST_3_MONTHS',
447
+ subscription_cycles: 3,
448
+ });
449
+
450
+ // Expires on a specific date, regardless of billing cycles
451
+ const discount2 = await client.discounts.create({
452
+ type: 'percentage',
453
+ amount: 1500,
454
+ code: 'SUMMER_ONLY',
455
+ expires_at: '2025-08-31T23:59:59Z',
456
+ });
457
+ ```
458
+
459
+ ## Resources
460
+
461
+ - [Discounts feature guide](https://docs.dodopayments.com/features/discount-codes)
462
+ - [Create discount API](https://docs.dodopayments.com/api-reference/discounts/create-discount)
463
+ - [Checkout Sessions API](https://docs.dodopayments.com/developer-resources/checkout-session)
464
+ - [Subscription plan changes](https://docs.dodopayments.com/api-reference/subscriptions/change-plan)