@lacspace/coupon 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 +86 -0
- package/dist/index.cjs +69 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +76 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.js +66 -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,86 @@
|
|
|
1
|
+
# @lacspace/coupon
|
|
2
|
+
|
|
3
|
+
A small, dependable **discount / coupon engine** — percent, fixed and free-shipping codes with validity windows, minimum-subtotal thresholds, discount caps and usage limits. All money is in **integer minor units** (e.g. cents), so there is no floating-point drift.
|
|
4
|
+
|
|
5
|
+
- **Zero runtime dependencies**
|
|
6
|
+
- **Isomorphic** — Node, edge runtimes and browsers
|
|
7
|
+
- **TypeScript-first**, strict types
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @lacspace/coupon
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { applyCoupon, validateCoupon, type Coupon } from "@lacspace/coupon";
|
|
17
|
+
|
|
18
|
+
const coupon: Coupon = {
|
|
19
|
+
code: "SAVE20",
|
|
20
|
+
type: "percent",
|
|
21
|
+
value: 20, // 20%
|
|
22
|
+
maxDiscount: 500, // never more than $5.00
|
|
23
|
+
minSubtotal: 1000, // order must be at least $10.00
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
applyCoupon(coupon, { subtotal: 3000, shipping: 400 });
|
|
27
|
+
// { valid: true, discount: 500, shippingDiscount: 0, total: 2900 }
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## API
|
|
31
|
+
|
|
32
|
+
### `validateCoupon(coupon, { subtotal, now? }): { valid, reason? }`
|
|
33
|
+
|
|
34
|
+
Checks the validity window (`not-yet-started` / `expired`), `minSubtotal` (`below-min-subtotal`) and `usageLimit` vs `used` (`usage-limit-reached`). Does not compute a discount.
|
|
35
|
+
|
|
36
|
+
### `applyCoupon(coupon, { subtotal, shipping?, now? }): CouponResult`
|
|
37
|
+
|
|
38
|
+
Validates, then computes the discount:
|
|
39
|
+
|
|
40
|
+
| type | discount |
|
|
41
|
+
| --------------- | ---------------------------------------------------- |
|
|
42
|
+
| `percent` | `round(subtotal * value / 100)`, capped by `maxDiscount` and the subtotal |
|
|
43
|
+
| `fixed` | `min(value, subtotal)`, capped by `maxDiscount` |
|
|
44
|
+
| `free-shipping` | `shippingDiscount = shipping` |
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
total = max(0, subtotal - discount + shipping - shippingDiscount)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
When the coupon is invalid, discounts are `0` and the total is the untouched `subtotal + shipping`.
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
applyCoupon({ code: "FREESHIP", type: "free-shipping" }, { subtotal: 1000, shipping: 300 });
|
|
54
|
+
// { valid: true, discount: 0, shippingDiscount: 300, total: 1000 }
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Types
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
interface Coupon {
|
|
61
|
+
code: string;
|
|
62
|
+
type: "percent" | "fixed" | "free-shipping";
|
|
63
|
+
value?: number; // percent 0..100, or minor units for fixed
|
|
64
|
+
minSubtotal?: number;
|
|
65
|
+
maxDiscount?: number;
|
|
66
|
+
startsAt?: string; // ISO-8601
|
|
67
|
+
endsAt?: string; // ISO-8601
|
|
68
|
+
usageLimit?: number;
|
|
69
|
+
used?: number;
|
|
70
|
+
currency?: string;
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## The Lacspace Developer Platform
|
|
77
|
+
|
|
78
|
+
`@lacspace/coupon` is part of **63+ zero-dependency, isomorphic TypeScript packages**. Explore the ecosystem:
|
|
79
|
+
|
|
80
|
+
- 🗂️ **All packages** — https://developer.lacspace.com/packages
|
|
81
|
+
- 🧭 **Developer handbook** — https://developer.lacspace.com/handbook
|
|
82
|
+
- 🧪 **Live playground** — https://developer.lacspace.com/playground
|
|
83
|
+
- 🖥️ **Finished app templates** — https://templates.lacspace.com
|
|
84
|
+
- 🚀 **Scaffold a full app** — `npm create lacspace-app@latest`
|
|
85
|
+
|
|
86
|
+
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,69 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
function validateCoupon(coupon, ctx) {
|
|
5
|
+
const now = ctx.now ?? /* @__PURE__ */ new Date();
|
|
6
|
+
const t = now.getTime();
|
|
7
|
+
if (coupon.startsAt) {
|
|
8
|
+
const starts = new Date(coupon.startsAt).getTime();
|
|
9
|
+
if (!Number.isNaN(starts) && t < starts) {
|
|
10
|
+
return { valid: false, reason: "not-yet-started" };
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
if (coupon.endsAt) {
|
|
14
|
+
const ends = new Date(coupon.endsAt).getTime();
|
|
15
|
+
if (!Number.isNaN(ends) && t > ends) {
|
|
16
|
+
return { valid: false, reason: "expired" };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (typeof coupon.minSubtotal === "number" && ctx.subtotal < coupon.minSubtotal) {
|
|
20
|
+
return { valid: false, reason: "below-min-subtotal" };
|
|
21
|
+
}
|
|
22
|
+
if (typeof coupon.usageLimit === "number" && (coupon.used ?? 0) >= coupon.usageLimit) {
|
|
23
|
+
return { valid: false, reason: "usage-limit-reached" };
|
|
24
|
+
}
|
|
25
|
+
return { valid: true };
|
|
26
|
+
}
|
|
27
|
+
function applyCoupon(coupon, ctx) {
|
|
28
|
+
const subtotal = ctx.subtotal;
|
|
29
|
+
const shipping = ctx.shipping ?? 0;
|
|
30
|
+
const validation = validateCoupon(coupon, { subtotal, now: ctx.now });
|
|
31
|
+
if (!validation.valid) {
|
|
32
|
+
return {
|
|
33
|
+
...validation,
|
|
34
|
+
discount: 0,
|
|
35
|
+
shippingDiscount: 0,
|
|
36
|
+
total: Math.max(0, subtotal + shipping)
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
let discount = 0;
|
|
40
|
+
let shippingDiscount = 0;
|
|
41
|
+
switch (coupon.type) {
|
|
42
|
+
case "percent": {
|
|
43
|
+
const pct = coupon.value ?? 0;
|
|
44
|
+
discount = Math.round(subtotal * pct / 100);
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
case "fixed": {
|
|
48
|
+
const amount = coupon.value ?? 0;
|
|
49
|
+
discount = Math.min(amount, subtotal);
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
case "free-shipping": {
|
|
53
|
+
shippingDiscount = shipping;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (typeof coupon.maxDiscount === "number") {
|
|
58
|
+
discount = Math.min(discount, coupon.maxDiscount);
|
|
59
|
+
}
|
|
60
|
+
discount = Math.min(discount, subtotal);
|
|
61
|
+
discount = Math.max(0, discount);
|
|
62
|
+
const total = Math.max(0, subtotal - discount + shipping - shippingDiscount);
|
|
63
|
+
return { valid: true, discount, shippingDiscount, total };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
exports.applyCoupon = applyCoupon;
|
|
67
|
+
exports.validateCoupon = validateCoupon;
|
|
68
|
+
//# sourceMappingURL=index.cjs.map
|
|
69
|
+
//# 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/coupon\n *\n * Discount / coupon engine — percent, fixed and free-shipping codes with\n * validity windows, minimum-subtotal thresholds, discount caps and usage\n * limits. All money is expressed in integer minor units (e.g. cents) so there\n * is no floating-point drift.\n *\n * Zero runtime dependencies. Isomorphic (Node, edge, browser).\n */\n\n/** A discount coupon definition. */\nexport interface Coupon {\n /** The code a customer enters. */\n code: string;\n /** Kind of discount. */\n type: \"percent\" | \"fixed\" | \"free-shipping\";\n /**\n * For `percent`, a whole percentage 0..100. For `fixed`, an amount in minor\n * units. Ignored for `free-shipping`.\n */\n value?: number;\n /** Minimum order subtotal (minor units) required for the coupon to apply. */\n minSubtotal?: number;\n /** Maximum discount (minor units) the coupon may grant. */\n maxDiscount?: number;\n /** ISO-8601 timestamp before which the coupon is not yet valid. */\n startsAt?: string;\n /** ISO-8601 timestamp after which the coupon has expired. */\n endsAt?: string;\n /** How many times the coupon may be redeemed in total. */\n usageLimit?: number;\n /** How many times it has already been redeemed. */\n used?: number;\n /** Optional currency tag for downstream formatting. */\n currency?: string;\n}\n\n/** Result of validating a coupon against a context. */\nexport interface CouponValidation {\n valid: boolean;\n reason?: string;\n}\n\n/** Result of applying a coupon to an order. */\nexport interface CouponResult extends CouponValidation {\n /** Discount applied to the subtotal (minor units). */\n discount: number;\n /** Discount applied to shipping (minor units). */\n shippingDiscount: number;\n /** Final payable total (minor units). */\n total: number;\n}\n\n/**\n * Validate a coupon against an order context: checks the validity window,\n * minimum-subtotal threshold and usage limit. Does not compute any discount.\n */\nexport function validateCoupon(\n coupon: Coupon,\n ctx: { subtotal: number; now?: Date },\n): CouponValidation {\n const now = ctx.now ?? new Date();\n const t = now.getTime();\n\n if (coupon.startsAt) {\n const starts = new Date(coupon.startsAt).getTime();\n if (!Number.isNaN(starts) && t < starts) {\n return { valid: false, reason: \"not-yet-started\" };\n }\n }\n\n if (coupon.endsAt) {\n const ends = new Date(coupon.endsAt).getTime();\n if (!Number.isNaN(ends) && t > ends) {\n return { valid: false, reason: \"expired\" };\n }\n }\n\n if (typeof coupon.minSubtotal === \"number\" && ctx.subtotal < coupon.minSubtotal) {\n return { valid: false, reason: \"below-min-subtotal\" };\n }\n\n if (\n typeof coupon.usageLimit === \"number\" &&\n (coupon.used ?? 0) >= coupon.usageLimit\n ) {\n return { valid: false, reason: \"usage-limit-reached\" };\n }\n\n return { valid: true };\n}\n\n/**\n * Apply a coupon to an order. Validates first; when invalid, returns zero\n * discounts and the untouched total. When valid, computes the discount:\n *\n * - `percent` → `round(subtotal * value / 100)`, capped by `maxDiscount` and\n * never more than the subtotal.\n * - `fixed` → `min(value, subtotal)`, capped by `maxDiscount`.\n * - `free-shipping` → `shippingDiscount = shipping`.\n *\n * `total = max(0, subtotal - discount + shipping - shippingDiscount)`.\n */\nexport function applyCoupon(\n coupon: Coupon,\n ctx: { subtotal: number; shipping?: number; now?: Date },\n): CouponResult {\n const subtotal = ctx.subtotal;\n const shipping = ctx.shipping ?? 0;\n\n const validation = validateCoupon(coupon, { subtotal, now: ctx.now });\n if (!validation.valid) {\n return {\n ...validation,\n discount: 0,\n shippingDiscount: 0,\n total: Math.max(0, subtotal + shipping),\n };\n }\n\n let discount = 0;\n let shippingDiscount = 0;\n\n switch (coupon.type) {\n case \"percent\": {\n const pct = coupon.value ?? 0;\n discount = Math.round((subtotal * pct) / 100);\n break;\n }\n case \"fixed\": {\n const amount = coupon.value ?? 0;\n discount = Math.min(amount, subtotal);\n break;\n }\n case \"free-shipping\": {\n shippingDiscount = shipping;\n break;\n }\n }\n\n if (typeof coupon.maxDiscount === \"number\") {\n discount = Math.min(discount, coupon.maxDiscount);\n }\n discount = Math.min(discount, subtotal);\n discount = Math.max(0, discount);\n\n const total = Math.max(0, subtotal - discount + shipping - shippingDiscount);\n\n return { valid: true, discount, shippingDiscount, total };\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @lacspace/coupon
|
|
3
|
+
*
|
|
4
|
+
* Discount / coupon engine — percent, fixed and free-shipping codes with
|
|
5
|
+
* validity windows, minimum-subtotal thresholds, discount caps and usage
|
|
6
|
+
* limits. All money is expressed in integer minor units (e.g. cents) so there
|
|
7
|
+
* is no floating-point drift.
|
|
8
|
+
*
|
|
9
|
+
* Zero runtime dependencies. Isomorphic (Node, edge, browser).
|
|
10
|
+
*/
|
|
11
|
+
/** A discount coupon definition. */
|
|
12
|
+
interface Coupon {
|
|
13
|
+
/** The code a customer enters. */
|
|
14
|
+
code: string;
|
|
15
|
+
/** Kind of discount. */
|
|
16
|
+
type: "percent" | "fixed" | "free-shipping";
|
|
17
|
+
/**
|
|
18
|
+
* For `percent`, a whole percentage 0..100. For `fixed`, an amount in minor
|
|
19
|
+
* units. Ignored for `free-shipping`.
|
|
20
|
+
*/
|
|
21
|
+
value?: number;
|
|
22
|
+
/** Minimum order subtotal (minor units) required for the coupon to apply. */
|
|
23
|
+
minSubtotal?: number;
|
|
24
|
+
/** Maximum discount (minor units) the coupon may grant. */
|
|
25
|
+
maxDiscount?: number;
|
|
26
|
+
/** ISO-8601 timestamp before which the coupon is not yet valid. */
|
|
27
|
+
startsAt?: string;
|
|
28
|
+
/** ISO-8601 timestamp after which the coupon has expired. */
|
|
29
|
+
endsAt?: string;
|
|
30
|
+
/** How many times the coupon may be redeemed in total. */
|
|
31
|
+
usageLimit?: number;
|
|
32
|
+
/** How many times it has already been redeemed. */
|
|
33
|
+
used?: number;
|
|
34
|
+
/** Optional currency tag for downstream formatting. */
|
|
35
|
+
currency?: string;
|
|
36
|
+
}
|
|
37
|
+
/** Result of validating a coupon against a context. */
|
|
38
|
+
interface CouponValidation {
|
|
39
|
+
valid: boolean;
|
|
40
|
+
reason?: string;
|
|
41
|
+
}
|
|
42
|
+
/** Result of applying a coupon to an order. */
|
|
43
|
+
interface CouponResult extends CouponValidation {
|
|
44
|
+
/** Discount applied to the subtotal (minor units). */
|
|
45
|
+
discount: number;
|
|
46
|
+
/** Discount applied to shipping (minor units). */
|
|
47
|
+
shippingDiscount: number;
|
|
48
|
+
/** Final payable total (minor units). */
|
|
49
|
+
total: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Validate a coupon against an order context: checks the validity window,
|
|
53
|
+
* minimum-subtotal threshold and usage limit. Does not compute any discount.
|
|
54
|
+
*/
|
|
55
|
+
declare function validateCoupon(coupon: Coupon, ctx: {
|
|
56
|
+
subtotal: number;
|
|
57
|
+
now?: Date;
|
|
58
|
+
}): CouponValidation;
|
|
59
|
+
/**
|
|
60
|
+
* Apply a coupon to an order. Validates first; when invalid, returns zero
|
|
61
|
+
* discounts and the untouched total. When valid, computes the discount:
|
|
62
|
+
*
|
|
63
|
+
* - `percent` → `round(subtotal * value / 100)`, capped by `maxDiscount` and
|
|
64
|
+
* never more than the subtotal.
|
|
65
|
+
* - `fixed` → `min(value, subtotal)`, capped by `maxDiscount`.
|
|
66
|
+
* - `free-shipping` → `shippingDiscount = shipping`.
|
|
67
|
+
*
|
|
68
|
+
* `total = max(0, subtotal - discount + shipping - shippingDiscount)`.
|
|
69
|
+
*/
|
|
70
|
+
declare function applyCoupon(coupon: Coupon, ctx: {
|
|
71
|
+
subtotal: number;
|
|
72
|
+
shipping?: number;
|
|
73
|
+
now?: Date;
|
|
74
|
+
}): CouponResult;
|
|
75
|
+
|
|
76
|
+
export { type Coupon, type CouponResult, type CouponValidation, applyCoupon, validateCoupon };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @lacspace/coupon
|
|
3
|
+
*
|
|
4
|
+
* Discount / coupon engine — percent, fixed and free-shipping codes with
|
|
5
|
+
* validity windows, minimum-subtotal thresholds, discount caps and usage
|
|
6
|
+
* limits. All money is expressed in integer minor units (e.g. cents) so there
|
|
7
|
+
* is no floating-point drift.
|
|
8
|
+
*
|
|
9
|
+
* Zero runtime dependencies. Isomorphic (Node, edge, browser).
|
|
10
|
+
*/
|
|
11
|
+
/** A discount coupon definition. */
|
|
12
|
+
interface Coupon {
|
|
13
|
+
/** The code a customer enters. */
|
|
14
|
+
code: string;
|
|
15
|
+
/** Kind of discount. */
|
|
16
|
+
type: "percent" | "fixed" | "free-shipping";
|
|
17
|
+
/**
|
|
18
|
+
* For `percent`, a whole percentage 0..100. For `fixed`, an amount in minor
|
|
19
|
+
* units. Ignored for `free-shipping`.
|
|
20
|
+
*/
|
|
21
|
+
value?: number;
|
|
22
|
+
/** Minimum order subtotal (minor units) required for the coupon to apply. */
|
|
23
|
+
minSubtotal?: number;
|
|
24
|
+
/** Maximum discount (minor units) the coupon may grant. */
|
|
25
|
+
maxDiscount?: number;
|
|
26
|
+
/** ISO-8601 timestamp before which the coupon is not yet valid. */
|
|
27
|
+
startsAt?: string;
|
|
28
|
+
/** ISO-8601 timestamp after which the coupon has expired. */
|
|
29
|
+
endsAt?: string;
|
|
30
|
+
/** How many times the coupon may be redeemed in total. */
|
|
31
|
+
usageLimit?: number;
|
|
32
|
+
/** How many times it has already been redeemed. */
|
|
33
|
+
used?: number;
|
|
34
|
+
/** Optional currency tag for downstream formatting. */
|
|
35
|
+
currency?: string;
|
|
36
|
+
}
|
|
37
|
+
/** Result of validating a coupon against a context. */
|
|
38
|
+
interface CouponValidation {
|
|
39
|
+
valid: boolean;
|
|
40
|
+
reason?: string;
|
|
41
|
+
}
|
|
42
|
+
/** Result of applying a coupon to an order. */
|
|
43
|
+
interface CouponResult extends CouponValidation {
|
|
44
|
+
/** Discount applied to the subtotal (minor units). */
|
|
45
|
+
discount: number;
|
|
46
|
+
/** Discount applied to shipping (minor units). */
|
|
47
|
+
shippingDiscount: number;
|
|
48
|
+
/** Final payable total (minor units). */
|
|
49
|
+
total: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Validate a coupon against an order context: checks the validity window,
|
|
53
|
+
* minimum-subtotal threshold and usage limit. Does not compute any discount.
|
|
54
|
+
*/
|
|
55
|
+
declare function validateCoupon(coupon: Coupon, ctx: {
|
|
56
|
+
subtotal: number;
|
|
57
|
+
now?: Date;
|
|
58
|
+
}): CouponValidation;
|
|
59
|
+
/**
|
|
60
|
+
* Apply a coupon to an order. Validates first; when invalid, returns zero
|
|
61
|
+
* discounts and the untouched total. When valid, computes the discount:
|
|
62
|
+
*
|
|
63
|
+
* - `percent` → `round(subtotal * value / 100)`, capped by `maxDiscount` and
|
|
64
|
+
* never more than the subtotal.
|
|
65
|
+
* - `fixed` → `min(value, subtotal)`, capped by `maxDiscount`.
|
|
66
|
+
* - `free-shipping` → `shippingDiscount = shipping`.
|
|
67
|
+
*
|
|
68
|
+
* `total = max(0, subtotal - discount + shipping - shippingDiscount)`.
|
|
69
|
+
*/
|
|
70
|
+
declare function applyCoupon(coupon: Coupon, ctx: {
|
|
71
|
+
subtotal: number;
|
|
72
|
+
shipping?: number;
|
|
73
|
+
now?: Date;
|
|
74
|
+
}): CouponResult;
|
|
75
|
+
|
|
76
|
+
export { type Coupon, type CouponResult, type CouponValidation, applyCoupon, validateCoupon };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
function validateCoupon(coupon, ctx) {
|
|
3
|
+
const now = ctx.now ?? /* @__PURE__ */ new Date();
|
|
4
|
+
const t = now.getTime();
|
|
5
|
+
if (coupon.startsAt) {
|
|
6
|
+
const starts = new Date(coupon.startsAt).getTime();
|
|
7
|
+
if (!Number.isNaN(starts) && t < starts) {
|
|
8
|
+
return { valid: false, reason: "not-yet-started" };
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
if (coupon.endsAt) {
|
|
12
|
+
const ends = new Date(coupon.endsAt).getTime();
|
|
13
|
+
if (!Number.isNaN(ends) && t > ends) {
|
|
14
|
+
return { valid: false, reason: "expired" };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (typeof coupon.minSubtotal === "number" && ctx.subtotal < coupon.minSubtotal) {
|
|
18
|
+
return { valid: false, reason: "below-min-subtotal" };
|
|
19
|
+
}
|
|
20
|
+
if (typeof coupon.usageLimit === "number" && (coupon.used ?? 0) >= coupon.usageLimit) {
|
|
21
|
+
return { valid: false, reason: "usage-limit-reached" };
|
|
22
|
+
}
|
|
23
|
+
return { valid: true };
|
|
24
|
+
}
|
|
25
|
+
function applyCoupon(coupon, ctx) {
|
|
26
|
+
const subtotal = ctx.subtotal;
|
|
27
|
+
const shipping = ctx.shipping ?? 0;
|
|
28
|
+
const validation = validateCoupon(coupon, { subtotal, now: ctx.now });
|
|
29
|
+
if (!validation.valid) {
|
|
30
|
+
return {
|
|
31
|
+
...validation,
|
|
32
|
+
discount: 0,
|
|
33
|
+
shippingDiscount: 0,
|
|
34
|
+
total: Math.max(0, subtotal + shipping)
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
let discount = 0;
|
|
38
|
+
let shippingDiscount = 0;
|
|
39
|
+
switch (coupon.type) {
|
|
40
|
+
case "percent": {
|
|
41
|
+
const pct = coupon.value ?? 0;
|
|
42
|
+
discount = Math.round(subtotal * pct / 100);
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
case "fixed": {
|
|
46
|
+
const amount = coupon.value ?? 0;
|
|
47
|
+
discount = Math.min(amount, subtotal);
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
case "free-shipping": {
|
|
51
|
+
shippingDiscount = shipping;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (typeof coupon.maxDiscount === "number") {
|
|
56
|
+
discount = Math.min(discount, coupon.maxDiscount);
|
|
57
|
+
}
|
|
58
|
+
discount = Math.min(discount, subtotal);
|
|
59
|
+
discount = Math.max(0, discount);
|
|
60
|
+
const total = Math.max(0, subtotal - discount + shipping - shippingDiscount);
|
|
61
|
+
return { valid: true, discount, shippingDiscount, total };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { applyCoupon, validateCoupon };
|
|
65
|
+
//# sourceMappingURL=index.js.map
|
|
66
|
+
//# 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/coupon\n *\n * Discount / coupon engine — percent, fixed and free-shipping codes with\n * validity windows, minimum-subtotal thresholds, discount caps and usage\n * limits. All money is expressed in integer minor units (e.g. cents) so there\n * is no floating-point drift.\n *\n * Zero runtime dependencies. Isomorphic (Node, edge, browser).\n */\n\n/** A discount coupon definition. */\nexport interface Coupon {\n /** The code a customer enters. */\n code: string;\n /** Kind of discount. */\n type: \"percent\" | \"fixed\" | \"free-shipping\";\n /**\n * For `percent`, a whole percentage 0..100. For `fixed`, an amount in minor\n * units. Ignored for `free-shipping`.\n */\n value?: number;\n /** Minimum order subtotal (minor units) required for the coupon to apply. */\n minSubtotal?: number;\n /** Maximum discount (minor units) the coupon may grant. */\n maxDiscount?: number;\n /** ISO-8601 timestamp before which the coupon is not yet valid. */\n startsAt?: string;\n /** ISO-8601 timestamp after which the coupon has expired. */\n endsAt?: string;\n /** How many times the coupon may be redeemed in total. */\n usageLimit?: number;\n /** How many times it has already been redeemed. */\n used?: number;\n /** Optional currency tag for downstream formatting. */\n currency?: string;\n}\n\n/** Result of validating a coupon against a context. */\nexport interface CouponValidation {\n valid: boolean;\n reason?: string;\n}\n\n/** Result of applying a coupon to an order. */\nexport interface CouponResult extends CouponValidation {\n /** Discount applied to the subtotal (minor units). */\n discount: number;\n /** Discount applied to shipping (minor units). */\n shippingDiscount: number;\n /** Final payable total (minor units). */\n total: number;\n}\n\n/**\n * Validate a coupon against an order context: checks the validity window,\n * minimum-subtotal threshold and usage limit. Does not compute any discount.\n */\nexport function validateCoupon(\n coupon: Coupon,\n ctx: { subtotal: number; now?: Date },\n): CouponValidation {\n const now = ctx.now ?? new Date();\n const t = now.getTime();\n\n if (coupon.startsAt) {\n const starts = new Date(coupon.startsAt).getTime();\n if (!Number.isNaN(starts) && t < starts) {\n return { valid: false, reason: \"not-yet-started\" };\n }\n }\n\n if (coupon.endsAt) {\n const ends = new Date(coupon.endsAt).getTime();\n if (!Number.isNaN(ends) && t > ends) {\n return { valid: false, reason: \"expired\" };\n }\n }\n\n if (typeof coupon.minSubtotal === \"number\" && ctx.subtotal < coupon.minSubtotal) {\n return { valid: false, reason: \"below-min-subtotal\" };\n }\n\n if (\n typeof coupon.usageLimit === \"number\" &&\n (coupon.used ?? 0) >= coupon.usageLimit\n ) {\n return { valid: false, reason: \"usage-limit-reached\" };\n }\n\n return { valid: true };\n}\n\n/**\n * Apply a coupon to an order. Validates first; when invalid, returns zero\n * discounts and the untouched total. When valid, computes the discount:\n *\n * - `percent` → `round(subtotal * value / 100)`, capped by `maxDiscount` and\n * never more than the subtotal.\n * - `fixed` → `min(value, subtotal)`, capped by `maxDiscount`.\n * - `free-shipping` → `shippingDiscount = shipping`.\n *\n * `total = max(0, subtotal - discount + shipping - shippingDiscount)`.\n */\nexport function applyCoupon(\n coupon: Coupon,\n ctx: { subtotal: number; shipping?: number; now?: Date },\n): CouponResult {\n const subtotal = ctx.subtotal;\n const shipping = ctx.shipping ?? 0;\n\n const validation = validateCoupon(coupon, { subtotal, now: ctx.now });\n if (!validation.valid) {\n return {\n ...validation,\n discount: 0,\n shippingDiscount: 0,\n total: Math.max(0, subtotal + shipping),\n };\n }\n\n let discount = 0;\n let shippingDiscount = 0;\n\n switch (coupon.type) {\n case \"percent\": {\n const pct = coupon.value ?? 0;\n discount = Math.round((subtotal * pct) / 100);\n break;\n }\n case \"fixed\": {\n const amount = coupon.value ?? 0;\n discount = Math.min(amount, subtotal);\n break;\n }\n case \"free-shipping\": {\n shippingDiscount = shipping;\n break;\n }\n }\n\n if (typeof coupon.maxDiscount === \"number\") {\n discount = Math.min(discount, coupon.maxDiscount);\n }\n discount = Math.min(discount, subtotal);\n discount = Math.max(0, discount);\n\n const total = Math.max(0, subtotal - discount + shipping - shippingDiscount);\n\n return { valid: true, discount, shippingDiscount, total };\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lacspace/coupon",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Discount & coupon engine — percent, fixed and free-shipping codes with validity windows, minimum-subtotal, discount caps and usage limits. Integer minor units, no float drift. Zero-dependency, isomorphic.",
|
|
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
|
+
"coupon",
|
|
31
|
+
"discount",
|
|
32
|
+
"promo-code",
|
|
33
|
+
"voucher",
|
|
34
|
+
"percent-off",
|
|
35
|
+
"free-shipping",
|
|
36
|
+
"checkout",
|
|
37
|
+
"ecommerce",
|
|
38
|
+
"minor-units",
|
|
39
|
+
"isomorphic",
|
|
40
|
+
"zero-dependency",
|
|
41
|
+
"typescript"
|
|
42
|
+
],
|
|
43
|
+
"author": "Lacspace <contact@lacspace.com>",
|
|
44
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
45
|
+
"homepage": "https://developer.lacspace.com/packages/coupon",
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "git+https://github.com/lacspace/npm-packages.git",
|
|
49
|
+
"directory": "coupon"
|
|
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
|
+
}
|