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/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
- try {
398
- const userDoc = await getAuthenticatedUser(http);
399
- const stores = await listStores(http);
400
- const actualMode = resolveActualMode(userDoc.meta?.test_mode);
401
- const summary = {
402
- user: userDoc.data.attributes,
403
- storeCount: stores.length,
404
- storeIds: stores.map((store) => store.id),
405
- declaredMode: mode,
406
- ...actualMode ? { actualMode } : {}
407
- };
408
- if (actualMode && actualMode !== mode) {
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
- return buildResult("connection", mode, issues, summary);
432
- } catch (err) {
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
- mapErrorToIssue(err, {
435
- unauthorized: {
436
- code: ISSUE_CODES.AUTH_FAILED,
437
- message: "API key rejected by Lemon Squeezy.",
438
- suggestedFix: "Regenerate the key at https://app.lemonsqueezy.com/settings/api."
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
- // src/validate/store.ts
447
- async function validateStore(http, mode, storeId) {
448
- const issues = [];
449
- const fetched = await probeFetch(() => getStore(http, storeId), {
450
- notFoundCode: ISSUE_CODES.STORE_NOT_FOUND,
451
- notFoundMessage: `Store ${storeId} not found with the current API key.`,
452
- notFoundFix: "Check the store ID and confirm the API key belongs to the account that owns it.",
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("store", mode, issues, fetched.resource.attributes, {
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 listProducts(http, storeId) {
473
- return http.paginate("/v1/products", {
474
- "filter[store_id]": String(storeId)
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/variants.ts
479
- async function getVariant(http, variantId) {
480
- return http.getResource(`/v1/variants/${variantId}`);
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/product.ts
489
- async function validateProduct(http, mode, options) {
463
+ // src/validate/discount.ts
464
+ async function validateDiscount(http, mode, options) {
490
465
  const issues = [];
491
- const fetched = await probeFetch(() => getProduct(http, options.productId), {
492
- notFoundCode: ISSUE_CODES.PRODUCT_NOT_FOUND,
493
- notFoundMessage: `Product ${options.productId} not found.`,
494
- notFoundFix: "Verify the product ID in the Lemon Squeezy dashboard.",
495
- notFoundContext: { productId: String(options.productId) }
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("product", mode, issues, void 0, {
500
- label: `product ${options.productId}`,
501
- id: String(options.productId)
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
- if (options.expectedStoreId !== void 0) {
506
- const mismatch = checkStoreOwnership({
507
- expectedStoreId: options.expectedStoreId,
508
- actualStoreId: attrs.store_id,
509
- code: ISSUE_CODES.PRODUCT_WRONG_STORE,
510
- label: "Product",
511
- suggestedFix: "Either use the correct store ID or the correct product ID \u2014 IDs should not cross stores."
512
- });
513
- if (mismatch) issues.push(mismatch);
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
- if (attrs.status !== "published") {
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.PRODUCT_UNPUBLISHED,
513
+ ISSUE_CODES.DISCOUNT_EXPIRED,
519
514
  "error",
520
- `Product is in "${attrs.status}" state, not "published".`,
515
+ `Discount "${attrs.name}" expired at ${attrs.expires_at}.`,
521
516
  {
522
- suggestedFix: "Publish the product in the Lemon Squeezy dashboard before selling.",
523
- context: { status: attrs.status }
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 (!attrs.buy_now_url) {
523
+ if (attrs.starts_at && new Date(attrs.starts_at) > now) {
529
524
  issues.push(
530
525
  issue(
531
- ISSUE_CODES.PRODUCT_NO_BUY_URL,
526
+ ISSUE_CODES.DISCOUNT_NOT_STARTED,
532
527
  "warning",
533
- "Product has no buy-now URL. Hosted checkout may be disabled.",
534
- { suggestedFix: "Enable buy-now in product settings, or use a custom checkout flow." }
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
- try {
539
- const variants = await listVariantsForProduct(http, options.productId);
540
- if (variants.length === 0) {
541
- issues.push(
542
- issue(
543
- ISSUE_CODES.VARIANT_MISSING,
544
- "error",
545
- "Product has no variants. Customers cannot purchase it.",
546
- { suggestedFix: "Add at least one variant in the product configuration." }
547
- )
548
- );
549
- } else if (!variants.some((variant) => variant.attributes.status === "published")) {
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
- return buildResult("product", mode, issues, attrs, {
564
- label: attrs.name,
565
- id: String(options.productId)
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/core/equality.ts
570
- function sameWebhookUrl(a, b) {
571
- return normalizeWebhookUrl(a) === normalizeWebhookUrl(b);
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/resources/webhooks.ts
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
- return buildResult("discount", mode, issues, attrs, {
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/licenseKey.ts
953
- async function validateLicenseKey(http, mode, options) {
1112
+ // src/validate/webhook.ts
1113
+ async function validateWebhook(http, mode, options) {
954
1114
  const issues = [];
955
- const fetched = await probeFetch(() => getLicenseKey(http, options.licenseKeyId), {
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("licenseKey", mode, issues, void 0, {
964
- label: `license key ${options.licenseKeyId}`,
965
- id: String(options.licenseKeyId)
1118
+ return buildResult("webhook", mode, issues, void 0, {
1119
+ label: options.url,
1120
+ url: options.url
966
1121
  });
967
1122
  }
968
- const attrs = fetched.resource.attributes;
969
- const mismatch = checkStoreOwnership({
970
- expectedStoreId: options.storeId,
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.LICENSE_KEY_EXPIRED,
1128
+ ISSUE_CODES.WEBHOOK_NOT_FOUND,
994
1129
  "error",
995
- `License key ${attrs.key_short} expired at ${attrs.expires_at}.`,
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: "Increase the activation limit or deactivate unused instances.",
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
- return buildResult("licenseKey", mode, issues, attrs, {
1021
- label: attrs.key_short,
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 variant = fetched.resource;
1047
- const attrs = variant.attributes;
1048
- if (!attrs.is_subscription) {
1049
- issues.push(
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.PLAN_INVALID_INTERVAL,
1148
+ ISSUE_CODES.WEBHOOK_EVENTS_MISSING,
1078
1149
  "error",
1079
- `Subscription variant has invalid interval_count: ${attrs.interval_count}. Must be a positive integer.`,
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: "Set a valid trial interval and count, or disable the free trial.",
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 (attrs.status === "draft") {
1158
+ if (missingOptional.length > 0) {
1117
1159
  issues.push(
1118
1160
  issue(
1119
- ISSUE_CODES.PLAN_DRAFT,
1120
- "warning",
1121
- `Subscription variant is in draft status \u2014 customers cannot subscribe.`,
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
- try {
1130
- const product = await getProduct(http, attrs.product_id);
1131
- const mismatch = checkStoreOwnership({
1132
- expectedStoreId: options.storeId,
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