@lacspace/commission 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,51 @@
1
+ Lacspace Free Licence
2
+ Version 1.0, August 2026
3
+
4
+ Copyright (c) 2026 Lacspace
5
+
6
+ PREAMBLE
7
+
8
+ This software is published by Lacspace under the Lacspace Free Licence โ€” a free,
9
+ permissive licence that lets you use this software for any purpose, including in
10
+ commercial products and services, at no cost. It grants the same freedoms as
11
+ common permissive open-source licences; the only condition is that this notice
12
+ travels with the software. The canonical, always-current text of this licence is
13
+ maintained at https://lacspace.com/licenses/lacspace-free-1.0
14
+
15
+ GRANT OF RIGHTS
16
+
17
+ Permission is hereby granted, free of charge, to any person or organisation
18
+ obtaining a copy of this software and its associated documentation and data files
19
+ (the "Software"), to deal in the Software without restriction, including without
20
+ limitation the rights to use, copy, modify, merge, publish, distribute,
21
+ sublicense, and/or sell copies of the Software, and to permit persons to whom the
22
+ Software is furnished to do so, subject to the conditions below. These rights are
23
+ granted for any purpose, personal or commercial, and are perpetual, worldwide,
24
+ non-exclusive, and royalty-free.
25
+
26
+ CONDITIONS
27
+
28
+ The above copyright notice, this permission notice, and the name of this licence
29
+ ("Lacspace Free Licence") shall be included in all copies or substantial portions
30
+ of the Software.
31
+
32
+ TRADEMARKS
33
+
34
+ This licence does not grant permission to use the trade names, trademarks, service
35
+ marks, logos, or product names of Lacspace, except as required to reproduce the
36
+ notice above or to describe the origin of the Software in a truthful manner.
37
+
38
+ DISCLAIMER OF WARRANTY AND LIMITATION OF LIABILITY
39
+
40
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
41
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
42
+ FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
43
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN
44
+ AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION
45
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
46
+
47
+ ---
48
+
49
+ The Lacspace Free Licence is a source-available, permissive licence and is not (as
50
+ of this version) an OSI-approved licence. In substance it grants the same freedoms
51
+ as the MIT Licence. Learn more at https://lacspace.com/licenses
package/README.md ADDED
@@ -0,0 +1,112 @@
1
+ <div align="center">
2
+
3
+ # @lacspace/commission
4
+
5
+ **A commission & payout calculation engine โ€” flat, percentage and marginal-tiered rules, plus exact proportional splits.**
6
+
7
+ [![npm version](https://img.shields.io/npm/v/@lacspace/commission?color=%2316a34a&label=npm)](https://www.npmjs.com/package/@lacspace/commission)
8
+ [![install size](https://packagephobia.com/badge?p=@lacspace/commission)](https://packagephobia.com/result?p=@lacspace/commission)
9
+ [![minzipped](https://img.shields.io/bundlephobia/minzip/@lacspace/commission?label=minzip)](https://bundlephobia.com/package/@lacspace/commission)
10
+ [![types](https://img.shields.io/badge/types-included-blue)](https://www.npmjs.com/package/@lacspace/commission)
11
+ [![license](https://img.shields.io/npm/l/@lacspace/commission?color=green)](https://github.com/lacspace/npm-packages/blob/main/LICENSE)
12
+
13
+ </div>
14
+
15
+ > Every marketplace and sales tool re-implements the same money maths badly: what commission does this sale earn, what's left for the seller, and how do you split a payout across several parties **without losing a cent**? This does all three, correctly, in integer minor units โ€” no floats, no rounding drift.
16
+
17
+ - ๐Ÿ’ฐ **Three rule kinds** โ€” `flat` fee, `percent`, or `tiered` marginal brackets
18
+ - ๐Ÿงข **Bounds** โ€” optional `min` (floor) and `max` (cap) on any rule
19
+ - โœ‚๏ธ **Exact splits** โ€” proportional allocation where the parts always sum to the total
20
+ - ๐Ÿ”ข **Integer minor units** โ€” cents / paisa in, cents / paisa out, never a float
21
+ - โšก Isomorphic โ€” Node, edge runtimes & browsers ยท ๐Ÿ“ฆ ESM + CJS ยท zero dependencies ยท fully typed
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ npm install @lacspace/commission # or pnpm add / yarn add / bun add
27
+ ```
28
+
29
+ ## Commission rules
30
+
31
+ ```ts
32
+ import { commission } from "@lacspace/commission";
33
+
34
+ // 15% of a $10.00 sale (amounts are in cents)
35
+ commission({ type: "percent", rate: 0.15 }, 1000);
36
+ // โ†’ { commission: 150, net: 850, effectiveRate: 0.15 }
37
+
38
+ // flat fee, floored to at least 50c and capped at $1.00
39
+ commission({ type: "flat", amount: 5, min: 50, max: 100 }, 1000);
40
+ // โ†’ { commission: 50, net: 950, effectiveRate: 0.05 }
41
+ ```
42
+
43
+ ## Marginal tiers
44
+
45
+ ```ts
46
+ import { commission } from "@lacspace/commission";
47
+
48
+ const rule = {
49
+ type: "tiered" as const,
50
+ tiers: [
51
+ { upTo: 1000, rate: 0.1 }, // first $10.00 @ 10%
52
+ { upTo: 5000, rate: 0.05 }, // next $40.00 @ 5%
53
+ { upTo: null, rate: 0.02 }, // remainder @ 2%
54
+ ],
55
+ };
56
+
57
+ commission(rule, 6000);
58
+ // 0..1000 โ†’ 100, 1000..5000 โ†’ 200, 5000..6000 โ†’ 20
59
+ // โ†’ { commission: 320, net: 5680, effectiveRate: 0.0533โ€ฆ }
60
+ ```
61
+
62
+ ## Splitting a payout
63
+
64
+ ```ts
65
+ import { split } from "@lacspace/commission";
66
+
67
+ // share $10.00 70/30 โ€” the parts always sum to exactly the total
68
+ split(1000, [
69
+ { party: "seller", rate: 0.7 },
70
+ { party: "platform", rate: 0.3 },
71
+ ]);
72
+ // โ†’ [ { party: "seller", amount: 700 }, { party: "platform", amount: 300 } ]
73
+
74
+ // awkward thirds: the leftover cent is handed to the largest remainder
75
+ split(100, [
76
+ { party: "a", rate: 1 },
77
+ { party: "b", rate: 1 },
78
+ { party: "c", rate: 1 },
79
+ ]);
80
+ // โ†’ [ { party: "a", amount: 34 }, { party: "b", amount: 33 }, { party: "c", amount: 33 } ]
81
+ ```
82
+
83
+ ## API
84
+
85
+ | Function | Description |
86
+ | --- | --- |
87
+ | `commission(rule, amount)` | `{ commission, net, effectiveRate }` โ€” charge for `amount` under `rule` |
88
+ | `split(amount, shares)` | `{ party, amount }[]` โ€” proportional split whose parts sum to `amount` exactly |
89
+
90
+ **Rule** is `{ type: "flat"; amount }` \| `{ type: "percent"; rate /*0..1*/ }` \| `{ type: "tiered"; tiers }`, each optionally with `{ min?, max? }`. Tiers are marginal brackets `{ upTo: number \| null; rate }`, the last with `upTo: null` for infinity. All money is in **integer minor units**; the raw commission is rounded to the nearest unit then clamped into `[min, max]`.
91
+
92
+ ## Licensing
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.
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)**.
97
+
98
+ <!-- LACSPACE-DEV-PLATFORM -->
99
+
100
+ ---
101
+
102
+ ## The Lacspace Developer Platform
103
+
104
+ `@lacspace/commission` is part of **63+ zero-dependency, isomorphic TypeScript packages**. Explore the ecosystem:
105
+
106
+ - ๐Ÿ—‚๏ธ **All packages** โ€” https://developer.lacspace.com/packages
107
+ - ๐Ÿงญ **Developer handbook** โ€” https://developer.lacspace.com/handbook
108
+ - ๐Ÿงช **Live playground** โ€” https://developer.lacspace.com/playground
109
+ - ๐Ÿ–ฅ๏ธ **Finished app templates** โ€” https://templates.lacspace.com
110
+ - ๐Ÿš€ **Scaffold a full app** โ€” `npm create lacspace-app@latest`
111
+
112
+ Free under the **[Lacspace Free Licence](https://lacspace.com/licenses/lacspace-free-1.0)** โ€” a permissive, free-to-use licence.
package/dist/index.cjs ADDED
@@ -0,0 +1,70 @@
1
+ 'use strict';
2
+
3
+ // src/index.ts
4
+ function commission(rule, amount) {
5
+ const amt = Math.trunc(amount);
6
+ let raw;
7
+ switch (rule.type) {
8
+ case "flat":
9
+ raw = rule.amount;
10
+ break;
11
+ case "percent":
12
+ raw = amt * rule.rate;
13
+ break;
14
+ case "tiered": {
15
+ let acc = 0;
16
+ let lower = 0;
17
+ for (const tier of rule.tiers) {
18
+ const upper = tier.upTo === null ? amt : Math.min(tier.upTo, amt);
19
+ if (upper > lower) {
20
+ acc += (upper - lower) * tier.rate;
21
+ lower = upper;
22
+ }
23
+ if (tier.upTo !== null && amt <= tier.upTo) break;
24
+ }
25
+ raw = acc;
26
+ break;
27
+ }
28
+ }
29
+ let c = Math.round(raw);
30
+ if (rule.max !== void 0 && c > rule.max) c = rule.max;
31
+ if (rule.min !== void 0 && c < rule.min) c = rule.min;
32
+ const net = amt - c;
33
+ const effectiveRate = amt === 0 ? 0 : c / amt;
34
+ return { commission: c, net, effectiveRate };
35
+ }
36
+ function split(amount, shares) {
37
+ const n = shares.length;
38
+ if (n === 0) return [];
39
+ const amt = Math.trunc(amount);
40
+ const total = shares.reduce((s, x) => s + x.rate, 0);
41
+ if (total <= 0) {
42
+ const base = Math.trunc(amt / n);
43
+ const rem = amt - base * n;
44
+ return shares.map((s, i) => ({ party: s.party, amount: base + (i < rem ? 1 : 0) }));
45
+ }
46
+ const ideals = shares.map((s) => amt * s.rate / total);
47
+ const parts = shares.map((s, i) => {
48
+ const ideal = ideals[i] ?? 0;
49
+ return { party: s.party, amount: Math.floor(ideal) };
50
+ });
51
+ const allocated = parts.reduce((a, p) => a + p.amount, 0);
52
+ let remainder = amt - allocated;
53
+ const order = ideals.map((ideal, i) => ({ i, frac: ideal - Math.floor(ideal) })).sort((a, b) => b.frac - a.frac);
54
+ let k = 0;
55
+ while (remainder > 0 && order.length > 0) {
56
+ const target = order[k % order.length];
57
+ if (target) {
58
+ const part = parts[target.i];
59
+ if (part) part.amount += 1;
60
+ }
61
+ remainder--;
62
+ k++;
63
+ }
64
+ return parts;
65
+ }
66
+
67
+ exports.commission = commission;
68
+ exports.split = split;
69
+ //# sourceMappingURL=index.cjs.map
70
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +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"]}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * @lacspace/commission โ€” commission & payout calculation engine.
3
+ *
4
+ * All money is expressed in **integer minor units** (e.g. cents, paisa).
5
+ * There are no floating-point money values anywhere in this package.
6
+ *
7
+ * Isomorphic, dependency-free, side-effect-free.
8
+ */
9
+ /** Optional floor / cap applied to a computed commission (minor units). */
10
+ interface CommissionBounds {
11
+ /** Minimum commission (floor), in minor units. */
12
+ min?: number;
13
+ /** Maximum commission (cap), in minor units. */
14
+ max?: number;
15
+ }
16
+ /** A single marginal bracket in a tiered rule. */
17
+ interface Tier {
18
+ /**
19
+ * Upper boundary of this bracket, in minor units, or `null` for infinity.
20
+ * The last tier in a list should use `upTo: null`.
21
+ */
22
+ upTo: number | null;
23
+ /** Marginal rate applied to the portion of `amount` inside this bracket (0..1). */
24
+ rate: number;
25
+ }
26
+ /** A commission rule: flat fee, percentage, or marginal tiers. */
27
+ type CommissionRule = ({
28
+ type: "flat";
29
+ amount: number;
30
+ } & CommissionBounds) | ({
31
+ type: "percent";
32
+ rate: number;
33
+ } & CommissionBounds) | ({
34
+ type: "tiered";
35
+ tiers: Tier[];
36
+ } & CommissionBounds);
37
+ /** Result of a commission computation (all money in minor units). */
38
+ interface CommissionResult {
39
+ /** Commission charged, in minor units. */
40
+ commission: number;
41
+ /** Amount left after commission (`amount - commission`), in minor units. */
42
+ net: number;
43
+ /** Effective rate = `commission / amount` (0..1); `0` when amount is `0`. */
44
+ effectiveRate: number;
45
+ }
46
+ /** A payout share used by {@link split}. */
47
+ interface Share {
48
+ party: string;
49
+ /** Relative weight of this share. Shares need not sum to 1. */
50
+ rate: number;
51
+ }
52
+ /** One allocated slice returned by {@link split}. */
53
+ interface SplitPart {
54
+ party: string;
55
+ /** Allocated amount, in minor units. */
56
+ amount: number;
57
+ }
58
+ /**
59
+ * Compute the commission for `amount` under `rule`.
60
+ *
61
+ * `amount` is truncated to an integer number of minor units. The raw
62
+ * commission is rounded to the nearest minor unit, then clamped into
63
+ * `[min, max]` when those bounds are present.
64
+ */
65
+ declare function commission(rule: CommissionRule, amount: number): CommissionResult;
66
+ /**
67
+ * Split `amount` (minor units) proportionally across `shares`.
68
+ *
69
+ * Uses the largest-remainder method so the returned amounts always sum to
70
+ * `amount` **exactly** โ€” no minor units are lost or invented. When the shares'
71
+ * rates sum to zero, `amount` is distributed as evenly as possible instead.
72
+ */
73
+ declare function split(amount: number, shares: Share[]): SplitPart[];
74
+
75
+ export { type CommissionBounds, type CommissionResult, type CommissionRule, type Share, type SplitPart, type Tier, commission, split };
@@ -0,0 +1,75 @@
1
+ /**
2
+ * @lacspace/commission โ€” commission & payout calculation engine.
3
+ *
4
+ * All money is expressed in **integer minor units** (e.g. cents, paisa).
5
+ * There are no floating-point money values anywhere in this package.
6
+ *
7
+ * Isomorphic, dependency-free, side-effect-free.
8
+ */
9
+ /** Optional floor / cap applied to a computed commission (minor units). */
10
+ interface CommissionBounds {
11
+ /** Minimum commission (floor), in minor units. */
12
+ min?: number;
13
+ /** Maximum commission (cap), in minor units. */
14
+ max?: number;
15
+ }
16
+ /** A single marginal bracket in a tiered rule. */
17
+ interface Tier {
18
+ /**
19
+ * Upper boundary of this bracket, in minor units, or `null` for infinity.
20
+ * The last tier in a list should use `upTo: null`.
21
+ */
22
+ upTo: number | null;
23
+ /** Marginal rate applied to the portion of `amount` inside this bracket (0..1). */
24
+ rate: number;
25
+ }
26
+ /** A commission rule: flat fee, percentage, or marginal tiers. */
27
+ type CommissionRule = ({
28
+ type: "flat";
29
+ amount: number;
30
+ } & CommissionBounds) | ({
31
+ type: "percent";
32
+ rate: number;
33
+ } & CommissionBounds) | ({
34
+ type: "tiered";
35
+ tiers: Tier[];
36
+ } & CommissionBounds);
37
+ /** Result of a commission computation (all money in minor units). */
38
+ interface CommissionResult {
39
+ /** Commission charged, in minor units. */
40
+ commission: number;
41
+ /** Amount left after commission (`amount - commission`), in minor units. */
42
+ net: number;
43
+ /** Effective rate = `commission / amount` (0..1); `0` when amount is `0`. */
44
+ effectiveRate: number;
45
+ }
46
+ /** A payout share used by {@link split}. */
47
+ interface Share {
48
+ party: string;
49
+ /** Relative weight of this share. Shares need not sum to 1. */
50
+ rate: number;
51
+ }
52
+ /** One allocated slice returned by {@link split}. */
53
+ interface SplitPart {
54
+ party: string;
55
+ /** Allocated amount, in minor units. */
56
+ amount: number;
57
+ }
58
+ /**
59
+ * Compute the commission for `amount` under `rule`.
60
+ *
61
+ * `amount` is truncated to an integer number of minor units. The raw
62
+ * commission is rounded to the nearest minor unit, then clamped into
63
+ * `[min, max]` when those bounds are present.
64
+ */
65
+ declare function commission(rule: CommissionRule, amount: number): CommissionResult;
66
+ /**
67
+ * Split `amount` (minor units) proportionally across `shares`.
68
+ *
69
+ * Uses the largest-remainder method so the returned amounts always sum to
70
+ * `amount` **exactly** โ€” no minor units are lost or invented. When the shares'
71
+ * rates sum to zero, `amount` is distributed as evenly as possible instead.
72
+ */
73
+ declare function split(amount: number, shares: Share[]): SplitPart[];
74
+
75
+ export { type CommissionBounds, type CommissionResult, type CommissionRule, type Share, type SplitPart, type Tier, commission, split };
package/dist/index.js ADDED
@@ -0,0 +1,67 @@
1
+ // src/index.ts
2
+ function commission(rule, amount) {
3
+ const amt = Math.trunc(amount);
4
+ let raw;
5
+ switch (rule.type) {
6
+ case "flat":
7
+ raw = rule.amount;
8
+ break;
9
+ case "percent":
10
+ raw = amt * rule.rate;
11
+ break;
12
+ case "tiered": {
13
+ let acc = 0;
14
+ let lower = 0;
15
+ for (const tier of rule.tiers) {
16
+ const upper = tier.upTo === null ? amt : Math.min(tier.upTo, amt);
17
+ if (upper > lower) {
18
+ acc += (upper - lower) * tier.rate;
19
+ lower = upper;
20
+ }
21
+ if (tier.upTo !== null && amt <= tier.upTo) break;
22
+ }
23
+ raw = acc;
24
+ break;
25
+ }
26
+ }
27
+ let c = Math.round(raw);
28
+ if (rule.max !== void 0 && c > rule.max) c = rule.max;
29
+ if (rule.min !== void 0 && c < rule.min) c = rule.min;
30
+ const net = amt - c;
31
+ const effectiveRate = amt === 0 ? 0 : c / amt;
32
+ return { commission: c, net, effectiveRate };
33
+ }
34
+ function split(amount, shares) {
35
+ const n = shares.length;
36
+ if (n === 0) return [];
37
+ const amt = Math.trunc(amount);
38
+ const total = shares.reduce((s, x) => s + x.rate, 0);
39
+ if (total <= 0) {
40
+ const base = Math.trunc(amt / n);
41
+ const rem = amt - base * n;
42
+ return shares.map((s, i) => ({ party: s.party, amount: base + (i < rem ? 1 : 0) }));
43
+ }
44
+ const ideals = shares.map((s) => amt * s.rate / total);
45
+ const parts = shares.map((s, i) => {
46
+ const ideal = ideals[i] ?? 0;
47
+ return { party: s.party, amount: Math.floor(ideal) };
48
+ });
49
+ const allocated = parts.reduce((a, p) => a + p.amount, 0);
50
+ let remainder = amt - allocated;
51
+ const order = ideals.map((ideal, i) => ({ i, frac: ideal - Math.floor(ideal) })).sort((a, b) => b.frac - a.frac);
52
+ let k = 0;
53
+ while (remainder > 0 && order.length > 0) {
54
+ const target = order[k % order.length];
55
+ if (target) {
56
+ const part = parts[target.i];
57
+ if (part) part.amount += 1;
58
+ }
59
+ remainder--;
60
+ k++;
61
+ }
62
+ return parts;
63
+ }
64
+
65
+ export { commission, split };
66
+ //# sourceMappingURL=index.js.map
67
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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"]}
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@lacspace/commission",
3
+ "version": "1.0.0",
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
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "sideEffects": false,
25
+ "scripts": {
26
+ "build": "tsup",
27
+ "prepublishOnly": "npm run build"
28
+ },
29
+ "keywords": [
30
+ "commission",
31
+ "payout",
32
+ "marketplace",
33
+ "sales-commission",
34
+ "tiered",
35
+ "percentage",
36
+ "revenue-share",
37
+ "split",
38
+ "minor-units",
39
+ "fintech",
40
+ "isomorphic",
41
+ "typescript"
42
+ ],
43
+ "author": "Lacspace <contact@lacspace.com>",
44
+ "license": "SEE LICENSE IN LICENSE",
45
+ "homepage": "https://developer.lacspace.com/packages/commission",
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/lacspace/npm-packages.git",
49
+ "directory": "commission"
50
+ },
51
+ "bugs": {
52
+ "url": "https://github.com/lacspace/npm-packages/issues"
53
+ },
54
+ "engines": {
55
+ "node": ">=18"
56
+ },
57
+ "dependencies": {},
58
+ "publishConfig": {
59
+ "access": "public"
60
+ }
61
+ }