@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 +51 -0
- package/README.md +112 -0
- package/dist/index.cjs +70 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +75 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.js +67 -0
- package/dist/index.js.map +1 -0
- package/package.json +61 -0
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
|
+
[](https://www.npmjs.com/package/@lacspace/commission)
|
|
8
|
+
[](https://packagephobia.com/result?p=@lacspace/commission)
|
|
9
|
+
[](https://bundlephobia.com/package/@lacspace/commission)
|
|
10
|
+
[](https://www.npmjs.com/package/@lacspace/commission)
|
|
11
|
+
[](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"]}
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|