@lacspace/commission 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -91,9 +91,9 @@ split(100, [
91
91
 
92
92
  ## Licensing
93
93
 
94
- This package is **free** under the **[Lacspace Free Licence](https://lacspace.com/licenses/lacspace-free-1.0)** — permissive freedoms. Use it in personal and commercial projects at no cost; just keep the notice.
94
+ This package is **free** under the **[Lacspace Free Licence](https://developer.lacspace.com/licenses/lacspace-free-1.0)** — permissive freedoms. Use it in personal and commercial projects at no cost; just keep the notice.
95
95
 
96
- Not every Lacspace package is free. We also offer **Commercial** (paid), **Client-specific**, and **Private** (proprietary) packages under separate terms. See the full **[Lacspace Licence Centre](https://lacspace.com/licenses)**.
96
+ Not every Lacspace package is free. We also offer **Commercial** (paid), **Client-specific**, and **Private** (proprietary) packages under separate terms. See the full **[Lacspace Licence Centre](https://developer.lacspace.com/licenses)**.
97
97
 
98
98
  <!-- LACSPACE-DEV-PLATFORM -->
99
99
 
@@ -101,7 +101,7 @@ Not every Lacspace package is free. We also offer **Commercial** (paid), **Clien
101
101
 
102
102
  ## The Lacspace Developer Platform
103
103
 
104
- `@lacspace/commission` is part of **63+ zero-dependency, isomorphic TypeScript packages**. Explore the ecosystem:
104
+ `@lacspace/commission` is part of **80+ zero-dependency, isomorphic TypeScript packages**. Explore the ecosystem:
105
105
 
106
106
  - 🗂️ **All packages** — https://developer.lacspace.com/packages
107
107
  - 🧭 **Developer handbook** — https://developer.lacspace.com/handbook
@@ -109,4 +109,4 @@ Not every Lacspace package is free. We also offer **Commercial** (paid), **Clien
109
109
  - 🖥️ **Finished app templates** — https://templates.lacspace.com
110
110
  - 🚀 **Scaffold a full app** — `npm create lacspace-app@latest`
111
111
 
112
- Free under the **[Lacspace Free Licence](https://lacspace.com/licenses/lacspace-free-1.0)** — a permissive, free-to-use licence.
112
+ Free under the **[Lacspace Free Licence](https://developer.lacspace.com/licenses/lacspace-free-1.0)** — a permissive, free-to-use licence.
package/dist/index.cjs CHANGED
@@ -41,7 +41,9 @@ function split(amount, shares) {
41
41
  if (total <= 0) {
42
42
  const base = Math.trunc(amt / n);
43
43
  const rem = amt - base * n;
44
- return shares.map((s, i) => ({ party: s.party, amount: base + (i < rem ? 1 : 0) }));
44
+ const step = rem >= 0 ? 1 : -1;
45
+ const count = Math.abs(rem);
46
+ return shares.map((s, i) => ({ party: s.party, amount: base + (i < count ? step : 0) }));
45
47
  }
46
48
  const ideals = shares.map((s) => amt * s.rate / total);
47
49
  const parts = shares.map((s, i) => {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * @lacspace/commission — commission & payout calculation engine.\n *\n * All money is expressed in **integer minor units** (e.g. cents, paisa).\n * There are no floating-point money values anywhere in this package.\n *\n * Isomorphic, dependency-free, side-effect-free.\n */\n\n/** Optional floor / cap applied to a computed commission (minor units). */\nexport interface CommissionBounds {\n /** Minimum commission (floor), in minor units. */\n min?: number;\n /** Maximum commission (cap), in minor units. */\n max?: number;\n}\n\n/** A single marginal bracket in a tiered rule. */\nexport interface Tier {\n /**\n * Upper boundary of this bracket, in minor units, or `null` for infinity.\n * The last tier in a list should use `upTo: null`.\n */\n upTo: number | null;\n /** Marginal rate applied to the portion of `amount` inside this bracket (0..1). */\n rate: number;\n}\n\n/** A commission rule: flat fee, percentage, or marginal tiers. */\nexport type CommissionRule =\n | ({ type: \"flat\"; amount: number } & CommissionBounds)\n | ({ type: \"percent\"; rate: number } & CommissionBounds)\n | ({ type: \"tiered\"; tiers: Tier[] } & CommissionBounds);\n\n/** Result of a commission computation (all money in minor units). */\nexport interface CommissionResult {\n /** Commission charged, in minor units. */\n commission: number;\n /** Amount left after commission (`amount - commission`), in minor units. */\n net: number;\n /** Effective rate = `commission / amount` (0..1); `0` when amount is `0`. */\n effectiveRate: number;\n}\n\n/** A payout share used by {@link split}. */\nexport interface Share {\n party: string;\n /** Relative weight of this share. Shares need not sum to 1. */\n rate: number;\n}\n\n/** One allocated slice returned by {@link split}. */\nexport interface SplitPart {\n party: string;\n /** Allocated amount, in minor units. */\n amount: number;\n}\n\n/**\n * Compute the commission for `amount` under `rule`.\n *\n * `amount` is truncated to an integer number of minor units. The raw\n * commission is rounded to the nearest minor unit, then clamped into\n * `[min, max]` when those bounds are present.\n */\nexport function commission(rule: CommissionRule, amount: number): CommissionResult {\n const amt = Math.trunc(amount);\n\n let raw: number;\n switch (rule.type) {\n case \"flat\":\n raw = rule.amount;\n break;\n case \"percent\":\n raw = amt * rule.rate;\n break;\n case \"tiered\": {\n let acc = 0;\n let lower = 0;\n for (const tier of rule.tiers) {\n const upper = tier.upTo === null ? amt : Math.min(tier.upTo, amt);\n if (upper > lower) {\n acc += (upper - lower) * tier.rate;\n lower = upper;\n }\n if (tier.upTo !== null && amt <= tier.upTo) break;\n }\n raw = acc;\n break;\n }\n }\n\n let c = Math.round(raw);\n if (rule.max !== undefined && c > rule.max) c = rule.max;\n if (rule.min !== undefined && c < rule.min) c = rule.min;\n\n const net = amt - c;\n const effectiveRate = amt === 0 ? 0 : c / amt;\n return { commission: c, net, effectiveRate };\n}\n\n/**\n * Split `amount` (minor units) proportionally across `shares`.\n *\n * Uses the largest-remainder method so the returned amounts always sum to\n * `amount` **exactly** — no minor units are lost or invented. When the shares'\n * rates sum to zero, `amount` is distributed as evenly as possible instead.\n */\nexport function split(amount: number, shares: Share[]): SplitPart[] {\n const n = shares.length;\n if (n === 0) return [];\n\n const amt = Math.trunc(amount);\n const total = shares.reduce((s, x) => s + x.rate, 0);\n\n // Degenerate weights: fall back to an even distribution.\n if (total <= 0) {\n const base = Math.trunc(amt / n);\n const rem = amt - base * n;\n return shares.map((s, i) => ({ party: s.party, amount: base + (i < rem ? 1 : 0) }));\n }\n\n const ideals = shares.map((s) => (amt * s.rate) / total);\n const parts: SplitPart[] = shares.map((s, i) => {\n const ideal = ideals[i] ?? 0;\n return { party: s.party, amount: Math.floor(ideal) };\n });\n\n const allocated = parts.reduce((a, p) => a + p.amount, 0);\n let remainder = amt - allocated;\n\n // Hand out the leftover minor units to the largest fractional parts first.\n const order = ideals\n .map((ideal, i) => ({ i, frac: ideal - Math.floor(ideal) }))\n .sort((a, b) => b.frac - a.frac);\n\n let k = 0;\n while (remainder > 0 && order.length > 0) {\n const target = order[k % order.length];\n if (target) {\n const part = parts[target.i];\n if (part) part.amount += 1;\n }\n remainder--;\n k++;\n }\n\n return parts;\n}\n"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * @lacspace/commission — commission & payout calculation engine.\n *\n * All money is expressed in **integer minor units** (e.g. cents, paisa).\n * There are no floating-point money values anywhere in this package.\n *\n * Isomorphic, dependency-free, side-effect-free.\n */\n\n/** Optional floor / cap applied to a computed commission (minor units). */\nexport interface CommissionBounds {\n /** Minimum commission (floor), in minor units. */\n min?: number;\n /** Maximum commission (cap), in minor units. */\n max?: number;\n}\n\n/** A single marginal bracket in a tiered rule. */\nexport interface Tier {\n /**\n * Upper boundary of this bracket, in minor units, or `null` for infinity.\n * The last tier in a list should use `upTo: null`.\n */\n upTo: number | null;\n /** Marginal rate applied to the portion of `amount` inside this bracket (0..1). */\n rate: number;\n}\n\n/** A commission rule: flat fee, percentage, or marginal tiers. */\nexport type CommissionRule =\n | ({ type: \"flat\"; amount: number } & CommissionBounds)\n | ({ type: \"percent\"; rate: number } & CommissionBounds)\n | ({ type: \"tiered\"; tiers: Tier[] } & CommissionBounds);\n\n/** Result of a commission computation (all money in minor units). */\nexport interface CommissionResult {\n /** Commission charged, in minor units. */\n commission: number;\n /** Amount left after commission (`amount - commission`), in minor units. */\n net: number;\n /** Effective rate = `commission / amount` (0..1); `0` when amount is `0`. */\n effectiveRate: number;\n}\n\n/** A payout share used by {@link split}. */\nexport interface Share {\n party: string;\n /** Relative weight of this share. Shares need not sum to 1. */\n rate: number;\n}\n\n/** One allocated slice returned by {@link split}. */\nexport interface SplitPart {\n party: string;\n /** Allocated amount, in minor units. */\n amount: number;\n}\n\n/**\n * Compute the commission for `amount` under `rule`.\n *\n * `amount` is truncated to an integer number of minor units. The raw\n * commission is rounded to the nearest minor unit, then clamped into\n * `[min, max]` when those bounds are present.\n */\nexport function commission(rule: CommissionRule, amount: number): CommissionResult {\n const amt = Math.trunc(amount);\n\n let raw: number;\n switch (rule.type) {\n case \"flat\":\n raw = rule.amount;\n break;\n case \"percent\":\n raw = amt * rule.rate;\n break;\n case \"tiered\": {\n let acc = 0;\n let lower = 0;\n for (const tier of rule.tiers) {\n const upper = tier.upTo === null ? amt : Math.min(tier.upTo, amt);\n if (upper > lower) {\n acc += (upper - lower) * tier.rate;\n lower = upper;\n }\n if (tier.upTo !== null && amt <= tier.upTo) break;\n }\n raw = acc;\n break;\n }\n }\n\n let c = Math.round(raw);\n if (rule.max !== undefined && c > rule.max) c = rule.max;\n if (rule.min !== undefined && c < rule.min) c = rule.min;\n\n const net = amt - c;\n const effectiveRate = amt === 0 ? 0 : c / amt;\n return { commission: c, net, effectiveRate };\n}\n\n/**\n * Split `amount` (minor units) proportionally across `shares`.\n *\n * Uses the largest-remainder method so the returned amounts always sum to\n * `amount` **exactly** — no minor units are lost or invented. When the shares'\n * rates sum to zero, `amount` is distributed as evenly as possible instead.\n */\nexport function split(amount: number, shares: Share[]): SplitPart[] {\n const n = shares.length;\n if (n === 0) return [];\n\n const amt = Math.trunc(amount);\n const total = shares.reduce((s, x) => s + x.rate, 0);\n\n // Degenerate weights: fall back to an even distribution.\n if (total <= 0) {\n const base = Math.trunc(amt / n);\n // `rem` may be negative when `amt` is negative; hand out the leftover units\n // one at a time in the matching direction so the parts still sum to `amt`.\n const rem = amt - base * n;\n const step = rem >= 0 ? 1 : -1;\n const count = Math.abs(rem);\n return shares.map((s, i) => ({ party: s.party, amount: base + (i < count ? step : 0) }));\n }\n\n const ideals = shares.map((s) => (amt * s.rate) / total);\n const parts: SplitPart[] = shares.map((s, i) => {\n const ideal = ideals[i] ?? 0;\n return { party: s.party, amount: Math.floor(ideal) };\n });\n\n const allocated = parts.reduce((a, p) => a + p.amount, 0);\n let remainder = amt - allocated;\n\n // Hand out the leftover minor units to the largest fractional parts first.\n const order = ideals\n .map((ideal, i) => ({ i, frac: ideal - Math.floor(ideal) }))\n .sort((a, b) => b.frac - a.frac);\n\n let k = 0;\n while (remainder > 0 && order.length > 0) {\n const target = order[k % order.length];\n if (target) {\n const part = parts[target.i];\n if (part) part.amount += 1;\n }\n remainder--;\n k++;\n }\n\n return parts;\n}\n"]}
package/dist/index.js CHANGED
@@ -39,7 +39,9 @@ function split(amount, shares) {
39
39
  if (total <= 0) {
40
40
  const base = Math.trunc(amt / n);
41
41
  const rem = amt - base * n;
42
- return shares.map((s, i) => ({ party: s.party, amount: base + (i < rem ? 1 : 0) }));
42
+ const step = rem >= 0 ? 1 : -1;
43
+ const count = Math.abs(rem);
44
+ return shares.map((s, i) => ({ party: s.party, amount: base + (i < count ? step : 0) }));
43
45
  }
44
46
  const ideals = shares.map((s) => amt * s.rate / total);
45
47
  const parts = shares.map((s, i) => {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @lacspace/commission — commission & payout calculation engine.\n *\n * All money is expressed in **integer minor units** (e.g. cents, paisa).\n * There are no floating-point money values anywhere in this package.\n *\n * Isomorphic, dependency-free, side-effect-free.\n */\n\n/** Optional floor / cap applied to a computed commission (minor units). */\nexport interface CommissionBounds {\n /** Minimum commission (floor), in minor units. */\n min?: number;\n /** Maximum commission (cap), in minor units. */\n max?: number;\n}\n\n/** A single marginal bracket in a tiered rule. */\nexport interface Tier {\n /**\n * Upper boundary of this bracket, in minor units, or `null` for infinity.\n * The last tier in a list should use `upTo: null`.\n */\n upTo: number | null;\n /** Marginal rate applied to the portion of `amount` inside this bracket (0..1). */\n rate: number;\n}\n\n/** A commission rule: flat fee, percentage, or marginal tiers. */\nexport type CommissionRule =\n | ({ type: \"flat\"; amount: number } & CommissionBounds)\n | ({ type: \"percent\"; rate: number } & CommissionBounds)\n | ({ type: \"tiered\"; tiers: Tier[] } & CommissionBounds);\n\n/** Result of a commission computation (all money in minor units). */\nexport interface CommissionResult {\n /** Commission charged, in minor units. */\n commission: number;\n /** Amount left after commission (`amount - commission`), in minor units. */\n net: number;\n /** Effective rate = `commission / amount` (0..1); `0` when amount is `0`. */\n effectiveRate: number;\n}\n\n/** A payout share used by {@link split}. */\nexport interface Share {\n party: string;\n /** Relative weight of this share. Shares need not sum to 1. */\n rate: number;\n}\n\n/** One allocated slice returned by {@link split}. */\nexport interface SplitPart {\n party: string;\n /** Allocated amount, in minor units. */\n amount: number;\n}\n\n/**\n * Compute the commission for `amount` under `rule`.\n *\n * `amount` is truncated to an integer number of minor units. The raw\n * commission is rounded to the nearest minor unit, then clamped into\n * `[min, max]` when those bounds are present.\n */\nexport function commission(rule: CommissionRule, amount: number): CommissionResult {\n const amt = Math.trunc(amount);\n\n let raw: number;\n switch (rule.type) {\n case \"flat\":\n raw = rule.amount;\n break;\n case \"percent\":\n raw = amt * rule.rate;\n break;\n case \"tiered\": {\n let acc = 0;\n let lower = 0;\n for (const tier of rule.tiers) {\n const upper = tier.upTo === null ? amt : Math.min(tier.upTo, amt);\n if (upper > lower) {\n acc += (upper - lower) * tier.rate;\n lower = upper;\n }\n if (tier.upTo !== null && amt <= tier.upTo) break;\n }\n raw = acc;\n break;\n }\n }\n\n let c = Math.round(raw);\n if (rule.max !== undefined && c > rule.max) c = rule.max;\n if (rule.min !== undefined && c < rule.min) c = rule.min;\n\n const net = amt - c;\n const effectiveRate = amt === 0 ? 0 : c / amt;\n return { commission: c, net, effectiveRate };\n}\n\n/**\n * Split `amount` (minor units) proportionally across `shares`.\n *\n * Uses the largest-remainder method so the returned amounts always sum to\n * `amount` **exactly** — no minor units are lost or invented. When the shares'\n * rates sum to zero, `amount` is distributed as evenly as possible instead.\n */\nexport function split(amount: number, shares: Share[]): SplitPart[] {\n const n = shares.length;\n if (n === 0) return [];\n\n const amt = Math.trunc(amount);\n const total = shares.reduce((s, x) => s + x.rate, 0);\n\n // Degenerate weights: fall back to an even distribution.\n if (total <= 0) {\n const base = Math.trunc(amt / n);\n const rem = amt - base * n;\n return shares.map((s, i) => ({ party: s.party, amount: base + (i < rem ? 1 : 0) }));\n }\n\n const ideals = shares.map((s) => (amt * s.rate) / total);\n const parts: SplitPart[] = shares.map((s, i) => {\n const ideal = ideals[i] ?? 0;\n return { party: s.party, amount: Math.floor(ideal) };\n });\n\n const allocated = parts.reduce((a, p) => a + p.amount, 0);\n let remainder = amt - allocated;\n\n // Hand out the leftover minor units to the largest fractional parts first.\n const order = ideals\n .map((ideal, i) => ({ i, frac: ideal - Math.floor(ideal) }))\n .sort((a, b) => b.frac - a.frac);\n\n let k = 0;\n while (remainder > 0 && order.length > 0) {\n const target = order[k % order.length];\n if (target) {\n const part = parts[target.i];\n if (part) part.amount += 1;\n }\n remainder--;\n k++;\n }\n\n return parts;\n}\n"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @lacspace/commission — commission & payout calculation engine.\n *\n * All money is expressed in **integer minor units** (e.g. cents, paisa).\n * There are no floating-point money values anywhere in this package.\n *\n * Isomorphic, dependency-free, side-effect-free.\n */\n\n/** Optional floor / cap applied to a computed commission (minor units). */\nexport interface CommissionBounds {\n /** Minimum commission (floor), in minor units. */\n min?: number;\n /** Maximum commission (cap), in minor units. */\n max?: number;\n}\n\n/** A single marginal bracket in a tiered rule. */\nexport interface Tier {\n /**\n * Upper boundary of this bracket, in minor units, or `null` for infinity.\n * The last tier in a list should use `upTo: null`.\n */\n upTo: number | null;\n /** Marginal rate applied to the portion of `amount` inside this bracket (0..1). */\n rate: number;\n}\n\n/** A commission rule: flat fee, percentage, or marginal tiers. */\nexport type CommissionRule =\n | ({ type: \"flat\"; amount: number } & CommissionBounds)\n | ({ type: \"percent\"; rate: number } & CommissionBounds)\n | ({ type: \"tiered\"; tiers: Tier[] } & CommissionBounds);\n\n/** Result of a commission computation (all money in minor units). */\nexport interface CommissionResult {\n /** Commission charged, in minor units. */\n commission: number;\n /** Amount left after commission (`amount - commission`), in minor units. */\n net: number;\n /** Effective rate = `commission / amount` (0..1); `0` when amount is `0`. */\n effectiveRate: number;\n}\n\n/** A payout share used by {@link split}. */\nexport interface Share {\n party: string;\n /** Relative weight of this share. Shares need not sum to 1. */\n rate: number;\n}\n\n/** One allocated slice returned by {@link split}. */\nexport interface SplitPart {\n party: string;\n /** Allocated amount, in minor units. */\n amount: number;\n}\n\n/**\n * Compute the commission for `amount` under `rule`.\n *\n * `amount` is truncated to an integer number of minor units. The raw\n * commission is rounded to the nearest minor unit, then clamped into\n * `[min, max]` when those bounds are present.\n */\nexport function commission(rule: CommissionRule, amount: number): CommissionResult {\n const amt = Math.trunc(amount);\n\n let raw: number;\n switch (rule.type) {\n case \"flat\":\n raw = rule.amount;\n break;\n case \"percent\":\n raw = amt * rule.rate;\n break;\n case \"tiered\": {\n let acc = 0;\n let lower = 0;\n for (const tier of rule.tiers) {\n const upper = tier.upTo === null ? amt : Math.min(tier.upTo, amt);\n if (upper > lower) {\n acc += (upper - lower) * tier.rate;\n lower = upper;\n }\n if (tier.upTo !== null && amt <= tier.upTo) break;\n }\n raw = acc;\n break;\n }\n }\n\n let c = Math.round(raw);\n if (rule.max !== undefined && c > rule.max) c = rule.max;\n if (rule.min !== undefined && c < rule.min) c = rule.min;\n\n const net = amt - c;\n const effectiveRate = amt === 0 ? 0 : c / amt;\n return { commission: c, net, effectiveRate };\n}\n\n/**\n * Split `amount` (minor units) proportionally across `shares`.\n *\n * Uses the largest-remainder method so the returned amounts always sum to\n * `amount` **exactly** — no minor units are lost or invented. When the shares'\n * rates sum to zero, `amount` is distributed as evenly as possible instead.\n */\nexport function split(amount: number, shares: Share[]): SplitPart[] {\n const n = shares.length;\n if (n === 0) return [];\n\n const amt = Math.trunc(amount);\n const total = shares.reduce((s, x) => s + x.rate, 0);\n\n // Degenerate weights: fall back to an even distribution.\n if (total <= 0) {\n const base = Math.trunc(amt / n);\n // `rem` may be negative when `amt` is negative; hand out the leftover units\n // one at a time in the matching direction so the parts still sum to `amt`.\n const rem = amt - base * n;\n const step = rem >= 0 ? 1 : -1;\n const count = Math.abs(rem);\n return shares.map((s, i) => ({ party: s.party, amount: base + (i < count ? step : 0) }));\n }\n\n const ideals = shares.map((s) => (amt * s.rate) / total);\n const parts: SplitPart[] = shares.map((s, i) => {\n const ideal = ideals[i] ?? 0;\n return { party: s.party, amount: Math.floor(ideal) };\n });\n\n const allocated = parts.reduce((a, p) => a + p.amount, 0);\n let remainder = amt - allocated;\n\n // Hand out the leftover minor units to the largest fractional parts first.\n const order = ideals\n .map((ideal, i) => ({ i, frac: ideal - Math.floor(ideal) }))\n .sort((a, b) => b.frac - a.frac);\n\n let k = 0;\n while (remainder > 0 && order.length > 0) {\n const target = order[k % order.length];\n if (target) {\n const part = parts[target.i];\n if (part) part.amount += 1;\n }\n remainder--;\n k++;\n }\n\n return parts;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lacspace/commission",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Commission & payout calculation engine — flat, percentage and marginal-tiered rules with min/cap, plus exact proportional split with remainder distribution. Integer minor units, zero floats. Isomorphic (Node, edge, browser).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",