fresh-squeezy 0.1.11 → 0.1.13
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.
- package/README.es.md +1 -1
- package/README.ja.md +1 -1
- package/README.md +1 -1
- package/README.pt-BR.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/cli.js +677 -622
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +605 -578
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +264 -185
- package/dist/index.d.ts +264 -185
- package/dist/index.js +597 -578
- package/dist/index.js.map +1 -1
- package/package.json +12 -11
- package/assets/fresh-squeezy-hero.png +0 -0
- package/bin/fresh-squeezy.js +0 -2
package/dist/index.cjs
CHANGED
|
@@ -32,6 +32,10 @@ __export(src_exports, {
|
|
|
32
32
|
SUPPORTED_RESOURCES: () => SUPPORTED_RESOURCES,
|
|
33
33
|
buildResult: () => buildResult,
|
|
34
34
|
buildSubscriptionItemUpdateBody: () => buildSubscriptionItemUpdateBody,
|
|
35
|
+
checkDiscount: () => checkDiscount,
|
|
36
|
+
checkLicenseKey: () => checkLicenseKey,
|
|
37
|
+
checkProduct: () => checkProduct,
|
|
38
|
+
checkSubscriptionPlan: () => checkSubscriptionPlan,
|
|
35
39
|
createFreshSqueezy: () => createFreshSqueezy,
|
|
36
40
|
doctor: () => doctor,
|
|
37
41
|
fetchActualMode: () => fetchActualMode,
|
|
@@ -73,7 +77,11 @@ __export(src_exports, {
|
|
|
73
77
|
sameWebhookUrl: () => sameWebhookUrl,
|
|
74
78
|
userResource: () => userResource,
|
|
75
79
|
validateConnection: () => validateConnection,
|
|
80
|
+
validateDiscount: () => validateDiscount,
|
|
81
|
+
validateLicenseKey: () => validateLicenseKey,
|
|
76
82
|
validateProduct: () => validateProduct,
|
|
83
|
+
validateStore: () => validateStore,
|
|
84
|
+
validateSubscriptionPlan: () => validateSubscriptionPlan,
|
|
77
85
|
validateWebhook: () => validateWebhook
|
|
78
86
|
});
|
|
79
87
|
module.exports = __toCommonJS(src_exports);
|
|
@@ -240,24 +248,12 @@ function extractJsonApiErrors(body) {
|
|
|
240
248
|
);
|
|
241
249
|
}
|
|
242
250
|
|
|
243
|
-
// src/resources/users.ts
|
|
244
|
-
async function getAuthenticatedUser(http) {
|
|
245
|
-
return http.request({ path: "/v1/users/me" });
|
|
246
|
-
}
|
|
247
|
-
function userResource(doc) {
|
|
248
|
-
return doc.data;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
251
|
// src/core/mode.ts
|
|
252
252
|
function resolveActualMode(testMode) {
|
|
253
253
|
if (testMode === true) return "test";
|
|
254
254
|
if (testMode === false) return "live";
|
|
255
255
|
return void 0;
|
|
256
256
|
}
|
|
257
|
-
async function fetchActualMode(http) {
|
|
258
|
-
const doc = await getAuthenticatedUser(http);
|
|
259
|
-
return resolveActualMode(doc.meta?.test_mode);
|
|
260
|
-
}
|
|
261
257
|
|
|
262
258
|
// src/resources/stores.ts
|
|
263
259
|
async function getStore(http, storeId) {
|
|
@@ -267,6 +263,14 @@ async function listStores(http) {
|
|
|
267
263
|
return http.paginate("/v1/stores");
|
|
268
264
|
}
|
|
269
265
|
|
|
266
|
+
// src/resources/users.ts
|
|
267
|
+
async function getAuthenticatedUser(http) {
|
|
268
|
+
return http.request({ path: "/v1/users/me" });
|
|
269
|
+
}
|
|
270
|
+
function userResource(doc) {
|
|
271
|
+
return doc.data;
|
|
272
|
+
}
|
|
273
|
+
|
|
270
274
|
// src/validate/rules.ts
|
|
271
275
|
var ISSUE_CODES = {
|
|
272
276
|
AUTH_FAILED: "AUTH_FAILED",
|
|
@@ -355,20 +359,20 @@ function mapErrorToIssue(err, mapping = {}) {
|
|
|
355
359
|
return issue(ISSUE_CODES.UNKNOWN, "error", message);
|
|
356
360
|
}
|
|
357
361
|
async function probeFetch(fetcher, options) {
|
|
362
|
+
return probeCollection(fetcher, {
|
|
363
|
+
notFound: {
|
|
364
|
+
code: options.notFoundCode,
|
|
365
|
+
message: options.notFoundMessage,
|
|
366
|
+
...options.notFoundFix !== void 0 ? { suggestedFix: options.notFoundFix } : {},
|
|
367
|
+
...options.notFoundContext !== void 0 ? { context: options.notFoundContext } : {}
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
async function probeCollection(fetcher, mapping = {}) {
|
|
358
372
|
try {
|
|
359
373
|
return { ok: true, resource: await fetcher() };
|
|
360
374
|
} catch (err) {
|
|
361
|
-
return {
|
|
362
|
-
ok: false,
|
|
363
|
-
issue: mapErrorToIssue(err, {
|
|
364
|
-
notFound: {
|
|
365
|
-
code: options.notFoundCode,
|
|
366
|
-
message: options.notFoundMessage,
|
|
367
|
-
...options.notFoundFix !== void 0 ? { suggestedFix: options.notFoundFix } : {},
|
|
368
|
-
...options.notFoundContext !== void 0 ? { context: options.notFoundContext } : {}
|
|
369
|
-
}
|
|
370
|
-
})
|
|
371
|
-
};
|
|
375
|
+
return { ok: false, issue: mapErrorToIssue(err, mapping) };
|
|
372
376
|
}
|
|
373
377
|
}
|
|
374
378
|
function checkStoreOwnership(check) {
|
|
@@ -394,187 +398,538 @@ function checkStoreOwnership(check) {
|
|
|
394
398
|
// src/validate/connection.ts
|
|
395
399
|
async function validateConnection(http, mode) {
|
|
396
400
|
const issues = [];
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
issues.push(
|
|
410
|
-
issue(
|
|
411
|
-
ISSUE_CODES.MODE_MISMATCH,
|
|
412
|
-
"error",
|
|
413
|
-
`API key is a ${actualMode}-mode key but was run with --mode ${mode}.`,
|
|
414
|
-
{
|
|
415
|
-
suggestedFix: `Either pass --mode ${actualMode} or use a ${mode}-mode key from https://app.lemonsqueezy.com/settings/api.`,
|
|
416
|
-
context: { declared: mode, actual: actualMode }
|
|
417
|
-
}
|
|
418
|
-
)
|
|
419
|
-
);
|
|
420
|
-
}
|
|
421
|
-
if (stores.length === 0) {
|
|
422
|
-
issues.push(
|
|
423
|
-
issue(
|
|
424
|
-
ISSUE_CODES.STORE_NOT_FOUND,
|
|
425
|
-
"warning",
|
|
426
|
-
"API key authenticated but no stores are reachable.",
|
|
427
|
-
{ suggestedFix: "Confirm the API key belongs to an account that owns at least one store." }
|
|
428
|
-
)
|
|
429
|
-
);
|
|
401
|
+
const fetched = await probeCollection(
|
|
402
|
+
async () => {
|
|
403
|
+
const userDoc2 = await getAuthenticatedUser(http);
|
|
404
|
+
const stores2 = await listStores(http);
|
|
405
|
+
return { userDoc: userDoc2, stores: stores2 };
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
unauthorized: {
|
|
409
|
+
code: ISSUE_CODES.AUTH_FAILED,
|
|
410
|
+
message: "API key rejected by Lemon Squeezy.",
|
|
411
|
+
suggestedFix: "Regenerate the key at https://app.lemonsqueezy.com/settings/api."
|
|
412
|
+
}
|
|
430
413
|
}
|
|
431
|
-
|
|
432
|
-
|
|
414
|
+
);
|
|
415
|
+
if (!fetched.ok) {
|
|
416
|
+
issues.push(fetched.issue);
|
|
417
|
+
return buildResult("connection", mode, issues);
|
|
418
|
+
}
|
|
419
|
+
const { userDoc, stores } = fetched.resource;
|
|
420
|
+
const actualMode = resolveActualMode(userDoc.meta?.test_mode);
|
|
421
|
+
const summary = {
|
|
422
|
+
user: userDoc.data.attributes,
|
|
423
|
+
storeCount: stores.length,
|
|
424
|
+
storeIds: stores.map((store) => store.id),
|
|
425
|
+
declaredMode: mode,
|
|
426
|
+
...actualMode ? { actualMode } : {}
|
|
427
|
+
};
|
|
428
|
+
if (actualMode && actualMode !== mode) {
|
|
433
429
|
issues.push(
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
430
|
+
issue(
|
|
431
|
+
ISSUE_CODES.MODE_MISMATCH,
|
|
432
|
+
"error",
|
|
433
|
+
`API key is a ${actualMode}-mode key but was run with --mode ${mode}.`,
|
|
434
|
+
{
|
|
435
|
+
suggestedFix: `Either pass --mode ${actualMode} or use a ${mode}-mode key from https://app.lemonsqueezy.com/settings/api.`,
|
|
436
|
+
context: { declared: mode, actual: actualMode }
|
|
439
437
|
}
|
|
440
|
-
|
|
438
|
+
)
|
|
441
439
|
);
|
|
442
|
-
return buildResult("connection", mode, issues);
|
|
443
440
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
notFoundContext: { storeId: String(storeId) }
|
|
454
|
-
});
|
|
455
|
-
if (!fetched.ok) {
|
|
456
|
-
issues.push(fetched.issue);
|
|
457
|
-
return buildResult("store", mode, issues, void 0, {
|
|
458
|
-
label: `store ${storeId}`,
|
|
459
|
-
id: String(storeId)
|
|
460
|
-
});
|
|
441
|
+
if (stores.length === 0) {
|
|
442
|
+
issues.push(
|
|
443
|
+
issue(
|
|
444
|
+
ISSUE_CODES.STORE_NOT_FOUND,
|
|
445
|
+
"warning",
|
|
446
|
+
"API key authenticated but no stores are reachable.",
|
|
447
|
+
{ suggestedFix: "Confirm the API key belongs to an account that owns at least one store." }
|
|
448
|
+
)
|
|
449
|
+
);
|
|
461
450
|
}
|
|
462
|
-
return buildResult("
|
|
463
|
-
label: fetched.resource.attributes.name,
|
|
464
|
-
id: String(storeId)
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// src/resources/products.ts
|
|
469
|
-
async function getProduct(http, productId) {
|
|
470
|
-
return http.getResource(`/v1/products/${productId}`);
|
|
451
|
+
return buildResult("connection", mode, issues, summary);
|
|
471
452
|
}
|
|
472
|
-
async function
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
});
|
|
453
|
+
async function fetchActualMode(http) {
|
|
454
|
+
const doc = await getAuthenticatedUser(http);
|
|
455
|
+
return resolveActualMode(doc.meta?.test_mode);
|
|
476
456
|
}
|
|
477
457
|
|
|
478
|
-
// src/resources/
|
|
479
|
-
async function
|
|
480
|
-
return http.getResource(`/v1/
|
|
481
|
-
}
|
|
482
|
-
async function listVariantsForProduct(http, productId) {
|
|
483
|
-
return http.paginate("/v1/variants", {
|
|
484
|
-
"filter[product_id]": String(productId)
|
|
485
|
-
});
|
|
458
|
+
// src/resources/discounts.ts
|
|
459
|
+
async function getDiscount(http, discountId) {
|
|
460
|
+
return http.getResource(`/v1/discounts/${discountId}`);
|
|
486
461
|
}
|
|
487
462
|
|
|
488
|
-
// src/validate/
|
|
489
|
-
async function
|
|
463
|
+
// src/validate/discount.ts
|
|
464
|
+
async function validateDiscount(http, mode, options) {
|
|
490
465
|
const issues = [];
|
|
491
|
-
const fetched = await probeFetch(() =>
|
|
492
|
-
notFoundCode: ISSUE_CODES.
|
|
493
|
-
notFoundMessage: `
|
|
494
|
-
notFoundFix: "Verify the
|
|
495
|
-
notFoundContext: {
|
|
466
|
+
const fetched = await probeFetch(() => getDiscount(http, options.discountId), {
|
|
467
|
+
notFoundCode: ISSUE_CODES.DISCOUNT_NOT_FOUND,
|
|
468
|
+
notFoundMessage: `Discount ${options.discountId} not found.`,
|
|
469
|
+
notFoundFix: "Verify the discount ID in the Lemon Squeezy dashboard.",
|
|
470
|
+
notFoundContext: { discountId: String(options.discountId) }
|
|
496
471
|
});
|
|
497
472
|
if (!fetched.ok) {
|
|
498
473
|
issues.push(fetched.issue);
|
|
499
|
-
return buildResult("
|
|
500
|
-
label: `
|
|
501
|
-
id: String(options.
|
|
474
|
+
return buildResult("discount", mode, issues, void 0, {
|
|
475
|
+
label: `discount ${options.discountId}`,
|
|
476
|
+
id: String(options.discountId)
|
|
502
477
|
});
|
|
503
478
|
}
|
|
504
479
|
const attrs = fetched.resource.attributes;
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
480
|
+
issues.push(...checkDiscount(attrs, { storeId: options.storeId }));
|
|
481
|
+
return buildResult("discount", mode, issues, attrs, {
|
|
482
|
+
label: `${attrs.name} (${attrs.code})`,
|
|
483
|
+
id: String(options.discountId)
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
function checkDiscount(attrs, options) {
|
|
487
|
+
const issues = [];
|
|
488
|
+
const mismatch = checkStoreOwnership({
|
|
489
|
+
expectedStoreId: options.storeId,
|
|
490
|
+
actualStoreId: attrs.store_id,
|
|
491
|
+
code: ISSUE_CODES.DISCOUNT_STORE_MISMATCH,
|
|
492
|
+
label: "Discount",
|
|
493
|
+
suggestedFix: "Use the correct store ID or discount ID \u2014 discounts should not cross stores."
|
|
494
|
+
});
|
|
495
|
+
if (mismatch) issues.push(mismatch);
|
|
496
|
+
if (attrs.status === "draft") {
|
|
497
|
+
issues.push(
|
|
498
|
+
issue(
|
|
499
|
+
ISSUE_CODES.DISCOUNT_DRAFT,
|
|
500
|
+
"warning",
|
|
501
|
+
`Discount "${attrs.name}" is in draft status \u2014 customers cannot redeem it.`,
|
|
502
|
+
{
|
|
503
|
+
suggestedFix: "Publish the discount in the Lemon Squeezy dashboard before sharing the code.",
|
|
504
|
+
context: { name: attrs.name, code: attrs.code }
|
|
505
|
+
}
|
|
506
|
+
)
|
|
507
|
+
);
|
|
514
508
|
}
|
|
515
|
-
|
|
509
|
+
const now = /* @__PURE__ */ new Date();
|
|
510
|
+
if (attrs.expires_at && new Date(attrs.expires_at) < now) {
|
|
516
511
|
issues.push(
|
|
517
512
|
issue(
|
|
518
|
-
ISSUE_CODES.
|
|
513
|
+
ISSUE_CODES.DISCOUNT_EXPIRED,
|
|
519
514
|
"error",
|
|
520
|
-
`
|
|
515
|
+
`Discount "${attrs.name}" expired at ${attrs.expires_at}.`,
|
|
521
516
|
{
|
|
522
|
-
suggestedFix: "
|
|
523
|
-
context: {
|
|
517
|
+
suggestedFix: "Extend the expiration date or create a new discount.",
|
|
518
|
+
context: { name: attrs.name, expiresAt: attrs.expires_at }
|
|
524
519
|
}
|
|
525
520
|
)
|
|
526
521
|
);
|
|
527
522
|
}
|
|
528
|
-
if (
|
|
523
|
+
if (attrs.starts_at && new Date(attrs.starts_at) > now) {
|
|
529
524
|
issues.push(
|
|
530
525
|
issue(
|
|
531
|
-
ISSUE_CODES.
|
|
526
|
+
ISSUE_CODES.DISCOUNT_NOT_STARTED,
|
|
532
527
|
"warning",
|
|
533
|
-
"
|
|
534
|
-
{
|
|
528
|
+
`Discount "${attrs.name}" starts at ${attrs.starts_at} \u2014 not yet active.`,
|
|
529
|
+
{
|
|
530
|
+
suggestedFix: "Wait for the start date or adjust it in the dashboard.",
|
|
531
|
+
context: { name: attrs.name, startsAt: attrs.starts_at }
|
|
532
|
+
}
|
|
535
533
|
)
|
|
536
534
|
);
|
|
537
535
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
"
|
|
546
|
-
{
|
|
547
|
-
|
|
548
|
-
)
|
|
549
|
-
|
|
550
|
-
issues.push(
|
|
551
|
-
issue(
|
|
552
|
-
ISSUE_CODES.VARIANT_UNPUBLISHED,
|
|
553
|
-
"error",
|
|
554
|
-
"Product has variants but none are published.",
|
|
555
|
-
{ suggestedFix: "Publish at least one variant." }
|
|
556
|
-
)
|
|
557
|
-
);
|
|
558
|
-
}
|
|
559
|
-
} catch (err) {
|
|
560
|
-
const message = err instanceof Error ? err.message : "Unknown error fetching variants";
|
|
561
|
-
issues.push(issue(ISSUE_CODES.UNKNOWN, "warning", message));
|
|
536
|
+
if (attrs.is_limited_redemptions && attrs.max_redemptions <= 0) {
|
|
537
|
+
issues.push(
|
|
538
|
+
issue(
|
|
539
|
+
ISSUE_CODES.DISCOUNT_REDEMPTIONS_EXHAUSTED,
|
|
540
|
+
"warning",
|
|
541
|
+
`Discount "${attrs.name}" has limited redemptions with max_redemptions \u2264 0.`,
|
|
542
|
+
{
|
|
543
|
+
suggestedFix: "Increase max_redemptions or disable the redemption limit.",
|
|
544
|
+
context: { name: attrs.name, maxRedemptions: attrs.max_redemptions }
|
|
545
|
+
}
|
|
546
|
+
)
|
|
547
|
+
);
|
|
562
548
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
549
|
+
if (attrs.amount <= 0) {
|
|
550
|
+
issues.push(
|
|
551
|
+
issue(
|
|
552
|
+
ISSUE_CODES.DISCOUNT_INVALID_AMOUNT,
|
|
553
|
+
"error",
|
|
554
|
+
`Discount "${attrs.name}" has amount ${attrs.amount} \u2014 must be positive.`,
|
|
555
|
+
{
|
|
556
|
+
suggestedFix: "Set a positive discount amount in the dashboard.",
|
|
557
|
+
context: { name: attrs.name, amount: attrs.amount }
|
|
558
|
+
}
|
|
559
|
+
)
|
|
560
|
+
);
|
|
561
|
+
} else if (attrs.amount_type === "percent" && attrs.amount > 100) {
|
|
562
|
+
issues.push(
|
|
563
|
+
issue(
|
|
564
|
+
ISSUE_CODES.DISCOUNT_INVALID_AMOUNT,
|
|
565
|
+
"error",
|
|
566
|
+
`Discount "${attrs.name}" is ${attrs.amount}% \u2014 percent discounts cannot exceed 100%.`,
|
|
567
|
+
{
|
|
568
|
+
suggestedFix: "Set the discount to 100% or less.",
|
|
569
|
+
context: { name: attrs.name, amount: attrs.amount, amountType: attrs.amount_type }
|
|
570
|
+
}
|
|
571
|
+
)
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
return issues;
|
|
567
575
|
}
|
|
568
576
|
|
|
569
|
-
// src/
|
|
570
|
-
function
|
|
571
|
-
return
|
|
572
|
-
}
|
|
573
|
-
function normalizeWebhookUrl(raw) {
|
|
574
|
-
return raw.replace(/\/+$/, "").toLowerCase();
|
|
577
|
+
// src/resources/licenseKeys.ts
|
|
578
|
+
async function getLicenseKey(http, licenseKeyId) {
|
|
579
|
+
return http.getResource(`/v1/license-keys/${licenseKeyId}`);
|
|
575
580
|
}
|
|
576
581
|
|
|
577
|
-
// src/
|
|
582
|
+
// src/validate/licenseKey.ts
|
|
583
|
+
async function validateLicenseKey(http, mode, options) {
|
|
584
|
+
const issues = [];
|
|
585
|
+
const fetched = await probeFetch(() => getLicenseKey(http, options.licenseKeyId), {
|
|
586
|
+
notFoundCode: ISSUE_CODES.LICENSE_KEY_NOT_FOUND,
|
|
587
|
+
notFoundMessage: `License key ${options.licenseKeyId} not found.`,
|
|
588
|
+
notFoundFix: "Verify the license key ID in the Lemon Squeezy dashboard.",
|
|
589
|
+
notFoundContext: { licenseKeyId: String(options.licenseKeyId) }
|
|
590
|
+
});
|
|
591
|
+
if (!fetched.ok) {
|
|
592
|
+
issues.push(fetched.issue);
|
|
593
|
+
return buildResult("licenseKey", mode, issues, void 0, {
|
|
594
|
+
label: `license key ${options.licenseKeyId}`,
|
|
595
|
+
id: String(options.licenseKeyId)
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
const attrs = fetched.resource.attributes;
|
|
599
|
+
issues.push(...checkLicenseKey(attrs, { storeId: options.storeId }));
|
|
600
|
+
return buildResult("licenseKey", mode, issues, attrs, {
|
|
601
|
+
label: attrs.key_short,
|
|
602
|
+
id: String(options.licenseKeyId)
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
function checkLicenseKey(attrs, options) {
|
|
606
|
+
const issues = [];
|
|
607
|
+
const mismatch = checkStoreOwnership({
|
|
608
|
+
expectedStoreId: options.storeId,
|
|
609
|
+
actualStoreId: attrs.store_id,
|
|
610
|
+
code: ISSUE_CODES.LICENSE_KEY_STORE_MISMATCH,
|
|
611
|
+
label: "License key",
|
|
612
|
+
suggestedFix: "Use the correct store ID or license key ID \u2014 keys should not cross stores."
|
|
613
|
+
});
|
|
614
|
+
if (mismatch) issues.push(mismatch);
|
|
615
|
+
if (attrs.disabled) {
|
|
616
|
+
issues.push(
|
|
617
|
+
issue(
|
|
618
|
+
ISSUE_CODES.LICENSE_KEY_DISABLED,
|
|
619
|
+
"error",
|
|
620
|
+
`License key ${attrs.key_short} is disabled.`,
|
|
621
|
+
{
|
|
622
|
+
suggestedFix: "Re-enable the license key in the Lemon Squeezy dashboard.",
|
|
623
|
+
context: { keyShort: attrs.key_short }
|
|
624
|
+
}
|
|
625
|
+
)
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
if (attrs.expires_at && new Date(attrs.expires_at) < /* @__PURE__ */ new Date()) {
|
|
629
|
+
issues.push(
|
|
630
|
+
issue(
|
|
631
|
+
ISSUE_CODES.LICENSE_KEY_EXPIRED,
|
|
632
|
+
"error",
|
|
633
|
+
`License key ${attrs.key_short} expired at ${attrs.expires_at}.`,
|
|
634
|
+
{
|
|
635
|
+
suggestedFix: "Extend the expiration date or issue a new license key.",
|
|
636
|
+
context: { keyShort: attrs.key_short, expiresAt: attrs.expires_at }
|
|
637
|
+
}
|
|
638
|
+
)
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
if (attrs.activation_limit !== null && attrs.instances_count >= attrs.activation_limit) {
|
|
642
|
+
issues.push(
|
|
643
|
+
issue(
|
|
644
|
+
ISSUE_CODES.LICENSE_KEY_AT_ACTIVATION_LIMIT,
|
|
645
|
+
"warning",
|
|
646
|
+
`License key ${attrs.key_short} has reached its activation limit (${attrs.instances_count}/${attrs.activation_limit}).`,
|
|
647
|
+
{
|
|
648
|
+
suggestedFix: "Increase the activation limit or deactivate unused instances.",
|
|
649
|
+
context: {
|
|
650
|
+
keyShort: attrs.key_short,
|
|
651
|
+
instancesCount: attrs.instances_count,
|
|
652
|
+
activationLimit: attrs.activation_limit
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
)
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
return issues;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// src/resources/products.ts
|
|
662
|
+
async function getProduct(http, productId) {
|
|
663
|
+
return http.getResource(`/v1/products/${productId}`);
|
|
664
|
+
}
|
|
665
|
+
async function listProducts(http, storeId) {
|
|
666
|
+
return http.paginate("/v1/products", {
|
|
667
|
+
"filter[store_id]": String(storeId)
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// src/resources/variants.ts
|
|
672
|
+
async function getVariant(http, variantId) {
|
|
673
|
+
return http.getResource(`/v1/variants/${variantId}`);
|
|
674
|
+
}
|
|
675
|
+
async function listVariantsForProduct(http, productId) {
|
|
676
|
+
return http.paginate("/v1/variants", {
|
|
677
|
+
"filter[product_id]": String(productId)
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// src/validate/product.ts
|
|
682
|
+
async function validateProduct(http, mode, options) {
|
|
683
|
+
const issues = [];
|
|
684
|
+
const fetched = await probeFetch(() => getProduct(http, options.productId), {
|
|
685
|
+
notFoundCode: ISSUE_CODES.PRODUCT_NOT_FOUND,
|
|
686
|
+
notFoundMessage: `Product ${options.productId} not found.`,
|
|
687
|
+
notFoundFix: "Verify the product ID in the Lemon Squeezy dashboard.",
|
|
688
|
+
notFoundContext: { productId: String(options.productId) }
|
|
689
|
+
});
|
|
690
|
+
if (!fetched.ok) {
|
|
691
|
+
issues.push(fetched.issue);
|
|
692
|
+
return buildResult("product", mode, issues, void 0, {
|
|
693
|
+
label: `product ${options.productId}`,
|
|
694
|
+
id: String(options.productId)
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
const attrs = fetched.resource.attributes;
|
|
698
|
+
issues.push(...checkProduct(attrs, { expectedStoreId: options.expectedStoreId }));
|
|
699
|
+
try {
|
|
700
|
+
const variants = await listVariantsForProduct(http, options.productId);
|
|
701
|
+
if (variants.length === 0) {
|
|
702
|
+
issues.push(
|
|
703
|
+
issue(
|
|
704
|
+
ISSUE_CODES.VARIANT_MISSING,
|
|
705
|
+
"error",
|
|
706
|
+
"Product has no variants. Customers cannot purchase it.",
|
|
707
|
+
{ suggestedFix: "Add at least one variant in the product configuration." }
|
|
708
|
+
)
|
|
709
|
+
);
|
|
710
|
+
} else if (!variants.some((variant) => variant.attributes.status === "published")) {
|
|
711
|
+
issues.push(
|
|
712
|
+
issue(
|
|
713
|
+
ISSUE_CODES.VARIANT_UNPUBLISHED,
|
|
714
|
+
"error",
|
|
715
|
+
"Product has variants but none are published.",
|
|
716
|
+
{ suggestedFix: "Publish at least one variant." }
|
|
717
|
+
)
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
} catch (err) {
|
|
721
|
+
const message = err instanceof Error ? err.message : "Unknown error fetching variants";
|
|
722
|
+
issues.push(issue(ISSUE_CODES.UNKNOWN, "warning", message));
|
|
723
|
+
}
|
|
724
|
+
return buildResult("product", mode, issues, attrs, {
|
|
725
|
+
label: attrs.name,
|
|
726
|
+
id: String(options.productId)
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
function checkProduct(attrs, options = {}) {
|
|
730
|
+
const issues = [];
|
|
731
|
+
if (options.expectedStoreId !== void 0) {
|
|
732
|
+
const mismatch = checkStoreOwnership({
|
|
733
|
+
expectedStoreId: options.expectedStoreId,
|
|
734
|
+
actualStoreId: attrs.store_id,
|
|
735
|
+
code: ISSUE_CODES.PRODUCT_WRONG_STORE,
|
|
736
|
+
label: "Product",
|
|
737
|
+
suggestedFix: "Either use the correct store ID or the correct product ID \u2014 IDs should not cross stores."
|
|
738
|
+
});
|
|
739
|
+
if (mismatch) issues.push(mismatch);
|
|
740
|
+
}
|
|
741
|
+
if (attrs.status !== "published") {
|
|
742
|
+
issues.push(
|
|
743
|
+
issue(
|
|
744
|
+
ISSUE_CODES.PRODUCT_UNPUBLISHED,
|
|
745
|
+
"error",
|
|
746
|
+
`Product is in "${attrs.status}" state, not "published".`,
|
|
747
|
+
{
|
|
748
|
+
suggestedFix: "Publish the product in the Lemon Squeezy dashboard before selling.",
|
|
749
|
+
context: { status: attrs.status }
|
|
750
|
+
}
|
|
751
|
+
)
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
if (!attrs.buy_now_url) {
|
|
755
|
+
issues.push(
|
|
756
|
+
issue(
|
|
757
|
+
ISSUE_CODES.PRODUCT_NO_BUY_URL,
|
|
758
|
+
"warning",
|
|
759
|
+
"Product has no buy-now URL. Hosted checkout may be disabled.",
|
|
760
|
+
{ suggestedFix: "Enable buy-now in product settings, or use a custom checkout flow." }
|
|
761
|
+
)
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
return issues;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// src/validate/store.ts
|
|
768
|
+
async function validateStore(http, mode, storeId) {
|
|
769
|
+
const issues = [];
|
|
770
|
+
const fetched = await probeFetch(() => getStore(http, storeId), {
|
|
771
|
+
notFoundCode: ISSUE_CODES.STORE_NOT_FOUND,
|
|
772
|
+
notFoundMessage: `Store ${storeId} not found with the current API key.`,
|
|
773
|
+
notFoundFix: "Check the store ID and confirm the API key belongs to the account that owns it.",
|
|
774
|
+
notFoundContext: { storeId: String(storeId) }
|
|
775
|
+
});
|
|
776
|
+
if (!fetched.ok) {
|
|
777
|
+
issues.push(fetched.issue);
|
|
778
|
+
return buildResult("store", mode, issues, void 0, {
|
|
779
|
+
label: `store ${storeId}`,
|
|
780
|
+
id: String(storeId)
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
return buildResult("store", mode, issues, fetched.resource.attributes, {
|
|
784
|
+
label: fetched.resource.attributes.name,
|
|
785
|
+
id: String(storeId)
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// src/validate/subscriptionPlan.ts
|
|
790
|
+
var VALID_INTERVALS = /* @__PURE__ */ new Set(["day", "week", "month", "year"]);
|
|
791
|
+
async function validateSubscriptionPlan(http, mode, options) {
|
|
792
|
+
const issues = [];
|
|
793
|
+
const fetched = await probeFetch(
|
|
794
|
+
() => getVariant(http, options.variantId),
|
|
795
|
+
{
|
|
796
|
+
notFoundCode: ISSUE_CODES.PLAN_VARIANT_NOT_FOUND,
|
|
797
|
+
notFoundMessage: `Variant ${options.variantId} not found.`,
|
|
798
|
+
notFoundFix: "Verify the variant ID in the Lemon Squeezy dashboard.",
|
|
799
|
+
notFoundContext: { variantId: String(options.variantId) }
|
|
800
|
+
}
|
|
801
|
+
);
|
|
802
|
+
if (!fetched.ok) {
|
|
803
|
+
issues.push(fetched.issue);
|
|
804
|
+
return buildResult("subscriptionPlan", mode, issues, void 0, {
|
|
805
|
+
label: `variant ${options.variantId}`,
|
|
806
|
+
id: String(options.variantId)
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
const variant = fetched.resource;
|
|
810
|
+
const attrs = variant.attributes;
|
|
811
|
+
issues.push(...checkSubscriptionPlan(attrs, { variantId: options.variantId }));
|
|
812
|
+
try {
|
|
813
|
+
const product = await getProduct(http, attrs.product_id);
|
|
814
|
+
const mismatch = checkStoreOwnership({
|
|
815
|
+
expectedStoreId: options.storeId,
|
|
816
|
+
actualStoreId: product.attributes.store_id,
|
|
817
|
+
code: ISSUE_CODES.PLAN_STORE_MISMATCH,
|
|
818
|
+
label: `Subscription variant (via product ${attrs.product_id})`,
|
|
819
|
+
suggestedFix: "Use the correct store ID or variant ID \u2014 plans should not cross stores.",
|
|
820
|
+
extraContext: { productId: String(attrs.product_id) }
|
|
821
|
+
});
|
|
822
|
+
if (mismatch) issues.push(mismatch);
|
|
823
|
+
} catch {
|
|
824
|
+
}
|
|
825
|
+
const summary = {
|
|
826
|
+
variantId: variant.id,
|
|
827
|
+
interval: attrs.interval,
|
|
828
|
+
intervalCount: attrs.interval_count,
|
|
829
|
+
price: attrs.price,
|
|
830
|
+
hasFreeTrial: attrs.has_free_trial,
|
|
831
|
+
status: attrs.status
|
|
832
|
+
};
|
|
833
|
+
return buildResult("subscriptionPlan", mode, issues, summary, {
|
|
834
|
+
label: attrs.name,
|
|
835
|
+
id: String(options.variantId)
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
function checkSubscriptionPlan(attrs, options) {
|
|
839
|
+
const issues = [];
|
|
840
|
+
if (!attrs.is_subscription) {
|
|
841
|
+
issues.push(
|
|
842
|
+
issue(
|
|
843
|
+
ISSUE_CODES.PLAN_NOT_SUBSCRIPTION,
|
|
844
|
+
"error",
|
|
845
|
+
`Variant ${options.variantId} is not a subscription variant (is_subscription is false).`,
|
|
846
|
+
{
|
|
847
|
+
suggestedFix: "Use a variant that has subscription billing enabled, or use the regular variant validator.",
|
|
848
|
+
context: { variantId: String(options.variantId) }
|
|
849
|
+
}
|
|
850
|
+
)
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
if (!attrs.interval || !VALID_INTERVALS.has(attrs.interval)) {
|
|
854
|
+
issues.push(
|
|
855
|
+
issue(
|
|
856
|
+
ISSUE_CODES.PLAN_INVALID_INTERVAL,
|
|
857
|
+
"error",
|
|
858
|
+
`Subscription variant has invalid interval: "${attrs.interval ?? "missing"}". Expected one of: day, week, month, year.`,
|
|
859
|
+
{
|
|
860
|
+
suggestedFix: "Set a valid billing interval in the variant configuration.",
|
|
861
|
+
context: { interval: attrs.interval ?? null }
|
|
862
|
+
}
|
|
863
|
+
)
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
if (attrs.interval_count === null || attrs.interval_count <= 0) {
|
|
867
|
+
issues.push(
|
|
868
|
+
issue(
|
|
869
|
+
ISSUE_CODES.PLAN_INVALID_INTERVAL,
|
|
870
|
+
"error",
|
|
871
|
+
`Subscription variant has invalid interval_count: ${attrs.interval_count}. Must be a positive integer.`,
|
|
872
|
+
{
|
|
873
|
+
suggestedFix: "Set interval_count to a positive value (e.g. 1 for monthly, 2 for biweekly).",
|
|
874
|
+
context: { intervalCount: attrs.interval_count }
|
|
875
|
+
}
|
|
876
|
+
)
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
if (attrs.price === 0 && attrs.is_subscription) {
|
|
880
|
+
issues.push(
|
|
881
|
+
issue(
|
|
882
|
+
ISSUE_CODES.PLAN_FREE_PRICE,
|
|
883
|
+
"warning",
|
|
884
|
+
"Subscription variant has a price of 0 \u2014 this is almost always a misconfiguration for paid plans.",
|
|
885
|
+
{
|
|
886
|
+
suggestedFix: "Set the variant price to the intended amount in cents, or confirm this is intentionally free.",
|
|
887
|
+
context: { price: attrs.price }
|
|
888
|
+
}
|
|
889
|
+
)
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
if (attrs.has_free_trial && (!attrs.trial_interval || (attrs.trial_interval_count ?? 0) <= 0)) {
|
|
893
|
+
issues.push(
|
|
894
|
+
issue(
|
|
895
|
+
ISSUE_CODES.PLAN_TRIAL_INCONSISTENT,
|
|
896
|
+
"warning",
|
|
897
|
+
`Subscription variant has free trial enabled but trial interval is misconfigured (interval: "${attrs.trial_interval ?? "missing"}", count: ${attrs.trial_interval_count ?? 0}).`,
|
|
898
|
+
{
|
|
899
|
+
suggestedFix: "Set a valid trial interval and count, or disable the free trial.",
|
|
900
|
+
context: {
|
|
901
|
+
trialInterval: attrs.trial_interval ?? null,
|
|
902
|
+
trialIntervalCount: attrs.trial_interval_count ?? null
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
)
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
if (attrs.status === "draft") {
|
|
909
|
+
issues.push(
|
|
910
|
+
issue(
|
|
911
|
+
ISSUE_CODES.PLAN_DRAFT,
|
|
912
|
+
"warning",
|
|
913
|
+
"Subscription variant is in draft status \u2014 customers cannot subscribe.",
|
|
914
|
+
{
|
|
915
|
+
suggestedFix: "Publish the variant in the Lemon Squeezy dashboard.",
|
|
916
|
+
context: { status: attrs.status }
|
|
917
|
+
}
|
|
918
|
+
)
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
return issues;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// src/core/equality.ts
|
|
925
|
+
function sameWebhookUrl(a, b) {
|
|
926
|
+
return normalizeWebhookUrl(a) === normalizeWebhookUrl(b);
|
|
927
|
+
}
|
|
928
|
+
function normalizeWebhookUrl(raw) {
|
|
929
|
+
return raw.replace(/\/+$/, "").toLowerCase();
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// src/resources/webhooks.ts
|
|
578
933
|
async function listWebhooksForStore(http, storeId) {
|
|
579
934
|
return http.paginate("/v1/webhooks", {
|
|
580
935
|
"filter[store_id]": String(storeId)
|
|
@@ -709,447 +1064,111 @@ var ACKNOWLEDGED_CHANGELOG_ENTRIES = [
|
|
|
709
1064
|
},
|
|
710
1065
|
{
|
|
711
1066
|
date: "2024-02-12",
|
|
712
|
-
summary: "Added Generate subscription invoice endpoint and invoice_immediately / disable_prorations parameters.",
|
|
713
|
-
handledBy: "SubscriptionItemUpdateAttributes.invoice_immediately / disable_prorations; invoice endpoint reachable via client.request()."
|
|
714
|
-
},
|
|
715
|
-
{
|
|
716
|
-
date: "2024-02-05",
|
|
717
|
-
summary: "Added tax_inclusive parameter to Order objects and Subscription invoice objects.",
|
|
718
|
-
handledBy: "OrderAttributes.tax_inclusive, SubscriptionInvoiceAttributes.tax_inclusive."
|
|
719
|
-
},
|
|
720
|
-
{
|
|
721
|
-
date: "2024-01-21",
|
|
722
|
-
summary: "Added setup_fee_enabled / setup_fee to Price objects; setup_fee* to Subscription invoice.",
|
|
723
|
-
handledBy: "PriceAttributes.setup_fee*, OrderAttributes.setup_fee*."
|
|
724
|
-
},
|
|
725
|
-
{
|
|
726
|
-
date: "2024-01-15",
|
|
727
|
-
summary: "Added unit_price_decimal to Price objects.",
|
|
728
|
-
handledBy: "PriceAttributes.unit_price_decimal."
|
|
729
|
-
},
|
|
730
|
-
{
|
|
731
|
-
date: "2024-01-05",
|
|
732
|
-
summary: "Added test_mode flag to /v1/users/me meta.",
|
|
733
|
-
handledBy: "Read in validateConnection to emit MODE_MISMATCH when the key's true mode differs from the caller's declared mode."
|
|
734
|
-
},
|
|
735
|
-
{
|
|
736
|
-
date: "2023-09-19",
|
|
737
|
-
summary: "Added customer_portal URLs to Subscription and Customer objects.",
|
|
738
|
-
handledBy: "SubscriptionUrls.customer_portal and CustomerUrls.customer_portal."
|
|
739
|
-
},
|
|
740
|
-
{
|
|
741
|
-
date: "2023-08-23",
|
|
742
|
-
summary: "Added checkout_data.variant_quantities, subscription items, prices, and usage-based billing.",
|
|
743
|
-
handledBy: "CheckoutData.variant_quantities, SubscriptionItemAttributes, UsageRecordAttributes, PriceAttributes."
|
|
744
|
-
},
|
|
745
|
-
{
|
|
746
|
-
date: "2023-08-14",
|
|
747
|
-
summary: "Added customer_id, user_name, user_email to Subscription invoice objects.",
|
|
748
|
-
handledBy: "SubscriptionInvoiceAttributes customer fields."
|
|
749
|
-
},
|
|
750
|
-
{
|
|
751
|
-
date: "2023-03-30",
|
|
752
|
-
summary: "Added receipt URL to Order objects.",
|
|
753
|
-
handledBy: "OrderUrls.receipt."
|
|
754
|
-
}
|
|
755
|
-
];
|
|
756
|
-
|
|
757
|
-
// src/validate/webhook.ts
|
|
758
|
-
async function validateWebhook(http, mode, options) {
|
|
759
|
-
const issues = [];
|
|
760
|
-
let webhooks;
|
|
761
|
-
try {
|
|
762
|
-
webhooks = await listWebhooksForStore(http, options.storeId);
|
|
763
|
-
} catch (err) {
|
|
764
|
-
if (err instanceof FreshSqueezyError) {
|
|
765
|
-
issues.push(
|
|
766
|
-
issue(ISSUE_CODES.UNKNOWN, "error", err.message, {
|
|
767
|
-
context: { status: err.status ?? null, code: err.code }
|
|
768
|
-
})
|
|
769
|
-
);
|
|
770
|
-
return buildResult("webhook", mode, issues, void 0, {
|
|
771
|
-
label: options.url,
|
|
772
|
-
url: options.url
|
|
773
|
-
});
|
|
774
|
-
}
|
|
775
|
-
const message = err instanceof Error ? err.message : "Unknown error";
|
|
776
|
-
issues.push(issue(ISSUE_CODES.UNKNOWN, "error", message));
|
|
777
|
-
return buildResult("webhook", mode, issues, void 0, {
|
|
778
|
-
label: options.url,
|
|
779
|
-
url: options.url
|
|
780
|
-
});
|
|
781
|
-
}
|
|
782
|
-
const match = webhooks.find((webhook) => sameWebhookUrl(webhook.attributes.url, options.url));
|
|
783
|
-
if (!match) {
|
|
784
|
-
issues.push(
|
|
785
|
-
issue(
|
|
786
|
-
ISSUE_CODES.WEBHOOK_NOT_FOUND,
|
|
787
|
-
"error",
|
|
788
|
-
`No webhook registered for URL ${options.url} on store ${options.storeId}.`,
|
|
789
|
-
{
|
|
790
|
-
suggestedFix: "Register the webhook in Lemon Squeezy (Settings \u2192 Webhooks) and subscribe to the recommended events.",
|
|
791
|
-
context: { storeId: String(options.storeId), url: options.url }
|
|
792
|
-
}
|
|
793
|
-
)
|
|
794
|
-
);
|
|
795
|
-
return buildResult("webhook", mode, issues, void 0, {
|
|
796
|
-
label: options.url,
|
|
797
|
-
url: options.url
|
|
798
|
-
});
|
|
799
|
-
}
|
|
800
|
-
const subscribed = new Set(match.attributes.events);
|
|
801
|
-
const missingRecommended = RECOMMENDED_WEBHOOK_EVENTS.filter((event) => !subscribed.has(event));
|
|
802
|
-
const missingOptional = OPTIONAL_WEBHOOK_EVENTS.filter((event) => !subscribed.has(event));
|
|
803
|
-
if (missingRecommended.length > 0) {
|
|
804
|
-
issues.push(
|
|
805
|
-
issue(
|
|
806
|
-
ISSUE_CODES.WEBHOOK_EVENTS_MISSING,
|
|
807
|
-
"error",
|
|
808
|
-
`Webhook is missing recommended events: ${missingRecommended.join(", ")}.`,
|
|
809
|
-
{
|
|
810
|
-
suggestedFix: "Subscribe to all recommended events so the integration survives plan changes and refunds.",
|
|
811
|
-
context: { missing: missingRecommended.join(",") }
|
|
812
|
-
}
|
|
813
|
-
)
|
|
814
|
-
);
|
|
815
|
-
}
|
|
816
|
-
if (missingOptional.length > 0) {
|
|
817
|
-
issues.push(
|
|
818
|
-
issue(
|
|
819
|
-
ISSUE_CODES.WEBHOOK_OPTIONAL_EVENTS,
|
|
820
|
-
"info",
|
|
821
|
-
`Optional events not subscribed: ${missingOptional.join(", ")}.`,
|
|
822
|
-
{ context: { missing: missingOptional.join(",") } }
|
|
823
|
-
)
|
|
824
|
-
);
|
|
825
|
-
}
|
|
826
|
-
return buildResult("webhook", mode, issues, match.attributes, {
|
|
827
|
-
label: match.attributes.url,
|
|
828
|
-
id: match.id,
|
|
829
|
-
url: match.attributes.url
|
|
830
|
-
});
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
// src/resources/discounts.ts
|
|
834
|
-
async function getDiscount(http, discountId) {
|
|
835
|
-
return http.getResource(`/v1/discounts/${discountId}`);
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
// src/validate/discount.ts
|
|
839
|
-
async function validateDiscount(http, mode, options) {
|
|
840
|
-
const issues = [];
|
|
841
|
-
const fetched = await probeFetch(() => getDiscount(http, options.discountId), {
|
|
842
|
-
notFoundCode: ISSUE_CODES.DISCOUNT_NOT_FOUND,
|
|
843
|
-
notFoundMessage: `Discount ${options.discountId} not found.`,
|
|
844
|
-
notFoundFix: "Verify the discount ID in the Lemon Squeezy dashboard.",
|
|
845
|
-
notFoundContext: { discountId: String(options.discountId) }
|
|
846
|
-
});
|
|
847
|
-
if (!fetched.ok) {
|
|
848
|
-
issues.push(fetched.issue);
|
|
849
|
-
return buildResult("discount", mode, issues, void 0, {
|
|
850
|
-
label: `discount ${options.discountId}`,
|
|
851
|
-
id: String(options.discountId)
|
|
852
|
-
});
|
|
853
|
-
}
|
|
854
|
-
const attrs = fetched.resource.attributes;
|
|
855
|
-
const mismatch = checkStoreOwnership({
|
|
856
|
-
expectedStoreId: options.storeId,
|
|
857
|
-
actualStoreId: attrs.store_id,
|
|
858
|
-
code: ISSUE_CODES.DISCOUNT_STORE_MISMATCH,
|
|
859
|
-
label: "Discount",
|
|
860
|
-
suggestedFix: "Use the correct store ID or discount ID \u2014 discounts should not cross stores."
|
|
861
|
-
});
|
|
862
|
-
if (mismatch) issues.push(mismatch);
|
|
863
|
-
if (attrs.status === "draft") {
|
|
864
|
-
issues.push(
|
|
865
|
-
issue(
|
|
866
|
-
ISSUE_CODES.DISCOUNT_DRAFT,
|
|
867
|
-
"warning",
|
|
868
|
-
`Discount "${attrs.name}" is in draft status \u2014 customers cannot redeem it.`,
|
|
869
|
-
{
|
|
870
|
-
suggestedFix: "Publish the discount in the Lemon Squeezy dashboard before sharing the code.",
|
|
871
|
-
context: { name: attrs.name, code: attrs.code }
|
|
872
|
-
}
|
|
873
|
-
)
|
|
874
|
-
);
|
|
875
|
-
}
|
|
876
|
-
const now = /* @__PURE__ */ new Date();
|
|
877
|
-
if (attrs.expires_at && new Date(attrs.expires_at) < now) {
|
|
878
|
-
issues.push(
|
|
879
|
-
issue(
|
|
880
|
-
ISSUE_CODES.DISCOUNT_EXPIRED,
|
|
881
|
-
"error",
|
|
882
|
-
`Discount "${attrs.name}" expired at ${attrs.expires_at}.`,
|
|
883
|
-
{
|
|
884
|
-
suggestedFix: "Extend the expiration date or create a new discount.",
|
|
885
|
-
context: { name: attrs.name, expiresAt: attrs.expires_at }
|
|
886
|
-
}
|
|
887
|
-
)
|
|
888
|
-
);
|
|
889
|
-
}
|
|
890
|
-
if (attrs.starts_at && new Date(attrs.starts_at) > now) {
|
|
891
|
-
issues.push(
|
|
892
|
-
issue(
|
|
893
|
-
ISSUE_CODES.DISCOUNT_NOT_STARTED,
|
|
894
|
-
"warning",
|
|
895
|
-
`Discount "${attrs.name}" starts at ${attrs.starts_at} \u2014 not yet active.`,
|
|
896
|
-
{
|
|
897
|
-
suggestedFix: "Wait for the start date or adjust it in the dashboard.",
|
|
898
|
-
context: { name: attrs.name, startsAt: attrs.starts_at }
|
|
899
|
-
}
|
|
900
|
-
)
|
|
901
|
-
);
|
|
902
|
-
}
|
|
903
|
-
if (attrs.is_limited_redemptions && attrs.max_redemptions <= 0) {
|
|
904
|
-
issues.push(
|
|
905
|
-
issue(
|
|
906
|
-
ISSUE_CODES.DISCOUNT_REDEMPTIONS_EXHAUSTED,
|
|
907
|
-
"warning",
|
|
908
|
-
`Discount "${attrs.name}" has limited redemptions with max_redemptions \u2264 0.`,
|
|
909
|
-
{
|
|
910
|
-
suggestedFix: "Increase max_redemptions or disable the redemption limit.",
|
|
911
|
-
context: { name: attrs.name, maxRedemptions: attrs.max_redemptions }
|
|
912
|
-
}
|
|
913
|
-
)
|
|
914
|
-
);
|
|
915
|
-
}
|
|
916
|
-
if (attrs.amount <= 0) {
|
|
917
|
-
issues.push(
|
|
918
|
-
issue(
|
|
919
|
-
ISSUE_CODES.DISCOUNT_INVALID_AMOUNT,
|
|
920
|
-
"error",
|
|
921
|
-
`Discount "${attrs.name}" has amount ${attrs.amount} \u2014 must be positive.`,
|
|
922
|
-
{
|
|
923
|
-
suggestedFix: "Set a positive discount amount in the dashboard.",
|
|
924
|
-
context: { name: attrs.name, amount: attrs.amount }
|
|
925
|
-
}
|
|
926
|
-
)
|
|
927
|
-
);
|
|
928
|
-
} else if (attrs.amount_type === "percent" && attrs.amount > 100) {
|
|
929
|
-
issues.push(
|
|
930
|
-
issue(
|
|
931
|
-
ISSUE_CODES.DISCOUNT_INVALID_AMOUNT,
|
|
932
|
-
"error",
|
|
933
|
-
`Discount "${attrs.name}" is ${attrs.amount}% \u2014 percent discounts cannot exceed 100%.`,
|
|
934
|
-
{
|
|
935
|
-
suggestedFix: "Set the discount to 100% or less.",
|
|
936
|
-
context: { name: attrs.name, amount: attrs.amount, amountType: attrs.amount_type }
|
|
937
|
-
}
|
|
938
|
-
)
|
|
939
|
-
);
|
|
1067
|
+
summary: "Added Generate subscription invoice endpoint and invoice_immediately / disable_prorations parameters.",
|
|
1068
|
+
handledBy: "SubscriptionItemUpdateAttributes.invoice_immediately / disable_prorations; invoice endpoint reachable via client.request()."
|
|
1069
|
+
},
|
|
1070
|
+
{
|
|
1071
|
+
date: "2024-02-05",
|
|
1072
|
+
summary: "Added tax_inclusive parameter to Order objects and Subscription invoice objects.",
|
|
1073
|
+
handledBy: "OrderAttributes.tax_inclusive, SubscriptionInvoiceAttributes.tax_inclusive."
|
|
1074
|
+
},
|
|
1075
|
+
{
|
|
1076
|
+
date: "2024-01-21",
|
|
1077
|
+
summary: "Added setup_fee_enabled / setup_fee to Price objects; setup_fee* to Subscription invoice.",
|
|
1078
|
+
handledBy: "PriceAttributes.setup_fee*, OrderAttributes.setup_fee*."
|
|
1079
|
+
},
|
|
1080
|
+
{
|
|
1081
|
+
date: "2024-01-15",
|
|
1082
|
+
summary: "Added unit_price_decimal to Price objects.",
|
|
1083
|
+
handledBy: "PriceAttributes.unit_price_decimal."
|
|
1084
|
+
},
|
|
1085
|
+
{
|
|
1086
|
+
date: "2024-01-05",
|
|
1087
|
+
summary: "Added test_mode flag to /v1/users/me meta.",
|
|
1088
|
+
handledBy: "Read in validateConnection to emit MODE_MISMATCH when the key's true mode differs from the caller's declared mode."
|
|
1089
|
+
},
|
|
1090
|
+
{
|
|
1091
|
+
date: "2023-09-19",
|
|
1092
|
+
summary: "Added customer_portal URLs to Subscription and Customer objects.",
|
|
1093
|
+
handledBy: "SubscriptionUrls.customer_portal and CustomerUrls.customer_portal."
|
|
1094
|
+
},
|
|
1095
|
+
{
|
|
1096
|
+
date: "2023-08-23",
|
|
1097
|
+
summary: "Added checkout_data.variant_quantities, subscription items, prices, and usage-based billing.",
|
|
1098
|
+
handledBy: "CheckoutData.variant_quantities, SubscriptionItemAttributes, UsageRecordAttributes, PriceAttributes."
|
|
1099
|
+
},
|
|
1100
|
+
{
|
|
1101
|
+
date: "2023-08-14",
|
|
1102
|
+
summary: "Added customer_id, user_name, user_email to Subscription invoice objects.",
|
|
1103
|
+
handledBy: "SubscriptionInvoiceAttributes customer fields."
|
|
1104
|
+
},
|
|
1105
|
+
{
|
|
1106
|
+
date: "2023-03-30",
|
|
1107
|
+
summary: "Added receipt URL to Order objects.",
|
|
1108
|
+
handledBy: "OrderUrls.receipt."
|
|
940
1109
|
}
|
|
941
|
-
|
|
942
|
-
label: `${attrs.name} (${attrs.code})`,
|
|
943
|
-
id: String(options.discountId)
|
|
944
|
-
});
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
// src/resources/licenseKeys.ts
|
|
948
|
-
async function getLicenseKey(http, licenseKeyId) {
|
|
949
|
-
return http.getResource(`/v1/license-keys/${licenseKeyId}`);
|
|
950
|
-
}
|
|
1110
|
+
];
|
|
951
1111
|
|
|
952
|
-
// src/validate/
|
|
953
|
-
async function
|
|
1112
|
+
// src/validate/webhook.ts
|
|
1113
|
+
async function validateWebhook(http, mode, options) {
|
|
954
1114
|
const issues = [];
|
|
955
|
-
const fetched = await
|
|
956
|
-
notFoundCode: ISSUE_CODES.LICENSE_KEY_NOT_FOUND,
|
|
957
|
-
notFoundMessage: `License key ${options.licenseKeyId} not found.`,
|
|
958
|
-
notFoundFix: "Verify the license key ID in the Lemon Squeezy dashboard.",
|
|
959
|
-
notFoundContext: { licenseKeyId: String(options.licenseKeyId) }
|
|
960
|
-
});
|
|
1115
|
+
const fetched = await probeCollection(() => listWebhooksForStore(http, options.storeId));
|
|
961
1116
|
if (!fetched.ok) {
|
|
962
1117
|
issues.push(fetched.issue);
|
|
963
|
-
return buildResult("
|
|
964
|
-
label:
|
|
965
|
-
|
|
1118
|
+
return buildResult("webhook", mode, issues, void 0, {
|
|
1119
|
+
label: options.url,
|
|
1120
|
+
url: options.url
|
|
966
1121
|
});
|
|
967
1122
|
}
|
|
968
|
-
const
|
|
969
|
-
const
|
|
970
|
-
|
|
971
|
-
actualStoreId: attrs.store_id,
|
|
972
|
-
code: ISSUE_CODES.LICENSE_KEY_STORE_MISMATCH,
|
|
973
|
-
label: "License key",
|
|
974
|
-
suggestedFix: "Use the correct store ID or license key ID \u2014 keys should not cross stores."
|
|
975
|
-
});
|
|
976
|
-
if (mismatch) issues.push(mismatch);
|
|
977
|
-
if (attrs.disabled) {
|
|
978
|
-
issues.push(
|
|
979
|
-
issue(
|
|
980
|
-
ISSUE_CODES.LICENSE_KEY_DISABLED,
|
|
981
|
-
"error",
|
|
982
|
-
`License key ${attrs.key_short} is disabled.`,
|
|
983
|
-
{
|
|
984
|
-
suggestedFix: "Re-enable the license key in the Lemon Squeezy dashboard.",
|
|
985
|
-
context: { keyShort: attrs.key_short }
|
|
986
|
-
}
|
|
987
|
-
)
|
|
988
|
-
);
|
|
989
|
-
}
|
|
990
|
-
if (attrs.expires_at && new Date(attrs.expires_at) < /* @__PURE__ */ new Date()) {
|
|
1123
|
+
const webhooks = fetched.resource;
|
|
1124
|
+
const match = webhooks.find((webhook) => sameWebhookUrl(webhook.attributes.url, options.url));
|
|
1125
|
+
if (!match) {
|
|
991
1126
|
issues.push(
|
|
992
1127
|
issue(
|
|
993
|
-
ISSUE_CODES.
|
|
1128
|
+
ISSUE_CODES.WEBHOOK_NOT_FOUND,
|
|
994
1129
|
"error",
|
|
995
|
-
`
|
|
996
|
-
{
|
|
997
|
-
suggestedFix: "Extend the expiration date or issue a new license key.",
|
|
998
|
-
context: { keyShort: attrs.key_short, expiresAt: attrs.expires_at }
|
|
999
|
-
}
|
|
1000
|
-
)
|
|
1001
|
-
);
|
|
1002
|
-
}
|
|
1003
|
-
if (attrs.activation_limit !== null && attrs.instances_count >= attrs.activation_limit) {
|
|
1004
|
-
issues.push(
|
|
1005
|
-
issue(
|
|
1006
|
-
ISSUE_CODES.LICENSE_KEY_AT_ACTIVATION_LIMIT,
|
|
1007
|
-
"warning",
|
|
1008
|
-
`License key ${attrs.key_short} has reached its activation limit (${attrs.instances_count}/${attrs.activation_limit}).`,
|
|
1130
|
+
`No webhook registered for URL ${options.url} on store ${options.storeId}.`,
|
|
1009
1131
|
{
|
|
1010
|
-
suggestedFix: "
|
|
1011
|
-
context: {
|
|
1012
|
-
keyShort: attrs.key_short,
|
|
1013
|
-
instancesCount: attrs.instances_count,
|
|
1014
|
-
activationLimit: attrs.activation_limit
|
|
1015
|
-
}
|
|
1132
|
+
suggestedFix: "Register the webhook in Lemon Squeezy (Settings \u2192 Webhooks) and subscribe to the recommended events.",
|
|
1133
|
+
context: { storeId: String(options.storeId), url: options.url }
|
|
1016
1134
|
}
|
|
1017
1135
|
)
|
|
1018
1136
|
);
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
id: String(options.licenseKeyId)
|
|
1023
|
-
});
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
// src/validate/subscriptionPlan.ts
|
|
1027
|
-
var VALID_INTERVALS = /* @__PURE__ */ new Set(["day", "week", "month", "year"]);
|
|
1028
|
-
async function validateSubscriptionPlan(http, mode, options) {
|
|
1029
|
-
const issues = [];
|
|
1030
|
-
const fetched = await probeFetch(
|
|
1031
|
-
() => getVariant(http, options.variantId),
|
|
1032
|
-
{
|
|
1033
|
-
notFoundCode: ISSUE_CODES.PLAN_VARIANT_NOT_FOUND,
|
|
1034
|
-
notFoundMessage: `Variant ${options.variantId} not found.`,
|
|
1035
|
-
notFoundFix: "Verify the variant ID in the Lemon Squeezy dashboard.",
|
|
1036
|
-
notFoundContext: { variantId: String(options.variantId) }
|
|
1037
|
-
}
|
|
1038
|
-
);
|
|
1039
|
-
if (!fetched.ok) {
|
|
1040
|
-
issues.push(fetched.issue);
|
|
1041
|
-
return buildResult("subscriptionPlan", mode, issues, void 0, {
|
|
1042
|
-
label: `variant ${options.variantId}`,
|
|
1043
|
-
id: String(options.variantId)
|
|
1137
|
+
return buildResult("webhook", mode, issues, void 0, {
|
|
1138
|
+
label: options.url,
|
|
1139
|
+
url: options.url
|
|
1044
1140
|
});
|
|
1045
1141
|
}
|
|
1046
|
-
const
|
|
1047
|
-
const
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
issue(
|
|
1051
|
-
ISSUE_CODES.PLAN_NOT_SUBSCRIPTION,
|
|
1052
|
-
"error",
|
|
1053
|
-
`Variant ${options.variantId} is not a subscription variant (is_subscription is false).`,
|
|
1054
|
-
{
|
|
1055
|
-
suggestedFix: "Use a variant that has subscription billing enabled, or use the regular variant validator.",
|
|
1056
|
-
context: { variantId: String(options.variantId) }
|
|
1057
|
-
}
|
|
1058
|
-
)
|
|
1059
|
-
);
|
|
1060
|
-
}
|
|
1061
|
-
if (!attrs.interval || !VALID_INTERVALS.has(attrs.interval)) {
|
|
1062
|
-
issues.push(
|
|
1063
|
-
issue(
|
|
1064
|
-
ISSUE_CODES.PLAN_INVALID_INTERVAL,
|
|
1065
|
-
"error",
|
|
1066
|
-
`Subscription variant has invalid interval: "${attrs.interval ?? "missing"}". Expected one of: day, week, month, year.`,
|
|
1067
|
-
{
|
|
1068
|
-
suggestedFix: "Set a valid billing interval in the variant configuration.",
|
|
1069
|
-
context: { interval: attrs.interval ?? null }
|
|
1070
|
-
}
|
|
1071
|
-
)
|
|
1072
|
-
);
|
|
1073
|
-
}
|
|
1074
|
-
if (attrs.interval_count === null || attrs.interval_count <= 0) {
|
|
1142
|
+
const subscribed = new Set(match.attributes.events);
|
|
1143
|
+
const missingRecommended = RECOMMENDED_WEBHOOK_EVENTS.filter((event) => !subscribed.has(event));
|
|
1144
|
+
const missingOptional = OPTIONAL_WEBHOOK_EVENTS.filter((event) => !subscribed.has(event));
|
|
1145
|
+
if (missingRecommended.length > 0) {
|
|
1075
1146
|
issues.push(
|
|
1076
1147
|
issue(
|
|
1077
|
-
ISSUE_CODES.
|
|
1148
|
+
ISSUE_CODES.WEBHOOK_EVENTS_MISSING,
|
|
1078
1149
|
"error",
|
|
1079
|
-
`
|
|
1080
|
-
{
|
|
1081
|
-
suggestedFix: "Set interval_count to a positive value (e.g. 1 for monthly, 2 for biweekly).",
|
|
1082
|
-
context: { intervalCount: attrs.interval_count }
|
|
1083
|
-
}
|
|
1084
|
-
)
|
|
1085
|
-
);
|
|
1086
|
-
}
|
|
1087
|
-
if (attrs.price === 0 && attrs.is_subscription) {
|
|
1088
|
-
issues.push(
|
|
1089
|
-
issue(
|
|
1090
|
-
ISSUE_CODES.PLAN_FREE_PRICE,
|
|
1091
|
-
"warning",
|
|
1092
|
-
`Subscription variant has a price of 0 \u2014 this is almost always a misconfiguration for paid plans.`,
|
|
1093
|
-
{
|
|
1094
|
-
suggestedFix: "Set the variant price to the intended amount in cents, or confirm this is intentionally free.",
|
|
1095
|
-
context: { price: attrs.price }
|
|
1096
|
-
}
|
|
1097
|
-
)
|
|
1098
|
-
);
|
|
1099
|
-
}
|
|
1100
|
-
if (attrs.has_free_trial && (!attrs.trial_interval || (attrs.trial_interval_count ?? 0) <= 0)) {
|
|
1101
|
-
issues.push(
|
|
1102
|
-
issue(
|
|
1103
|
-
ISSUE_CODES.PLAN_TRIAL_INCONSISTENT,
|
|
1104
|
-
"warning",
|
|
1105
|
-
`Subscription variant has free trial enabled but trial interval is misconfigured (interval: "${attrs.trial_interval ?? "missing"}", count: ${attrs.trial_interval_count ?? 0}).`,
|
|
1150
|
+
`Webhook is missing recommended events: ${missingRecommended.join(", ")}.`,
|
|
1106
1151
|
{
|
|
1107
|
-
suggestedFix: "
|
|
1108
|
-
context: {
|
|
1109
|
-
trialInterval: attrs.trial_interval ?? null,
|
|
1110
|
-
trialIntervalCount: attrs.trial_interval_count ?? null
|
|
1111
|
-
}
|
|
1152
|
+
suggestedFix: "Subscribe to all recommended events so the integration survives plan changes and refunds.",
|
|
1153
|
+
context: { missing: missingRecommended.join(",") }
|
|
1112
1154
|
}
|
|
1113
1155
|
)
|
|
1114
1156
|
);
|
|
1115
1157
|
}
|
|
1116
|
-
if (
|
|
1158
|
+
if (missingOptional.length > 0) {
|
|
1117
1159
|
issues.push(
|
|
1118
1160
|
issue(
|
|
1119
|
-
ISSUE_CODES.
|
|
1120
|
-
"
|
|
1121
|
-
`
|
|
1122
|
-
{
|
|
1123
|
-
suggestedFix: "Publish the variant in the Lemon Squeezy dashboard.",
|
|
1124
|
-
context: { status: attrs.status }
|
|
1125
|
-
}
|
|
1161
|
+
ISSUE_CODES.WEBHOOK_OPTIONAL_EVENTS,
|
|
1162
|
+
"info",
|
|
1163
|
+
`Optional events not subscribed: ${missingOptional.join(", ")}.`,
|
|
1164
|
+
{ context: { missing: missingOptional.join(",") } }
|
|
1126
1165
|
)
|
|
1127
1166
|
);
|
|
1128
1167
|
}
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
actualStoreId: product.attributes.store_id,
|
|
1134
|
-
code: ISSUE_CODES.PLAN_STORE_MISMATCH,
|
|
1135
|
-
label: `Subscription variant (via product ${attrs.product_id})`,
|
|
1136
|
-
suggestedFix: "Use the correct store ID or variant ID \u2014 plans should not cross stores.",
|
|
1137
|
-
extraContext: { productId: String(attrs.product_id) }
|
|
1138
|
-
});
|
|
1139
|
-
if (mismatch) issues.push(mismatch);
|
|
1140
|
-
} catch {
|
|
1141
|
-
}
|
|
1142
|
-
const summary = {
|
|
1143
|
-
variantId: variant.id,
|
|
1144
|
-
interval: attrs.interval,
|
|
1145
|
-
intervalCount: attrs.interval_count,
|
|
1146
|
-
price: attrs.price,
|
|
1147
|
-
hasFreeTrial: attrs.has_free_trial,
|
|
1148
|
-
status: attrs.status
|
|
1149
|
-
};
|
|
1150
|
-
return buildResult("subscriptionPlan", mode, issues, summary, {
|
|
1151
|
-
label: attrs.name,
|
|
1152
|
-
id: String(options.variantId)
|
|
1168
|
+
return buildResult("webhook", mode, issues, match.attributes, {
|
|
1169
|
+
label: match.attributes.url,
|
|
1170
|
+
id: match.id,
|
|
1171
|
+
url: match.attributes.url
|
|
1153
1172
|
});
|
|
1154
1173
|
}
|
|
1155
1174
|
|
|
@@ -1430,6 +1449,10 @@ async function listUsageRecordsForSubscriptionItem(http, subscriptionItemId) {
|
|
|
1430
1449
|
SUPPORTED_RESOURCES,
|
|
1431
1450
|
buildResult,
|
|
1432
1451
|
buildSubscriptionItemUpdateBody,
|
|
1452
|
+
checkDiscount,
|
|
1453
|
+
checkLicenseKey,
|
|
1454
|
+
checkProduct,
|
|
1455
|
+
checkSubscriptionPlan,
|
|
1433
1456
|
createFreshSqueezy,
|
|
1434
1457
|
doctor,
|
|
1435
1458
|
fetchActualMode,
|
|
@@ -1471,7 +1494,11 @@ async function listUsageRecordsForSubscriptionItem(http, subscriptionItemId) {
|
|
|
1471
1494
|
sameWebhookUrl,
|
|
1472
1495
|
userResource,
|
|
1473
1496
|
validateConnection,
|
|
1497
|
+
validateDiscount,
|
|
1498
|
+
validateLicenseKey,
|
|
1474
1499
|
validateProduct,
|
|
1500
|
+
validateStore,
|
|
1501
|
+
validateSubscriptionPlan,
|
|
1475
1502
|
validateWebhook
|
|
1476
1503
|
});
|
|
1477
1504
|
//# sourceMappingURL=index.cjs.map
|