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.cjs CHANGED
@@ -33,6 +33,7 @@ __export(src_exports, {
33
33
  getAuthenticatedUser: () => getAuthenticatedUser,
34
34
  getProduct: () => getProduct,
35
35
  getStore: () => getStore,
36
+ getVariant: () => getVariant,
36
37
  isOk: () => isOk,
37
38
  issue: () => issue,
38
39
  listProducts: () => listProducts,
@@ -213,6 +214,25 @@ var ISSUE_CODES = {
213
214
  WEBHOOK_NOT_FOUND: "WEBHOOK_NOT_FOUND",
214
215
  WEBHOOK_EVENTS_MISSING: "WEBHOOK_EVENTS_MISSING",
215
216
  WEBHOOK_OPTIONAL_EVENTS: "WEBHOOK_OPTIONAL_EVENTS",
217
+ DISCOUNT_NOT_FOUND: "DISCOUNT_NOT_FOUND",
218
+ DISCOUNT_DRAFT: "DISCOUNT_DRAFT",
219
+ DISCOUNT_EXPIRED: "DISCOUNT_EXPIRED",
220
+ DISCOUNT_NOT_STARTED: "DISCOUNT_NOT_STARTED",
221
+ DISCOUNT_REDEMPTIONS_EXHAUSTED: "DISCOUNT_REDEMPTIONS_EXHAUSTED",
222
+ DISCOUNT_INVALID_AMOUNT: "DISCOUNT_INVALID_AMOUNT",
223
+ DISCOUNT_STORE_MISMATCH: "DISCOUNT_STORE_MISMATCH",
224
+ LICENSE_KEY_NOT_FOUND: "LICENSE_KEY_NOT_FOUND",
225
+ LICENSE_KEY_DISABLED: "LICENSE_KEY_DISABLED",
226
+ LICENSE_KEY_EXPIRED: "LICENSE_KEY_EXPIRED",
227
+ LICENSE_KEY_AT_ACTIVATION_LIMIT: "LICENSE_KEY_AT_ACTIVATION_LIMIT",
228
+ LICENSE_KEY_STORE_MISMATCH: "LICENSE_KEY_STORE_MISMATCH",
229
+ PLAN_VARIANT_NOT_FOUND: "PLAN_VARIANT_NOT_FOUND",
230
+ PLAN_NOT_SUBSCRIPTION: "PLAN_NOT_SUBSCRIPTION",
231
+ PLAN_INVALID_INTERVAL: "PLAN_INVALID_INTERVAL",
232
+ PLAN_FREE_PRICE: "PLAN_FREE_PRICE",
233
+ PLAN_TRIAL_INCONSISTENT: "PLAN_TRIAL_INCONSISTENT",
234
+ PLAN_DRAFT: "PLAN_DRAFT",
235
+ PLAN_STORE_MISMATCH: "PLAN_STORE_MISMATCH",
216
236
  NETWORK_ERROR: "NETWORK_ERROR",
217
237
  UNKNOWN: "UNKNOWN"
218
238
  };
@@ -349,6 +369,9 @@ async function listProducts(http, storeId) {
349
369
  }
350
370
 
351
371
  // src/resources/variants.ts
372
+ async function getVariant(http, variantId) {
373
+ return http.getResource(`/v1/variants/${variantId}`);
374
+ }
352
375
  async function listVariantsForProduct(http, productId) {
353
376
  return http.getCollection("/v1/variants", {
354
377
  "filter[product_id]": String(productId)
@@ -570,6 +593,358 @@ function normalizeUrl(raw) {
570
593
  return raw.replace(/\/+$/, "").toLowerCase();
571
594
  }
572
595
 
596
+ // src/resources/discounts.ts
597
+ async function getDiscount(http, discountId) {
598
+ return http.getResource(`/v1/discounts/${discountId}`);
599
+ }
600
+
601
+ // src/validate/discount.ts
602
+ async function validateDiscount(http, mode, options) {
603
+ const issues = [];
604
+ let discount;
605
+ try {
606
+ discount = await getDiscount(http, options.discountId);
607
+ } catch (err) {
608
+ if (err instanceof FreshSqueezyError && err.status === 404) {
609
+ issues.push(
610
+ issue(ISSUE_CODES.DISCOUNT_NOT_FOUND, "error", `Discount ${options.discountId} not found.`, {
611
+ suggestedFix: "Verify the discount ID in the Lemon Squeezy dashboard.",
612
+ context: { discountId: String(options.discountId) }
613
+ })
614
+ );
615
+ return buildResult("discount", mode, issues);
616
+ }
617
+ const message = err instanceof Error ? err.message : "Unknown error";
618
+ issues.push(issue(ISSUE_CODES.UNKNOWN, "error", message));
619
+ return buildResult("discount", mode, issues);
620
+ }
621
+ const attrs = discount.attributes;
622
+ const expectedStore = String(options.storeId);
623
+ const actualStore = String(attrs.store_id);
624
+ if (expectedStore !== actualStore) {
625
+ issues.push(
626
+ issue(
627
+ ISSUE_CODES.DISCOUNT_STORE_MISMATCH,
628
+ "error",
629
+ `Discount belongs to store ${actualStore}, expected ${expectedStore}.`,
630
+ {
631
+ suggestedFix: "Use the correct store ID or discount ID \u2014 discounts should not cross stores.",
632
+ context: { expectedStoreId: expectedStore, actualStoreId: actualStore }
633
+ }
634
+ )
635
+ );
636
+ }
637
+ if (attrs.status === "draft") {
638
+ issues.push(
639
+ issue(
640
+ ISSUE_CODES.DISCOUNT_DRAFT,
641
+ "warning",
642
+ `Discount "${attrs.name}" is in draft status \u2014 customers cannot redeem it.`,
643
+ {
644
+ suggestedFix: "Publish the discount in the Lemon Squeezy dashboard before sharing the code.",
645
+ context: { name: attrs.name, code: attrs.code }
646
+ }
647
+ )
648
+ );
649
+ }
650
+ const now = /* @__PURE__ */ new Date();
651
+ if (attrs.expires_at && new Date(attrs.expires_at) < now) {
652
+ issues.push(
653
+ issue(
654
+ ISSUE_CODES.DISCOUNT_EXPIRED,
655
+ "error",
656
+ `Discount "${attrs.name}" expired at ${attrs.expires_at}.`,
657
+ {
658
+ suggestedFix: "Extend the expiration date or create a new discount.",
659
+ context: { name: attrs.name, expiresAt: attrs.expires_at }
660
+ }
661
+ )
662
+ );
663
+ }
664
+ if (attrs.starts_at && new Date(attrs.starts_at) > now) {
665
+ issues.push(
666
+ issue(
667
+ ISSUE_CODES.DISCOUNT_NOT_STARTED,
668
+ "warning",
669
+ `Discount "${attrs.name}" starts at ${attrs.starts_at} \u2014 not yet active.`,
670
+ {
671
+ suggestedFix: "Wait for the start date or adjust it in the dashboard.",
672
+ context: { name: attrs.name, startsAt: attrs.starts_at }
673
+ }
674
+ )
675
+ );
676
+ }
677
+ if (attrs.is_limited_redemptions && attrs.max_redemptions <= 0) {
678
+ issues.push(
679
+ issue(
680
+ ISSUE_CODES.DISCOUNT_REDEMPTIONS_EXHAUSTED,
681
+ "warning",
682
+ `Discount "${attrs.name}" has limited redemptions with max_redemptions \u2264 0.`,
683
+ {
684
+ suggestedFix: "Increase max_redemptions or disable the redemption limit.",
685
+ context: { name: attrs.name, maxRedemptions: attrs.max_redemptions }
686
+ }
687
+ )
688
+ );
689
+ }
690
+ if (attrs.amount <= 0) {
691
+ issues.push(
692
+ issue(
693
+ ISSUE_CODES.DISCOUNT_INVALID_AMOUNT,
694
+ "error",
695
+ `Discount "${attrs.name}" has amount ${attrs.amount} \u2014 must be positive.`,
696
+ {
697
+ suggestedFix: "Set a positive discount amount in the dashboard.",
698
+ context: { name: attrs.name, amount: attrs.amount }
699
+ }
700
+ )
701
+ );
702
+ } else if (attrs.amount_type === "percent" && attrs.amount > 100) {
703
+ issues.push(
704
+ issue(
705
+ ISSUE_CODES.DISCOUNT_INVALID_AMOUNT,
706
+ "error",
707
+ `Discount "${attrs.name}" is ${attrs.amount}% \u2014 percent discounts cannot exceed 100%.`,
708
+ {
709
+ suggestedFix: "Set the discount to 100% or less.",
710
+ context: { name: attrs.name, amount: attrs.amount, amountType: attrs.amount_type }
711
+ }
712
+ )
713
+ );
714
+ }
715
+ return buildResult("discount", mode, issues, attrs);
716
+ }
717
+
718
+ // src/resources/licenseKeys.ts
719
+ async function getLicenseKey(http, licenseKeyId) {
720
+ return http.getResource(`/v1/license-keys/${licenseKeyId}`);
721
+ }
722
+
723
+ // src/validate/licenseKey.ts
724
+ async function validateLicenseKey(http, mode, options) {
725
+ const issues = [];
726
+ let licenseKey;
727
+ try {
728
+ licenseKey = await getLicenseKey(http, options.licenseKeyId);
729
+ } catch (err) {
730
+ if (err instanceof FreshSqueezyError && err.status === 404) {
731
+ issues.push(
732
+ issue(
733
+ ISSUE_CODES.LICENSE_KEY_NOT_FOUND,
734
+ "error",
735
+ `License key ${options.licenseKeyId} not found.`,
736
+ {
737
+ suggestedFix: "Verify the license key ID in the Lemon Squeezy dashboard.",
738
+ context: { licenseKeyId: String(options.licenseKeyId) }
739
+ }
740
+ )
741
+ );
742
+ return buildResult("licenseKey", mode, issues);
743
+ }
744
+ const message = err instanceof Error ? err.message : "Unknown error";
745
+ issues.push(issue(ISSUE_CODES.UNKNOWN, "error", message));
746
+ return buildResult("licenseKey", mode, issues);
747
+ }
748
+ const attrs = licenseKey.attributes;
749
+ const expectedStore = String(options.storeId);
750
+ const actualStore = String(attrs.store_id);
751
+ if (expectedStore !== actualStore) {
752
+ issues.push(
753
+ issue(
754
+ ISSUE_CODES.LICENSE_KEY_STORE_MISMATCH,
755
+ "error",
756
+ `License key belongs to store ${actualStore}, expected ${expectedStore}.`,
757
+ {
758
+ suggestedFix: "Use the correct store ID or license key ID \u2014 keys should not cross stores.",
759
+ context: { expectedStoreId: expectedStore, actualStoreId: actualStore }
760
+ }
761
+ )
762
+ );
763
+ }
764
+ if (attrs.disabled) {
765
+ issues.push(
766
+ issue(
767
+ ISSUE_CODES.LICENSE_KEY_DISABLED,
768
+ "error",
769
+ `License key ${attrs.key_short} is disabled.`,
770
+ {
771
+ suggestedFix: "Re-enable the license key in the Lemon Squeezy dashboard.",
772
+ context: { keyShort: attrs.key_short }
773
+ }
774
+ )
775
+ );
776
+ }
777
+ if (attrs.expires_at && new Date(attrs.expires_at) < /* @__PURE__ */ new Date()) {
778
+ issues.push(
779
+ issue(
780
+ ISSUE_CODES.LICENSE_KEY_EXPIRED,
781
+ "error",
782
+ `License key ${attrs.key_short} expired at ${attrs.expires_at}.`,
783
+ {
784
+ suggestedFix: "Extend the expiration date or issue a new license key.",
785
+ context: { keyShort: attrs.key_short, expiresAt: attrs.expires_at }
786
+ }
787
+ )
788
+ );
789
+ }
790
+ if (attrs.activation_limit !== null && attrs.instances_count >= attrs.activation_limit) {
791
+ issues.push(
792
+ issue(
793
+ ISSUE_CODES.LICENSE_KEY_AT_ACTIVATION_LIMIT,
794
+ "warning",
795
+ `License key ${attrs.key_short} has reached its activation limit (${attrs.instances_count}/${attrs.activation_limit}).`,
796
+ {
797
+ suggestedFix: "Increase the activation limit or deactivate unused instances.",
798
+ context: {
799
+ keyShort: attrs.key_short,
800
+ instancesCount: attrs.instances_count,
801
+ activationLimit: attrs.activation_limit
802
+ }
803
+ }
804
+ )
805
+ );
806
+ }
807
+ return buildResult("licenseKey", mode, issues, attrs);
808
+ }
809
+
810
+ // src/validate/subscriptionPlan.ts
811
+ var VALID_INTERVALS = /* @__PURE__ */ new Set(["day", "week", "month", "year"]);
812
+ async function validateSubscriptionPlan(http, mode, options) {
813
+ const issues = [];
814
+ let variant;
815
+ try {
816
+ variant = await getVariant(http, options.variantId);
817
+ } catch (err) {
818
+ if (err instanceof FreshSqueezyError && err.status === 404) {
819
+ issues.push(
820
+ issue(
821
+ ISSUE_CODES.PLAN_VARIANT_NOT_FOUND,
822
+ "error",
823
+ `Variant ${options.variantId} not found.`,
824
+ {
825
+ suggestedFix: "Verify the variant ID in the Lemon Squeezy dashboard.",
826
+ context: { variantId: String(options.variantId) }
827
+ }
828
+ )
829
+ );
830
+ return buildResult("subscriptionPlan", mode, issues);
831
+ }
832
+ const message = err instanceof Error ? err.message : "Unknown error";
833
+ issues.push(issue(ISSUE_CODES.UNKNOWN, "error", message));
834
+ return buildResult("subscriptionPlan", mode, issues);
835
+ }
836
+ const attrs = variant.attributes;
837
+ if (!attrs.is_subscription) {
838
+ issues.push(
839
+ issue(
840
+ ISSUE_CODES.PLAN_NOT_SUBSCRIPTION,
841
+ "error",
842
+ `Variant ${options.variantId} is not a subscription variant (is_subscription is false).`,
843
+ {
844
+ suggestedFix: "Use a variant that has subscription billing enabled, or use the regular variant validator.",
845
+ context: { variantId: String(options.variantId) }
846
+ }
847
+ )
848
+ );
849
+ }
850
+ if (!attrs.interval || !VALID_INTERVALS.has(attrs.interval)) {
851
+ issues.push(
852
+ issue(
853
+ ISSUE_CODES.PLAN_INVALID_INTERVAL,
854
+ "error",
855
+ `Subscription variant has invalid interval: "${attrs.interval ?? "missing"}". Expected one of: day, week, month, year.`,
856
+ {
857
+ suggestedFix: "Set a valid billing interval in the variant configuration.",
858
+ context: { interval: attrs.interval ?? null }
859
+ }
860
+ )
861
+ );
862
+ }
863
+ if (attrs.interval_count === null || attrs.interval_count <= 0) {
864
+ issues.push(
865
+ issue(
866
+ ISSUE_CODES.PLAN_INVALID_INTERVAL,
867
+ "error",
868
+ `Subscription variant has invalid interval_count: ${attrs.interval_count}. Must be a positive integer.`,
869
+ {
870
+ suggestedFix: "Set interval_count to a positive value (e.g. 1 for monthly, 2 for biweekly).",
871
+ context: { intervalCount: attrs.interval_count }
872
+ }
873
+ )
874
+ );
875
+ }
876
+ if (attrs.price === 0 && attrs.is_subscription) {
877
+ issues.push(
878
+ issue(
879
+ ISSUE_CODES.PLAN_FREE_PRICE,
880
+ "warning",
881
+ `Subscription variant has a price of 0 \u2014 this is almost always a misconfiguration for paid plans.`,
882
+ {
883
+ suggestedFix: "Set the variant price to the intended amount in cents, or confirm this is intentionally free.",
884
+ context: { price: attrs.price }
885
+ }
886
+ )
887
+ );
888
+ }
889
+ if (attrs.has_free_trial && (!attrs.trial_interval || (attrs.trial_interval_count ?? 0) <= 0)) {
890
+ issues.push(
891
+ issue(
892
+ ISSUE_CODES.PLAN_TRIAL_INCONSISTENT,
893
+ "warning",
894
+ `Subscription variant has free trial enabled but trial interval is misconfigured (interval: "${attrs.trial_interval ?? "missing"}", count: ${attrs.trial_interval_count ?? 0}).`,
895
+ {
896
+ suggestedFix: "Set a valid trial interval and count, or disable the free trial.",
897
+ context: {
898
+ trialInterval: attrs.trial_interval ?? null,
899
+ trialIntervalCount: attrs.trial_interval_count ?? null
900
+ }
901
+ }
902
+ )
903
+ );
904
+ }
905
+ if (attrs.status === "draft") {
906
+ issues.push(
907
+ issue(
908
+ ISSUE_CODES.PLAN_DRAFT,
909
+ "warning",
910
+ `Subscription variant is in draft status \u2014 customers cannot subscribe.`,
911
+ {
912
+ suggestedFix: "Publish the variant in the Lemon Squeezy dashboard.",
913
+ context: { status: attrs.status }
914
+ }
915
+ )
916
+ );
917
+ }
918
+ try {
919
+ const product = await getProduct(http, attrs.product_id);
920
+ const expectedStore = String(options.storeId);
921
+ const actualStore = String(product.attributes.store_id);
922
+ if (expectedStore !== actualStore) {
923
+ issues.push(
924
+ issue(
925
+ ISSUE_CODES.PLAN_STORE_MISMATCH,
926
+ "error",
927
+ `Subscription variant belongs to store ${actualStore} (via product ${attrs.product_id}), expected ${expectedStore}.`,
928
+ {
929
+ suggestedFix: "Use the correct store ID or variant ID \u2014 plans should not cross stores.",
930
+ context: { expectedStoreId: expectedStore, actualStoreId: actualStore, productId: String(attrs.product_id) }
931
+ }
932
+ )
933
+ );
934
+ }
935
+ } catch {
936
+ }
937
+ const summary = {
938
+ variantId: variant.id,
939
+ interval: attrs.interval,
940
+ intervalCount: attrs.interval_count,
941
+ price: attrs.price,
942
+ hasFreeTrial: attrs.has_free_trial,
943
+ status: attrs.status
944
+ };
945
+ return buildResult("subscriptionPlan", mode, issues, summary);
946
+ }
947
+
573
948
  // src/validate/doctor.ts
574
949
  async function doctor(http, mode, options = {}) {
575
950
  const results = [];
@@ -597,6 +972,30 @@ async function doctor(http, mode, options = {}) {
597
972
  })
598
973
  );
599
974
  }
975
+ if (options.storeId !== void 0 && options.discountId !== void 0) {
976
+ results.push(
977
+ await validateDiscount(http, mode, {
978
+ storeId: options.storeId,
979
+ discountId: options.discountId
980
+ })
981
+ );
982
+ }
983
+ if (options.storeId !== void 0 && options.licenseKeyId !== void 0) {
984
+ results.push(
985
+ await validateLicenseKey(http, mode, {
986
+ storeId: options.storeId,
987
+ licenseKeyId: options.licenseKeyId
988
+ })
989
+ );
990
+ }
991
+ if (options.storeId !== void 0 && options.variantId !== void 0) {
992
+ results.push(
993
+ await validateSubscriptionPlan(http, mode, {
994
+ storeId: options.storeId,
995
+ variantId: options.variantId
996
+ })
997
+ );
998
+ }
600
999
  const ok = results.every((result) => result.ok);
601
1000
  return { ok, mode, results };
602
1001
  }
@@ -612,10 +1011,16 @@ function createFreshSqueezy(config = {}) {
612
1011
  validateStore: (storeId) => validateStore(http, resolved.mode, storeId),
613
1012
  validateProduct: (options) => validateProduct(http, resolved.mode, options),
614
1013
  validateWebhook: (options) => validateWebhook(http, resolved.mode, options),
1014
+ validateDiscount: (options) => validateDiscount(http, resolved.mode, options),
1015
+ validateLicenseKey: (options) => validateLicenseKey(http, resolved.mode, options),
1016
+ validateSubscriptionPlan: (options) => validateSubscriptionPlan(http, resolved.mode, options),
615
1017
  doctor: (options) => doctor(http, resolved.mode, {
616
1018
  storeId: options?.storeId ?? resolved.storeId,
617
1019
  productId: options?.productId,
618
- webhookUrl: options?.webhookUrl
1020
+ webhookUrl: options?.webhookUrl,
1021
+ discountId: options?.discountId,
1022
+ licenseKeyId: options?.licenseKeyId,
1023
+ variantId: options?.variantId
619
1024
  })
620
1025
  };
621
1026
  }
@@ -634,6 +1039,7 @@ function createFreshSqueezy(config = {}) {
634
1039
  getAuthenticatedUser,
635
1040
  getProduct,
636
1041
  getStore,
1042
+ getVariant,
637
1043
  isOk,
638
1044
  issue,
639
1045
  listProducts,