atmn 2.0.132 → 2.0.133

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/bin.js CHANGED
@@ -4782,6 +4782,25 @@ var LINT_RULES = {
4782
4782
  parentIdField: "planId",
4783
4783
  parentLabel: "plan",
4784
4784
  because: "An archived feature should not gain new customers through a live plan."
4785
+ },
4786
+ {
4787
+ kind: "targetForbids",
4788
+ field: "featureId",
4789
+ in: "features",
4790
+ matching: "featureId",
4791
+ target: "consumable",
4792
+ equals: false,
4793
+ whenEquals: {
4794
+ "price.tierBehavior": "volume",
4795
+ "price.billingMethod": "usage_based"
4796
+ },
4797
+ whenStated: [],
4798
+ whenAnyStated: [
4799
+ "price.tiers.0.flatAmount",
4800
+ "price.tiers.0.additionalCurrencies.*.flatAmount"
4801
+ ],
4802
+ whenUnstated: [],
4803
+ because: "A volume-tiered allocated item can't have a first-tier flatAmount: Stripe would charge it even with no seats over included. Move the fee to a later tier."
4785
4804
  }
4786
4805
  ]
4787
4806
  },
@@ -4959,12 +4978,14 @@ var LINT_RULES = {
4959
4978
  },
4960
4979
  rules: [
4961
4980
  {
4962
- kind: "valueWhen",
4963
- when: "tierBehavior",
4964
- equals: "volume",
4965
- field: "billingMethod",
4966
- mustBe: "prepaid",
4967
- because: "Volume tiers are prepaid-only."
4981
+ kind: "refusedWhen",
4982
+ whenEquals: {
4983
+ tierBehavior: "volume",
4984
+ billingMethod: "usage_based"
4985
+ },
4986
+ whenStated: [],
4987
+ whenUnstated: ["tiers.1"],
4988
+ because: "Volume-based pricing on a usage-based item needs at least two tiers. Add a tier, or use graduated pricing for a single rate."
4968
4989
  }
4969
4990
  ]
4970
4991
  },
@@ -6654,6 +6675,10 @@ var valueAtPath = ({
6654
6675
  }) => {
6655
6676
  let current = entry;
6656
6677
  for (const segment of path.split(".")) {
6678
+ if (Array.isArray(current)) {
6679
+ current = current[Number(segment)];
6680
+ continue;
6681
+ }
6657
6682
  if (!isEntry(current))
6658
6683
  return;
6659
6684
  current = current[segment];
@@ -6795,6 +6820,21 @@ var COMPARE = {
6795
6820
  ">": { holds: (a, b) => a > b, words: "greater than" },
6796
6821
  ">=": { holds: (a, b) => a >= b, words: "at least" }
6797
6822
  };
6823
+ var entryMatchesWhen = ({
6824
+ entry,
6825
+ rule
6826
+ }) => {
6827
+ const isStatedValue = (value) => value !== undefined && value !== null && value !== 0 && value !== false;
6828
+ const isStated = (path) => {
6829
+ const [head, tail] = path.split(".*.");
6830
+ if (tail === undefined)
6831
+ return isStatedValue(valueAtPath({ entry, path }));
6832
+ const list = valueAtPath({ entry, path: head });
6833
+ return Array.isArray(list) && list.some((element) => isEntry(element) && isStatedValue(element[tail]));
6834
+ };
6835
+ const anyStated = "whenAnyStated" in rule && rule.whenAnyStated ? rule.whenAnyStated.some(isStated) : true;
6836
+ return Object.entries(rule.whenEquals).every(([path, value]) => valueAtPath({ entry, path }) === value) && rule.whenStated.every(isStated) && anyStated && !rule.whenUnstated.some(isStated);
6837
+ };
6798
6838
  var entryRuleFailures = ({
6799
6839
  entry,
6800
6840
  rule,
@@ -6914,6 +6954,21 @@ var entryRuleFailures = ({
6914
6954
  `${rule.label} ${show(entry[rule.field])} is ${rule.target}. Unarchive it, or archive ${rule.parentLabel} ${show(parent?.[rule.parentIdField])}. ${rule.because}`
6915
6955
  ];
6916
6956
  }
6957
+ case "refusedWhen":
6958
+ return entryMatchesWhen({ entry, rule }) ? [rule.because] : [];
6959
+ case "targetForbids": {
6960
+ if (!entryMatchesWhen({ entry, rule }))
6961
+ return [];
6962
+ const target = document[rule.in];
6963
+ if (!Array.isArray(target))
6964
+ return [];
6965
+ const row = target.find((candidate) => isEntry(candidate) && candidate[rule.matching] === entry[rule.field]);
6966
+ if (!row || row[rule.target] !== rule.equals)
6967
+ return [];
6968
+ return [
6969
+ `${rule.in} ${show(entry[rule.field])} has ${rule.target} ${show(rule.equals)}, so this is refused. ${rule.because}`
6970
+ ];
6971
+ }
6917
6972
  case "unique":
6918
6973
  case "linkedOnce":
6919
6974
  case "exactlyOneActive":
@@ -7641,7 +7696,7 @@ var createClient = (options) => ({
7641
7696
  });
7642
7697
 
7643
7698
  // src/version.ts
7644
- var version = "2.0.132";
7699
+ var version = "2.0.133";
7645
7700
 
7646
7701
  // src/http/autumnFetch.ts
7647
7702
  var runtime = typeof Bun !== "undefined" ? `bun ${Bun.version}` : `node ${process.versions.node}`;
@@ -10305,7 +10360,7 @@ Billing controls follow the same rule: \`·\` lines under the item they guard, i
10305
10360
  - A prepaid quantity **includes** the included amount, and so does each tier's \`to\` — the first tier's \`to\` must exceed \`included\`.
10306
10361
  - \`billingUnits\` rounds usage **up** when billing.
10307
10362
  - Set explicitly, never lean on defaults: \`billingMethod\` and \`interval\` on every item price, \`tierBehavior\` on tiered prices, and every trial field (\`durationLength\`, \`durationType\`, \`cardRequired\`, \`onEnd\`). Explicit defaults cause no spurious diffs.
10308
- - Volume tiers charge the flat amount of the reached tier and are prepaid-only; graduated (the default) sums across brackets.
10363
+ - Volume tiers on usage-based items bill only usage past the included amount, at the rate of the tier total usage reaches, plus its flat fee. On prepaid items they charge every unit (included ones too) at the reached tier's rate and flat amount. Graduated (the default) sums across brackets. Volume can't be combined with threshold billing, and allocated volume can't have a tier-1 flat amount.
10309
10364
  - Rollover needs a resetting allowance; \`max\` and \`maxPercentage\` are mutually exclusive; \`expiryDurationType\` is required.
10310
10365
  - \`billingControls\` is a plain object on the plan with camelCase fields like the rest of the config (\`featureId\`, \`overageLimit\`); each control list replaces wholesale on update.
10311
10366
  - Plan \`config\` (\`ignorePastDue\`, \`anchorToMonthStart\`) is unmanaged when omitted. Once stated it is the whole set: a flag left out is off, so \`config: {}\` turns every flag off.
@@ -10879,7 +10934,9 @@ Autumn:
10879
10934
  2. Finds the tier the total falls into
10880
10935
  3. Charges the flat amount for that tier
10881
10936
 
10882
- Volume tiers are prepaid-only.
10937
+ Volume tiers work on prepaid and usage-based (pay-per-use or allocated) items. On usage-based items, volume bills only usage past the included amount, at the rate of the tier total usage reaches, plus its flat fee; at or below the included amount nothing is charged. On prepaid items, every unit (the included ones too) is charged at the rate of the tier the total lands in, plus its flat amount. Allocated items can't have a flat amount on the first tier. A plan with volume-priced allocated seats can't yet be attached to more than one entity on the same customer, because their seats would share one Stripe price.
10938
+
10939
+ Volume tiers can't be combined with threshold billing.
10883
10940
 
10884
10941
  | Total volume | Matching tier | Charge |
10885
10942
  |-------------|---------------|--------|
@@ -23426,8 +23483,10 @@ var formatInterval = (interval, count) => {
23426
23483
  var formatPrice = (price) => {
23427
23484
  if (price === null || price === undefined)
23428
23485
  return "Free";
23429
- if (price.tiers !== undefined && price.tiers.length > 0)
23430
- return `${price.tiers.length} tiers ${formatInterval(price.interval, price.intervalCount)}`;
23486
+ if (price.tiers !== undefined && price.tiers.length > 0) {
23487
+ const behavior = price.tierBehavior ? `${price.tierBehavior} ` : "";
23488
+ return `${price.tiers.length} ${behavior}tiers ${formatInterval(price.interval, price.intervalCount)}`;
23489
+ }
23431
23490
  return `${formatMoney(price.amount ?? 0)} ${formatInterval(price.interval, price.intervalCount)}`;
23432
23491
  };
23433
23492
  var formatItem = (item) => {
package/dist/index.js CHANGED
@@ -451,6 +451,25 @@ var LINT_RULES = {
451
451
  parentIdField: "planId",
452
452
  parentLabel: "plan",
453
453
  because: "An archived feature should not gain new customers through a live plan."
454
+ },
455
+ {
456
+ kind: "targetForbids",
457
+ field: "featureId",
458
+ in: "features",
459
+ matching: "featureId",
460
+ target: "consumable",
461
+ equals: false,
462
+ whenEquals: {
463
+ "price.tierBehavior": "volume",
464
+ "price.billingMethod": "usage_based"
465
+ },
466
+ whenStated: [],
467
+ whenAnyStated: [
468
+ "price.tiers.0.flatAmount",
469
+ "price.tiers.0.additionalCurrencies.*.flatAmount"
470
+ ],
471
+ whenUnstated: [],
472
+ because: "A volume-tiered allocated item can't have a first-tier flatAmount: Stripe would charge it even with no seats over included. Move the fee to a later tier."
454
473
  }
455
474
  ]
456
475
  },
@@ -628,12 +647,14 @@ var LINT_RULES = {
628
647
  },
629
648
  rules: [
630
649
  {
631
- kind: "valueWhen",
632
- when: "tierBehavior",
633
- equals: "volume",
634
- field: "billingMethod",
635
- mustBe: "prepaid",
636
- because: "Volume tiers are prepaid-only."
650
+ kind: "refusedWhen",
651
+ whenEquals: {
652
+ tierBehavior: "volume",
653
+ billingMethod: "usage_based"
654
+ },
655
+ whenStated: [],
656
+ whenUnstated: ["tiers.1"],
657
+ because: "Volume-based pricing on a usage-based item needs at least two tiers. Add a tier, or use graduated pricing for a single rate."
637
658
  }
638
659
  ]
639
660
  },
@@ -2323,6 +2344,10 @@ var valueAtPath = ({
2323
2344
  }) => {
2324
2345
  let current = entry;
2325
2346
  for (const segment of path.split(".")) {
2347
+ if (Array.isArray(current)) {
2348
+ current = current[Number(segment)];
2349
+ continue;
2350
+ }
2326
2351
  if (!isEntry(current))
2327
2352
  return;
2328
2353
  current = current[segment];
@@ -2464,6 +2489,21 @@ var COMPARE = {
2464
2489
  ">": { holds: (a, b) => a > b, words: "greater than" },
2465
2490
  ">=": { holds: (a, b) => a >= b, words: "at least" }
2466
2491
  };
2492
+ var entryMatchesWhen = ({
2493
+ entry,
2494
+ rule
2495
+ }) => {
2496
+ const isStatedValue = (value) => value !== undefined && value !== null && value !== 0 && value !== false;
2497
+ const isStated = (path) => {
2498
+ const [head, tail] = path.split(".*.");
2499
+ if (tail === undefined)
2500
+ return isStatedValue(valueAtPath({ entry, path }));
2501
+ const list = valueAtPath({ entry, path: head });
2502
+ return Array.isArray(list) && list.some((element) => isEntry(element) && isStatedValue(element[tail]));
2503
+ };
2504
+ const anyStated = "whenAnyStated" in rule && rule.whenAnyStated ? rule.whenAnyStated.some(isStated) : true;
2505
+ return Object.entries(rule.whenEquals).every(([path, value]) => valueAtPath({ entry, path }) === value) && rule.whenStated.every(isStated) && anyStated && !rule.whenUnstated.some(isStated);
2506
+ };
2467
2507
  var entryRuleFailures = ({
2468
2508
  entry,
2469
2509
  rule,
@@ -2583,6 +2623,21 @@ var entryRuleFailures = ({
2583
2623
  `${rule.label} ${show(entry[rule.field])} is ${rule.target}. Unarchive it, or archive ${rule.parentLabel} ${show(parent?.[rule.parentIdField])}. ${rule.because}`
2584
2624
  ];
2585
2625
  }
2626
+ case "refusedWhen":
2627
+ return entryMatchesWhen({ entry, rule }) ? [rule.because] : [];
2628
+ case "targetForbids": {
2629
+ if (!entryMatchesWhen({ entry, rule }))
2630
+ return [];
2631
+ const target = document[rule.in];
2632
+ if (!Array.isArray(target))
2633
+ return [];
2634
+ const row = target.find((candidate) => isEntry(candidate) && candidate[rule.matching] === entry[rule.field]);
2635
+ if (!row || row[rule.target] !== rule.equals)
2636
+ return [];
2637
+ return [
2638
+ `${rule.in} ${show(entry[rule.field])} has ${rule.target} ${show(rule.equals)}, so this is refused. ${rule.because}`
2639
+ ];
2640
+ }
2586
2641
  case "unique":
2587
2642
  case "linkedOnce":
2588
2643
  case "exactlyOneActive":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atmn",
3
- "version": "2.0.132",
3
+ "version": "2.0.133",
4
4
  "license": "MIT",
5
5
  "description": "The CLI for Autumn — define your pricing in code, sync it to Autumn, and keep everything in version control.",
6
6
  "bin": {
@@ -49,5 +49,5 @@
49
49
  "open": "10.2.0",
50
50
  "yaml": "2.9.0"
51
51
  },
52
- "gitHead": "8076bc8446a24f08aed613ec1010d22cb044e543"
52
+ "gitHead": "18bcf1751d169fd7dae4c28638db67093a6c0687"
53
53
  }