fresh-squeezy 0.1.2 → 0.1.4

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.js CHANGED
@@ -164,6 +164,25 @@ var ISSUE_CODES = {
164
164
  WEBHOOK_NOT_FOUND: "WEBHOOK_NOT_FOUND",
165
165
  WEBHOOK_EVENTS_MISSING: "WEBHOOK_EVENTS_MISSING",
166
166
  WEBHOOK_OPTIONAL_EVENTS: "WEBHOOK_OPTIONAL_EVENTS",
167
+ DISCOUNT_NOT_FOUND: "DISCOUNT_NOT_FOUND",
168
+ DISCOUNT_DRAFT: "DISCOUNT_DRAFT",
169
+ DISCOUNT_EXPIRED: "DISCOUNT_EXPIRED",
170
+ DISCOUNT_NOT_STARTED: "DISCOUNT_NOT_STARTED",
171
+ DISCOUNT_REDEMPTIONS_EXHAUSTED: "DISCOUNT_REDEMPTIONS_EXHAUSTED",
172
+ DISCOUNT_INVALID_AMOUNT: "DISCOUNT_INVALID_AMOUNT",
173
+ DISCOUNT_STORE_MISMATCH: "DISCOUNT_STORE_MISMATCH",
174
+ LICENSE_KEY_NOT_FOUND: "LICENSE_KEY_NOT_FOUND",
175
+ LICENSE_KEY_DISABLED: "LICENSE_KEY_DISABLED",
176
+ LICENSE_KEY_EXPIRED: "LICENSE_KEY_EXPIRED",
177
+ LICENSE_KEY_AT_ACTIVATION_LIMIT: "LICENSE_KEY_AT_ACTIVATION_LIMIT",
178
+ LICENSE_KEY_STORE_MISMATCH: "LICENSE_KEY_STORE_MISMATCH",
179
+ PLAN_VARIANT_NOT_FOUND: "PLAN_VARIANT_NOT_FOUND",
180
+ PLAN_NOT_SUBSCRIPTION: "PLAN_NOT_SUBSCRIPTION",
181
+ PLAN_INVALID_INTERVAL: "PLAN_INVALID_INTERVAL",
182
+ PLAN_FREE_PRICE: "PLAN_FREE_PRICE",
183
+ PLAN_TRIAL_INCONSISTENT: "PLAN_TRIAL_INCONSISTENT",
184
+ PLAN_DRAFT: "PLAN_DRAFT",
185
+ PLAN_STORE_MISMATCH: "PLAN_STORE_MISMATCH",
167
186
  NETWORK_ERROR: "NETWORK_ERROR",
168
187
  UNKNOWN: "UNKNOWN"
169
188
  };
@@ -300,6 +319,9 @@ async function listProducts(http, storeId) {
300
319
  }
301
320
 
302
321
  // src/resources/variants.ts
322
+ async function getVariant(http, variantId) {
323
+ return http.getResource(`/v1/variants/${variantId}`);
324
+ }
303
325
  async function listVariantsForProduct(http, productId) {
304
326
  return http.getCollection("/v1/variants", {
305
327
  "filter[product_id]": String(productId)
@@ -521,6 +543,358 @@ function normalizeUrl(raw) {
521
543
  return raw.replace(/\/+$/, "").toLowerCase();
522
544
  }
523
545
 
546
+ // src/resources/discounts.ts
547
+ async function getDiscount(http, discountId) {
548
+ return http.getResource(`/v1/discounts/${discountId}`);
549
+ }
550
+
551
+ // src/validate/discount.ts
552
+ async function validateDiscount(http, mode, options) {
553
+ const issues = [];
554
+ let discount;
555
+ try {
556
+ discount = await getDiscount(http, options.discountId);
557
+ } catch (err) {
558
+ if (err instanceof FreshSqueezyError && err.status === 404) {
559
+ issues.push(
560
+ issue(ISSUE_CODES.DISCOUNT_NOT_FOUND, "error", `Discount ${options.discountId} not found.`, {
561
+ suggestedFix: "Verify the discount ID in the Lemon Squeezy dashboard.",
562
+ context: { discountId: String(options.discountId) }
563
+ })
564
+ );
565
+ return buildResult("discount", mode, issues);
566
+ }
567
+ const message = err instanceof Error ? err.message : "Unknown error";
568
+ issues.push(issue(ISSUE_CODES.UNKNOWN, "error", message));
569
+ return buildResult("discount", mode, issues);
570
+ }
571
+ const attrs = discount.attributes;
572
+ const expectedStore = String(options.storeId);
573
+ const actualStore = String(attrs.store_id);
574
+ if (expectedStore !== actualStore) {
575
+ issues.push(
576
+ issue(
577
+ ISSUE_CODES.DISCOUNT_STORE_MISMATCH,
578
+ "error",
579
+ `Discount belongs to store ${actualStore}, expected ${expectedStore}.`,
580
+ {
581
+ suggestedFix: "Use the correct store ID or discount ID \u2014 discounts should not cross stores.",
582
+ context: { expectedStoreId: expectedStore, actualStoreId: actualStore }
583
+ }
584
+ )
585
+ );
586
+ }
587
+ if (attrs.status === "draft") {
588
+ issues.push(
589
+ issue(
590
+ ISSUE_CODES.DISCOUNT_DRAFT,
591
+ "warning",
592
+ `Discount "${attrs.name}" is in draft status \u2014 customers cannot redeem it.`,
593
+ {
594
+ suggestedFix: "Publish the discount in the Lemon Squeezy dashboard before sharing the code.",
595
+ context: { name: attrs.name, code: attrs.code }
596
+ }
597
+ )
598
+ );
599
+ }
600
+ const now = /* @__PURE__ */ new Date();
601
+ if (attrs.expires_at && new Date(attrs.expires_at) < now) {
602
+ issues.push(
603
+ issue(
604
+ ISSUE_CODES.DISCOUNT_EXPIRED,
605
+ "error",
606
+ `Discount "${attrs.name}" expired at ${attrs.expires_at}.`,
607
+ {
608
+ suggestedFix: "Extend the expiration date or create a new discount.",
609
+ context: { name: attrs.name, expiresAt: attrs.expires_at }
610
+ }
611
+ )
612
+ );
613
+ }
614
+ if (attrs.starts_at && new Date(attrs.starts_at) > now) {
615
+ issues.push(
616
+ issue(
617
+ ISSUE_CODES.DISCOUNT_NOT_STARTED,
618
+ "warning",
619
+ `Discount "${attrs.name}" starts at ${attrs.starts_at} \u2014 not yet active.`,
620
+ {
621
+ suggestedFix: "Wait for the start date or adjust it in the dashboard.",
622
+ context: { name: attrs.name, startsAt: attrs.starts_at }
623
+ }
624
+ )
625
+ );
626
+ }
627
+ if (attrs.is_limited_redemptions && attrs.max_redemptions <= 0) {
628
+ issues.push(
629
+ issue(
630
+ ISSUE_CODES.DISCOUNT_REDEMPTIONS_EXHAUSTED,
631
+ "warning",
632
+ `Discount "${attrs.name}" has limited redemptions with max_redemptions \u2264 0.`,
633
+ {
634
+ suggestedFix: "Increase max_redemptions or disable the redemption limit.",
635
+ context: { name: attrs.name, maxRedemptions: attrs.max_redemptions }
636
+ }
637
+ )
638
+ );
639
+ }
640
+ if (attrs.amount <= 0) {
641
+ issues.push(
642
+ issue(
643
+ ISSUE_CODES.DISCOUNT_INVALID_AMOUNT,
644
+ "error",
645
+ `Discount "${attrs.name}" has amount ${attrs.amount} \u2014 must be positive.`,
646
+ {
647
+ suggestedFix: "Set a positive discount amount in the dashboard.",
648
+ context: { name: attrs.name, amount: attrs.amount }
649
+ }
650
+ )
651
+ );
652
+ } else if (attrs.amount_type === "percent" && attrs.amount > 100) {
653
+ issues.push(
654
+ issue(
655
+ ISSUE_CODES.DISCOUNT_INVALID_AMOUNT,
656
+ "error",
657
+ `Discount "${attrs.name}" is ${attrs.amount}% \u2014 percent discounts cannot exceed 100%.`,
658
+ {
659
+ suggestedFix: "Set the discount to 100% or less.",
660
+ context: { name: attrs.name, amount: attrs.amount, amountType: attrs.amount_type }
661
+ }
662
+ )
663
+ );
664
+ }
665
+ return buildResult("discount", mode, issues, attrs);
666
+ }
667
+
668
+ // src/resources/licenseKeys.ts
669
+ async function getLicenseKey(http, licenseKeyId) {
670
+ return http.getResource(`/v1/license-keys/${licenseKeyId}`);
671
+ }
672
+
673
+ // src/validate/licenseKey.ts
674
+ async function validateLicenseKey(http, mode, options) {
675
+ const issues = [];
676
+ let licenseKey;
677
+ try {
678
+ licenseKey = await getLicenseKey(http, options.licenseKeyId);
679
+ } catch (err) {
680
+ if (err instanceof FreshSqueezyError && err.status === 404) {
681
+ issues.push(
682
+ issue(
683
+ ISSUE_CODES.LICENSE_KEY_NOT_FOUND,
684
+ "error",
685
+ `License key ${options.licenseKeyId} not found.`,
686
+ {
687
+ suggestedFix: "Verify the license key ID in the Lemon Squeezy dashboard.",
688
+ context: { licenseKeyId: String(options.licenseKeyId) }
689
+ }
690
+ )
691
+ );
692
+ return buildResult("licenseKey", mode, issues);
693
+ }
694
+ const message = err instanceof Error ? err.message : "Unknown error";
695
+ issues.push(issue(ISSUE_CODES.UNKNOWN, "error", message));
696
+ return buildResult("licenseKey", mode, issues);
697
+ }
698
+ const attrs = licenseKey.attributes;
699
+ const expectedStore = String(options.storeId);
700
+ const actualStore = String(attrs.store_id);
701
+ if (expectedStore !== actualStore) {
702
+ issues.push(
703
+ issue(
704
+ ISSUE_CODES.LICENSE_KEY_STORE_MISMATCH,
705
+ "error",
706
+ `License key belongs to store ${actualStore}, expected ${expectedStore}.`,
707
+ {
708
+ suggestedFix: "Use the correct store ID or license key ID \u2014 keys should not cross stores.",
709
+ context: { expectedStoreId: expectedStore, actualStoreId: actualStore }
710
+ }
711
+ )
712
+ );
713
+ }
714
+ if (attrs.disabled) {
715
+ issues.push(
716
+ issue(
717
+ ISSUE_CODES.LICENSE_KEY_DISABLED,
718
+ "error",
719
+ `License key ${attrs.key_short} is disabled.`,
720
+ {
721
+ suggestedFix: "Re-enable the license key in the Lemon Squeezy dashboard.",
722
+ context: { keyShort: attrs.key_short }
723
+ }
724
+ )
725
+ );
726
+ }
727
+ if (attrs.expires_at && new Date(attrs.expires_at) < /* @__PURE__ */ new Date()) {
728
+ issues.push(
729
+ issue(
730
+ ISSUE_CODES.LICENSE_KEY_EXPIRED,
731
+ "error",
732
+ `License key ${attrs.key_short} expired at ${attrs.expires_at}.`,
733
+ {
734
+ suggestedFix: "Extend the expiration date or issue a new license key.",
735
+ context: { keyShort: attrs.key_short, expiresAt: attrs.expires_at }
736
+ }
737
+ )
738
+ );
739
+ }
740
+ if (attrs.activation_limit !== null && attrs.instances_count >= attrs.activation_limit) {
741
+ issues.push(
742
+ issue(
743
+ ISSUE_CODES.LICENSE_KEY_AT_ACTIVATION_LIMIT,
744
+ "warning",
745
+ `License key ${attrs.key_short} has reached its activation limit (${attrs.instances_count}/${attrs.activation_limit}).`,
746
+ {
747
+ suggestedFix: "Increase the activation limit or deactivate unused instances.",
748
+ context: {
749
+ keyShort: attrs.key_short,
750
+ instancesCount: attrs.instances_count,
751
+ activationLimit: attrs.activation_limit
752
+ }
753
+ }
754
+ )
755
+ );
756
+ }
757
+ return buildResult("licenseKey", mode, issues, attrs);
758
+ }
759
+
760
+ // src/validate/subscriptionPlan.ts
761
+ var VALID_INTERVALS = /* @__PURE__ */ new Set(["day", "week", "month", "year"]);
762
+ async function validateSubscriptionPlan(http, mode, options) {
763
+ const issues = [];
764
+ let variant;
765
+ try {
766
+ variant = await getVariant(http, options.variantId);
767
+ } catch (err) {
768
+ if (err instanceof FreshSqueezyError && err.status === 404) {
769
+ issues.push(
770
+ issue(
771
+ ISSUE_CODES.PLAN_VARIANT_NOT_FOUND,
772
+ "error",
773
+ `Variant ${options.variantId} not found.`,
774
+ {
775
+ suggestedFix: "Verify the variant ID in the Lemon Squeezy dashboard.",
776
+ context: { variantId: String(options.variantId) }
777
+ }
778
+ )
779
+ );
780
+ return buildResult("subscriptionPlan", mode, issues);
781
+ }
782
+ const message = err instanceof Error ? err.message : "Unknown error";
783
+ issues.push(issue(ISSUE_CODES.UNKNOWN, "error", message));
784
+ return buildResult("subscriptionPlan", mode, issues);
785
+ }
786
+ const attrs = variant.attributes;
787
+ if (!attrs.is_subscription) {
788
+ issues.push(
789
+ issue(
790
+ ISSUE_CODES.PLAN_NOT_SUBSCRIPTION,
791
+ "error",
792
+ `Variant ${options.variantId} is not a subscription variant (is_subscription is false).`,
793
+ {
794
+ suggestedFix: "Use a variant that has subscription billing enabled, or use the regular variant validator.",
795
+ context: { variantId: String(options.variantId) }
796
+ }
797
+ )
798
+ );
799
+ }
800
+ if (!attrs.interval || !VALID_INTERVALS.has(attrs.interval)) {
801
+ issues.push(
802
+ issue(
803
+ ISSUE_CODES.PLAN_INVALID_INTERVAL,
804
+ "error",
805
+ `Subscription variant has invalid interval: "${attrs.interval ?? "missing"}". Expected one of: day, week, month, year.`,
806
+ {
807
+ suggestedFix: "Set a valid billing interval in the variant configuration.",
808
+ context: { interval: attrs.interval ?? null }
809
+ }
810
+ )
811
+ );
812
+ }
813
+ if (attrs.interval_count === null || attrs.interval_count <= 0) {
814
+ issues.push(
815
+ issue(
816
+ ISSUE_CODES.PLAN_INVALID_INTERVAL,
817
+ "error",
818
+ `Subscription variant has invalid interval_count: ${attrs.interval_count}. Must be a positive integer.`,
819
+ {
820
+ suggestedFix: "Set interval_count to a positive value (e.g. 1 for monthly, 2 for biweekly).",
821
+ context: { intervalCount: attrs.interval_count }
822
+ }
823
+ )
824
+ );
825
+ }
826
+ if (attrs.price === 0 && attrs.is_subscription) {
827
+ issues.push(
828
+ issue(
829
+ ISSUE_CODES.PLAN_FREE_PRICE,
830
+ "warning",
831
+ `Subscription variant has a price of 0 \u2014 this is almost always a misconfiguration for paid plans.`,
832
+ {
833
+ suggestedFix: "Set the variant price to the intended amount in cents, or confirm this is intentionally free.",
834
+ context: { price: attrs.price }
835
+ }
836
+ )
837
+ );
838
+ }
839
+ if (attrs.has_free_trial && (!attrs.trial_interval || (attrs.trial_interval_count ?? 0) <= 0)) {
840
+ issues.push(
841
+ issue(
842
+ ISSUE_CODES.PLAN_TRIAL_INCONSISTENT,
843
+ "warning",
844
+ `Subscription variant has free trial enabled but trial interval is misconfigured (interval: "${attrs.trial_interval ?? "missing"}", count: ${attrs.trial_interval_count ?? 0}).`,
845
+ {
846
+ suggestedFix: "Set a valid trial interval and count, or disable the free trial.",
847
+ context: {
848
+ trialInterval: attrs.trial_interval ?? null,
849
+ trialIntervalCount: attrs.trial_interval_count ?? null
850
+ }
851
+ }
852
+ )
853
+ );
854
+ }
855
+ if (attrs.status === "draft") {
856
+ issues.push(
857
+ issue(
858
+ ISSUE_CODES.PLAN_DRAFT,
859
+ "warning",
860
+ `Subscription variant is in draft status \u2014 customers cannot subscribe.`,
861
+ {
862
+ suggestedFix: "Publish the variant in the Lemon Squeezy dashboard.",
863
+ context: { status: attrs.status }
864
+ }
865
+ )
866
+ );
867
+ }
868
+ try {
869
+ const product = await getProduct(http, attrs.product_id);
870
+ const expectedStore = String(options.storeId);
871
+ const actualStore = String(product.attributes.store_id);
872
+ if (expectedStore !== actualStore) {
873
+ issues.push(
874
+ issue(
875
+ ISSUE_CODES.PLAN_STORE_MISMATCH,
876
+ "error",
877
+ `Subscription variant belongs to store ${actualStore} (via product ${attrs.product_id}), expected ${expectedStore}.`,
878
+ {
879
+ suggestedFix: "Use the correct store ID or variant ID \u2014 plans should not cross stores.",
880
+ context: { expectedStoreId: expectedStore, actualStoreId: actualStore, productId: String(attrs.product_id) }
881
+ }
882
+ )
883
+ );
884
+ }
885
+ } catch {
886
+ }
887
+ const summary = {
888
+ variantId: variant.id,
889
+ interval: attrs.interval,
890
+ intervalCount: attrs.interval_count,
891
+ price: attrs.price,
892
+ hasFreeTrial: attrs.has_free_trial,
893
+ status: attrs.status
894
+ };
895
+ return buildResult("subscriptionPlan", mode, issues, summary);
896
+ }
897
+
524
898
  // src/validate/doctor.ts
525
899
  async function doctor(http, mode, options = {}) {
526
900
  const results = [];
@@ -548,6 +922,30 @@ async function doctor(http, mode, options = {}) {
548
922
  })
549
923
  );
550
924
  }
925
+ if (options.storeId !== void 0 && options.discountId !== void 0) {
926
+ results.push(
927
+ await validateDiscount(http, mode, {
928
+ storeId: options.storeId,
929
+ discountId: options.discountId
930
+ })
931
+ );
932
+ }
933
+ if (options.storeId !== void 0 && options.licenseKeyId !== void 0) {
934
+ results.push(
935
+ await validateLicenseKey(http, mode, {
936
+ storeId: options.storeId,
937
+ licenseKeyId: options.licenseKeyId
938
+ })
939
+ );
940
+ }
941
+ if (options.storeId !== void 0 && options.variantId !== void 0) {
942
+ results.push(
943
+ await validateSubscriptionPlan(http, mode, {
944
+ storeId: options.storeId,
945
+ variantId: options.variantId
946
+ })
947
+ );
948
+ }
551
949
  const ok = results.every((result) => result.ok);
552
950
  return { ok, mode, results };
553
951
  }
@@ -563,10 +961,16 @@ function createFreshSqueezy(config = {}) {
563
961
  validateStore: (storeId) => validateStore(http, resolved.mode, storeId),
564
962
  validateProduct: (options) => validateProduct(http, resolved.mode, options),
565
963
  validateWebhook: (options) => validateWebhook(http, resolved.mode, options),
964
+ validateDiscount: (options) => validateDiscount(http, resolved.mode, options),
965
+ validateLicenseKey: (options) => validateLicenseKey(http, resolved.mode, options),
966
+ validateSubscriptionPlan: (options) => validateSubscriptionPlan(http, resolved.mode, options),
566
967
  doctor: (options) => doctor(http, resolved.mode, {
567
968
  storeId: options?.storeId ?? resolved.storeId,
568
969
  productId: options?.productId,
569
- webhookUrl: options?.webhookUrl
970
+ webhookUrl: options?.webhookUrl,
971
+ discountId: options?.discountId,
972
+ licenseKeyId: options?.licenseKeyId,
973
+ variantId: options?.variantId
570
974
  })
571
975
  };
572
976
  }
@@ -584,6 +988,7 @@ export {
584
988
  getAuthenticatedUser,
585
989
  getProduct,
586
990
  getStore,
991
+ getVariant,
587
992
  isOk,
588
993
  issue,
589
994
  listProducts,