@classytic/primitives 0.19.0 → 0.21.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/CHANGELOG.md +920 -751
- package/README.md +160 -159
- package/dist/approval.d.mts +50 -11
- package/dist/approval.mjs +44 -10
- package/dist/bank-transaction.d.mts +1 -2
- package/dist/{brand-LLXqoh5J.d.mts → brand-Lj_75Y_r.d.mts} +6 -6
- package/dist/brand.d.mts +1 -1
- package/dist/cadence-5X6TiD49.d.mts +107 -0
- package/dist/cadence.d.mts +1 -92
- package/dist/cadence.mjs +135 -6
- package/dist/calendar-DJWSbcgE.d.mts +46 -0
- package/dist/calendar.d.mts +1 -45
- package/dist/calendar.mjs +2 -2
- package/dist/canonical.mjs +17 -0
- package/dist/condition.mjs +2 -2
- package/dist/context.d.mts +28 -2
- package/dist/currency.d.mts +2 -2
- package/dist/currency.mjs +134 -19
- package/dist/environment.d.mts +85 -0
- package/dist/environment.mjs +48 -0
- package/dist/event-infra.d.mts +16 -32
- package/dist/event-infra.mjs +16 -46
- package/dist/idempotency.d.mts +263 -0
- package/dist/idempotency.mjs +226 -0
- package/dist/identity-verification.d.mts +0 -1
- package/dist/memory-outbox.d.mts +63 -0
- package/dist/memory-outbox.mjs +172 -0
- package/dist/mixin.d.mts +1 -1
- package/dist/monetization.d.mts +1 -2
- package/dist/money-CfAvfNiM.d.mts +346 -0
- package/dist/money.d.mts +1 -140
- package/dist/money.mjs +6 -15
- package/dist/otp.mjs +1 -1
- package/dist/outbox.d.mts +49 -2
- package/dist/outbox.mjs +64 -1
- package/dist/payment-allocation-status.d.mts +10 -4
- package/dist/payment-allocation-status.mjs +14 -0
- package/dist/payment-events.d.mts +132 -5
- package/dist/payment-events.mjs +34 -2
- package/dist/payment-gateway.d.mts +211 -8
- package/dist/payment-gateway.mjs +18 -1
- package/dist/payment-method-kind.d.mts +23 -11
- package/dist/payment-method-kind.mjs +17 -0
- package/dist/period.d.mts +149 -7
- package/dist/period.mjs +316 -5
- package/dist/person-parse.d.mts +81 -0
- package/dist/person-parse.mjs +109 -0
- package/dist/phone.d.mts +0 -1
- package/dist/phone.mjs +3 -3
- package/dist/proration.d.mts +2 -3
- package/dist/proration.mjs +18 -6
- package/dist/retention.mjs +3 -3
- package/dist/shelf-life.mjs +1 -1
- package/dist/sla-policy.d.mts +1 -2
- package/dist/state-diagram.d.mts +0 -1
- package/dist/state-diagram.mjs +1 -1
- package/dist/state-machine.mjs +13 -0
- package/dist/suspension.d.mts +6 -4
- package/dist/suspension.mjs +3 -2
- package/dist/timezone-CxUjxmRU.d.mts +113 -0
- package/dist/timezone.d.mts +2 -100
- package/dist/timezone.mjs +38 -3
- package/dist/unit-cost-rate.d.mts +1 -3
- package/package.json +249 -232
- package/dist/currency-BCJnJBlb.d.mts +0 -119
package/dist/cadence.d.mts
CHANGED
|
@@ -1,93 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
/**
|
|
3
|
-
* Recurrence spec + pure date arithmetic.
|
|
4
|
-
*
|
|
5
|
-
* Describes "every N days", "every weekday", "on the 15th of each month", etc.
|
|
6
|
-
* Consumed by `@classytic/order` (blanket / recurring orders, subscription
|
|
7
|
-
* renewal), `@classytic/flow` (replenishment schedules), and
|
|
8
|
-
* `@classytic/commission` (vesting periods, statement close).
|
|
9
|
-
*
|
|
10
|
-
* No cron parser dep — the `cron` kind stores an opaque string that the host
|
|
11
|
-
* parses with its own library (node-cron, croner, etc.). This keeps primitives
|
|
12
|
-
* zero-dep and lets hosts pick whatever cron dialect they want.
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* const monthly: Cadence = {
|
|
16
|
-
* kind: 'monthly',
|
|
17
|
-
* interval: 1,
|
|
18
|
-
* dayOfMonth: 15,
|
|
19
|
-
* startAt: new Date('2026-01-15T00:00:00Z'),
|
|
20
|
-
* };
|
|
21
|
-
* nextOccurrence(monthly, new Date('2026-02-20T00:00:00Z'));
|
|
22
|
-
* // → 2026-03-15T00:00:00Z
|
|
23
|
-
*/
|
|
24
|
-
type CadenceKind = 'daily' | 'weekly' | 'monthly' | 'yearly' | 'cron';
|
|
25
|
-
/** ISO weekday — 1=Mon … 7=Sun. Matches Temporal + ISO 8601. */
|
|
26
|
-
type IsoWeekday = 1 | 2 | 3 | 4 | 5 | 6 | 7;
|
|
27
|
-
interface CadenceBase {
|
|
28
|
-
readonly interval: number;
|
|
29
|
-
readonly startAt: Date;
|
|
30
|
-
readonly endAt?: Date;
|
|
31
|
-
/**
|
|
32
|
-
* Optional timezone IANA name (e.g. `Asia/Dhaka`). Pure functions in this
|
|
33
|
-
* module operate on UTC Date values; the field is carried for the host to
|
|
34
|
-
* localize occurrences when displaying. Occurrence math here is UTC-based.
|
|
35
|
-
*/
|
|
36
|
-
readonly timezone?: string;
|
|
37
|
-
}
|
|
38
|
-
interface DailyCadence extends CadenceBase {
|
|
39
|
-
readonly kind: 'daily';
|
|
40
|
-
}
|
|
41
|
-
interface WeeklyCadence extends CadenceBase {
|
|
42
|
-
readonly kind: 'weekly';
|
|
43
|
-
/**
|
|
44
|
-
* If provided, only these ISO weekdays count as occurrences; the earliest
|
|
45
|
-
* matching weekday on or after `startAt` is the first occurrence. If omitted,
|
|
46
|
-
* the weekday of `startAt` is used.
|
|
47
|
-
*/
|
|
48
|
-
readonly daysOfWeek?: readonly IsoWeekday[];
|
|
49
|
-
}
|
|
50
|
-
interface MonthlyCadence extends CadenceBase {
|
|
51
|
-
readonly kind: 'monthly';
|
|
52
|
-
/**
|
|
53
|
-
* Day of month, 1–31. If the target month has fewer days (e.g. 31st → Feb),
|
|
54
|
-
* occurrence snaps to the last day of the month.
|
|
55
|
-
*/
|
|
56
|
-
readonly dayOfMonth: number;
|
|
57
|
-
}
|
|
58
|
-
interface YearlyCadence extends CadenceBase {
|
|
59
|
-
readonly kind: 'yearly';
|
|
60
|
-
/** Calendar month 1–12. */
|
|
61
|
-
readonly month: number;
|
|
62
|
-
/** Day of month 1–31. Snaps to last day of month if needed (e.g. Feb 29 → Feb 28 in non-leap years). */
|
|
63
|
-
readonly dayOfMonth: number;
|
|
64
|
-
}
|
|
65
|
-
interface CronCadence extends CadenceBase {
|
|
66
|
-
readonly kind: 'cron';
|
|
67
|
-
/** Opaque cron expression — caller parses with its own cron library. */
|
|
68
|
-
readonly expression: string;
|
|
69
|
-
}
|
|
70
|
-
type Cadence = DailyCadence | WeeklyCadence | MonthlyCadence | YearlyCadence | CronCadence;
|
|
71
|
-
type CadenceErrorCode = 'INVALID_INTERVAL' | 'INVALID_DAY_OF_MONTH' | 'INVALID_MONTH' | 'INVALID_DAYS_OF_WEEK' | 'INVALID_START_AT' | 'INVALID_END_AT' | 'UNSUPPORTED_CRON';
|
|
72
|
-
declare class CadenceError extends Error {
|
|
73
|
-
readonly name = "CadenceError";
|
|
74
|
-
readonly code: CadenceErrorCode;
|
|
75
|
-
constructor(code: CadenceErrorCode, message: string);
|
|
76
|
-
}
|
|
77
|
-
/** Validate a Cadence spec. Call this before storing host-side. */
|
|
78
|
-
declare function validateCadence(c: Cadence): void;
|
|
79
|
-
/**
|
|
80
|
-
* Next occurrence strictly after `from`. Returns `null` if the cadence has
|
|
81
|
-
* ended (endAt passed) or the first occurrence is still in the future.
|
|
82
|
-
*
|
|
83
|
-
* Cron cadences return `null` — hosts must parse the expression themselves.
|
|
84
|
-
*/
|
|
85
|
-
declare function nextOccurrence(cadence: Cadence, from: Date): Date | null;
|
|
86
|
-
/**
|
|
87
|
-
* Enumerate occurrences in `[from, to)` inclusive at `from`, exclusive at `to`.
|
|
88
|
-
* Returns up to `limit` results (default 10_000) to guard against runaway
|
|
89
|
-
* misconfigured cadences. Cron returns an empty array.
|
|
90
|
-
*/
|
|
91
|
-
declare function occurrencesBetween(cadence: Cadence, from: Date, to: Date, limit?: number): Date[];
|
|
92
|
-
//#endregion
|
|
1
|
+
import { a as CronCadence, c as MonthlyCadence, d as nextOccurrence, f as occurrencesBetween, i as CadenceKind, l as WeeklyCadence, n as CadenceError, o as DailyCadence, p as validateCadence, r as CadenceErrorCode, s as IsoWeekday, t as Cadence, u as YearlyCadence } from "./cadence-5X6TiD49.mjs";
|
|
93
2
|
export { Cadence, CadenceError, CadenceErrorCode, CadenceKind, CronCadence, DailyCadence, IsoWeekday, MonthlyCadence, WeeklyCadence, YearlyCadence, nextOccurrence, occurrencesBetween, validateCadence };
|
package/dist/cadence.mjs
CHANGED
|
@@ -1,4 +1,27 @@
|
|
|
1
|
+
import { addCivilDays, civilDate, civilDateOf, civilDateToInstant, civilDaysBetween, isValidTimeZone, localTimeParts } from "./timezone.mjs";
|
|
1
2
|
//#region src/scheduling/cadence.ts
|
|
3
|
+
/**
|
|
4
|
+
* Recurrence spec + pure date arithmetic.
|
|
5
|
+
*
|
|
6
|
+
* Describes "every N days", "every weekday", "on the 15th of each month", etc.
|
|
7
|
+
* Consumed by `@classytic/order` (blanket / recurring orders, subscription
|
|
8
|
+
* renewal), `@classytic/flow` (replenishment schedules), and
|
|
9
|
+
* `@classytic/commission` (vesting periods, statement close).
|
|
10
|
+
*
|
|
11
|
+
* No cron parser dep — the `cron` kind stores an opaque string that the host
|
|
12
|
+
* parses with its own library (node-cron, croner, etc.). This keeps primitives
|
|
13
|
+
* zero-dep and lets hosts pick whatever cron dialect they want.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* const monthly: Cadence = {
|
|
17
|
+
* kind: 'monthly',
|
|
18
|
+
* interval: 1,
|
|
19
|
+
* dayOfMonth: 15,
|
|
20
|
+
* startAt: new Date('2026-01-15T00:00:00Z'),
|
|
21
|
+
* };
|
|
22
|
+
* nextOccurrence(monthly, new Date('2026-02-20T00:00:00Z'));
|
|
23
|
+
* // → 2026-03-15T00:00:00Z
|
|
24
|
+
*/
|
|
2
25
|
var CadenceError = class extends Error {
|
|
3
26
|
name = "CadenceError";
|
|
4
27
|
code;
|
|
@@ -13,6 +36,7 @@ function validateCadence(c) {
|
|
|
13
36
|
if (Number.isNaN(c.startAt.getTime())) throw new CadenceError("INVALID_START_AT", "startAt is not a valid Date");
|
|
14
37
|
if (c.endAt && Number.isNaN(c.endAt.getTime())) throw new CadenceError("INVALID_END_AT", "endAt is not a valid Date");
|
|
15
38
|
if (c.endAt && c.endAt.getTime() < c.startAt.getTime()) throw new CadenceError("INVALID_END_AT", "endAt must be >= startAt");
|
|
39
|
+
if (c.timezone !== void 0 && !isValidTimeZone(c.timezone)) throw new CadenceError("INVALID_TIMEZONE", `timezone must be a valid IANA zone name (e.g. 'Asia/Dhaka'), got '${c.timezone}'`);
|
|
16
40
|
if (c.kind === "weekly" && c.daysOfWeek) {
|
|
17
41
|
if (c.daysOfWeek.length === 0) throw new CadenceError("INVALID_DAYS_OF_WEEK", "daysOfWeek cannot be empty");
|
|
18
42
|
for (const d of c.daysOfWeek) if (!Number.isInteger(d) || d < 1 || d > 7) throw new CadenceError("INVALID_DAYS_OF_WEEK", `daysOfWeek entry must be 1–7, got ${d}`);
|
|
@@ -61,17 +85,122 @@ function occurrencesBetween(cadence, from, to, limit = 1e4) {
|
|
|
61
85
|
return result;
|
|
62
86
|
}
|
|
63
87
|
function stepForward(cadence, from) {
|
|
88
|
+
const zone = cadence.timezone;
|
|
64
89
|
switch (cadence.kind) {
|
|
65
|
-
case "daily": return stepDaily(cadence, from);
|
|
66
|
-
case "weekly": return stepWeekly(cadence, from);
|
|
67
|
-
case "monthly": return stepMonthly(cadence, from);
|
|
68
|
-
case "yearly": return stepYearly(cadence, from);
|
|
90
|
+
case "daily": return zone === void 0 ? stepDaily(cadence, from) : stepDailyZoned(cadence, from, zone);
|
|
91
|
+
case "weekly": return zone === void 0 ? stepWeekly(cadence, from) : stepWeeklyZoned(cadence, from, zone);
|
|
92
|
+
case "monthly": return zone === void 0 ? stepMonthly(cadence, from) : stepMonthlyZoned(cadence, from, zone);
|
|
93
|
+
case "yearly": return zone === void 0 ? stepYearly(cadence, from) : stepYearlyZoned(cadence, from, zone);
|
|
69
94
|
default: return null;
|
|
70
95
|
}
|
|
71
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* The anchor's LOCAL wall clock. Seconds and milliseconds come straight from
|
|
99
|
+
* the instant because every modern IANA offset is a whole number of minutes —
|
|
100
|
+
* they are the same in every zone.
|
|
101
|
+
*/
|
|
102
|
+
function wallClockOf(instant, zone) {
|
|
103
|
+
const cd = civilDateOf(instant, zone);
|
|
104
|
+
const { hour, minute } = localTimeParts(instant, zone);
|
|
105
|
+
return {
|
|
106
|
+
year: Number(cd.slice(0, 4)),
|
|
107
|
+
month: Number(cd.slice(5, 7)),
|
|
108
|
+
day: Number(cd.slice(8, 10)),
|
|
109
|
+
hour,
|
|
110
|
+
minute,
|
|
111
|
+
second: instant.getUTCSeconds(),
|
|
112
|
+
millisecond: instant.getUTCMilliseconds()
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
const pad2 = (n) => String(n).padStart(2, "0");
|
|
116
|
+
function civilOf(year, month, day) {
|
|
117
|
+
return civilDate(`${String(year).padStart(4, "0")}-${pad2(month)}-${pad2(day)}`);
|
|
118
|
+
}
|
|
119
|
+
/** Bind a local calendar day + the anchor's local time-of-day to an instant. */
|
|
120
|
+
function bindWall(cd, wall, zone) {
|
|
121
|
+
return civilDateToInstant(cd, zone, {
|
|
122
|
+
hour: wall.hour,
|
|
123
|
+
minute: wall.minute,
|
|
124
|
+
second: wall.second,
|
|
125
|
+
millisecond: wall.millisecond
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/** Occurrences are anchor-local-date + n×interval LOCAL days, at the anchor's local time. */
|
|
129
|
+
function stepDailyZoned(c, from, zone) {
|
|
130
|
+
const wall = wallClockOf(c.startAt, zone);
|
|
131
|
+
const anchorCd = civilOf(wall.year, wall.month, wall.day);
|
|
132
|
+
const elapsed = civilDaysBetween(anchorCd, civilDateOf(from, zone));
|
|
133
|
+
let n = Math.max(0, Math.floor(elapsed / c.interval) * c.interval);
|
|
134
|
+
for (let i = 0; i < 1e3; i++) {
|
|
135
|
+
const candidate = bindWall(addCivilDays(anchorCd, n), wall, zone);
|
|
136
|
+
if (candidate.getTime() > from.getTime()) return candidate;
|
|
137
|
+
n += c.interval;
|
|
138
|
+
}
|
|
139
|
+
return bindWall(addCivilDays(anchorCd, n), wall, zone);
|
|
140
|
+
}
|
|
141
|
+
/** ISO weekday of a civil date — zone-free, a calendar day has a determinate weekday. */
|
|
142
|
+
function isoWeekdayOfCivil(cd) {
|
|
143
|
+
const utc = Date.UTC(Number(cd.slice(0, 4)), Number(cd.slice(5, 7)) - 1, Number(cd.slice(8, 10)));
|
|
144
|
+
return (new Date(utc).getUTCDay() + 6) % 7 + 1;
|
|
145
|
+
}
|
|
146
|
+
function stepWeeklyZoned(c, from, zone) {
|
|
147
|
+
const wall = wallClockOf(c.startAt, zone);
|
|
148
|
+
const anchorCd = civilOf(wall.year, wall.month, wall.day);
|
|
149
|
+
const targetDays = c.daysOfWeek ?? [isoWeekdayOfCivil(anchorCd)];
|
|
150
|
+
let cursor = civilDateOf(from, zone);
|
|
151
|
+
const maxSteps = Math.max(60, c.interval * 14) + 2;
|
|
152
|
+
for (let i = 0; i < maxSteps; i++) {
|
|
153
|
+
if (targetDays.includes(isoWeekdayOfCivil(cursor))) {
|
|
154
|
+
const weeksSinceAnchor = Math.floor(civilDaysBetween(anchorCd, cursor) / 7);
|
|
155
|
+
if (weeksSinceAnchor >= 0 && weeksSinceAnchor % c.interval === 0) {
|
|
156
|
+
const candidate = bindWall(cursor, wall, zone);
|
|
157
|
+
if (candidate.getTime() > from.getTime()) return candidate;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
cursor = addCivilDays(cursor, 1);
|
|
161
|
+
}
|
|
162
|
+
return bindWall(cursor, wall, zone);
|
|
163
|
+
}
|
|
164
|
+
function stepMonthlyZoned(c, from, zone) {
|
|
165
|
+
const wall = wallClockOf(c.startAt, zone);
|
|
166
|
+
let candYear = wall.year;
|
|
167
|
+
let candMonth = wall.month;
|
|
168
|
+
const fromCd = civilDateOf(from, zone);
|
|
169
|
+
const monthsDiff = (Number(fromCd.slice(0, 4)) - wall.year) * 12 + (Number(fromCd.slice(5, 7)) - wall.month);
|
|
170
|
+
if (monthsDiff > 0) {
|
|
171
|
+
const aligned = Math.floor(monthsDiff / c.interval) * c.interval;
|
|
172
|
+
const abs = candYear * 12 + (candMonth - 1) + aligned;
|
|
173
|
+
candYear = Math.floor(abs / 12);
|
|
174
|
+
candMonth = abs % 12 + 1;
|
|
175
|
+
}
|
|
176
|
+
for (let i = 0; i < 1e3; i++) {
|
|
177
|
+
const day = clampDayOfMonth(c.dayOfMonth, candYear, candMonth - 1);
|
|
178
|
+
const candidate = bindWall(civilOf(candYear, candMonth, day), wall, zone);
|
|
179
|
+
if (candidate.getTime() > from.getTime()) return candidate;
|
|
180
|
+
candMonth += c.interval;
|
|
181
|
+
while (candMonth > 12) {
|
|
182
|
+
candMonth -= 12;
|
|
183
|
+
candYear += 1;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return bindWall(civilOf(candYear, candMonth, clampDayOfMonth(c.dayOfMonth, candYear, candMonth - 1)), wall, zone);
|
|
187
|
+
}
|
|
188
|
+
function stepYearlyZoned(c, from, zone) {
|
|
189
|
+
const wall = wallClockOf(c.startAt, zone);
|
|
190
|
+
let candYear = wall.year;
|
|
191
|
+
const yearsDiff = Number(civilDateOf(from, zone).slice(0, 4)) - wall.year;
|
|
192
|
+
if (yearsDiff > 0) candYear += Math.floor(yearsDiff / c.interval) * c.interval;
|
|
193
|
+
for (let i = 0; i < 1e3; i++) {
|
|
194
|
+
const day = clampDayOfMonth(c.dayOfMonth, candYear, c.month - 1);
|
|
195
|
+
const candidate = bindWall(civilOf(candYear, c.month, day), wall, zone);
|
|
196
|
+
if (candidate.getTime() > from.getTime()) return candidate;
|
|
197
|
+
candYear += c.interval;
|
|
198
|
+
}
|
|
199
|
+
return bindWall(civilOf(candYear, c.month, clampDayOfMonth(c.dayOfMonth, candYear, c.month - 1)), wall, zone);
|
|
200
|
+
}
|
|
72
201
|
function stepDaily(c, from) {
|
|
73
202
|
const start = c.startAt;
|
|
74
|
-
const diffDays = Math.floor((from.getTime() - start.getTime()) /
|
|
203
|
+
const diffDays = Math.floor((from.getTime() - start.getTime()) / 864e5);
|
|
75
204
|
const nextNthDay = Math.floor(diffDays / c.interval) * c.interval + c.interval;
|
|
76
205
|
const nextMs = start.getTime() + nextNthDay * 24 * 60 * 60 * 1e3;
|
|
77
206
|
return new Date(nextMs);
|
|
@@ -79,7 +208,7 @@ function stepDaily(c, from) {
|
|
|
79
208
|
function stepWeekly(c, from) {
|
|
80
209
|
const anchor = c.startAt;
|
|
81
210
|
const sortedDays = [...c.daysOfWeek ?? [isoWeekday(anchor)]].sort((a, b) => a - b);
|
|
82
|
-
const oneDay =
|
|
211
|
+
const oneDay = 864e5;
|
|
83
212
|
const msPerWeek = 7 * oneDay;
|
|
84
213
|
let cursor = from.getTime() + oneDay;
|
|
85
214
|
const maxSteps = Math.max(60, c.interval * 14);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
//#region src/scheduling/calendar.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Calendar boundaries + arithmetic — the ONE place the stack computes
|
|
4
|
+
* "start of day / month", weekday, and month/day/year addition, so no package
|
|
5
|
+
* hand-rolls it with server-LOCAL `Date` methods (`getFullYear`/`getMonth`/
|
|
6
|
+
* `getDate`/`getDay`), which silently shift with the deploy machine's `TZ` env.
|
|
7
|
+
*
|
|
8
|
+
* Every function is PURE and takes a fixed **UTC offset in minutes** (east of
|
|
9
|
+
* UTC — `360` = UTC+6 / Asia-Dhaka). Same convention as `cadence` / `sla-policy`:
|
|
10
|
+
* no IANA tz database, no DST. A fixed offset is exact for zones without DST
|
|
11
|
+
* (e.g. Bangladesh, India, most of Asia) and is what a "daily limit resets at
|
|
12
|
+
* local midnight" boundary needs. For DST-correct wall-clock math the host
|
|
13
|
+
* resolves the offset for the instant and passes it in.
|
|
14
|
+
*
|
|
15
|
+
* `Date` instants are UTC epoch and timezone-independent — these helpers only
|
|
16
|
+
* decide WHERE the day/month boundary falls, never how "now" is recorded.
|
|
17
|
+
*
|
|
18
|
+
* Resolving that offset from an IANA zone name is `/timezone`'s job — the
|
|
19
|
+
* two compose: `startOfMonth(instant, zoneOffsetMinutes(instant, zone))`.
|
|
20
|
+
* For a DST-exact DAY boundary prefer the civil-date round-trip from
|
|
21
|
+
* `/timezone`: `civilDateToInstant(civilDateOf(instant, zone), zone)`.
|
|
22
|
+
*/
|
|
23
|
+
/** Minutes east of UTC. `0` = UTC, `360` = UTC+6 (Asia/Dhaka), `-300` = UTC-5. */
|
|
24
|
+
type UtcOffsetMinutes = number;
|
|
25
|
+
/**
|
|
26
|
+
* The absolute instant of local midnight for the day CONTAINING `instant`, in
|
|
27
|
+
* the given offset. `startOfDay(2026-07-02T03:00Z, 360)` → `2026-07-01T18:00Z`
|
|
28
|
+
* (Dhaka midnight). With `offsetMinutes = 0` it's plain UTC midnight.
|
|
29
|
+
*/
|
|
30
|
+
declare function startOfDay(instant: Date, offsetMinutes?: UtcOffsetMinutes): Date;
|
|
31
|
+
/** The absolute instant of the first-of-month midnight, in the given offset. */
|
|
32
|
+
declare function startOfMonth(instant: Date, offsetMinutes?: UtcOffsetMinutes): Date;
|
|
33
|
+
/** ISO weekday (1 = Monday … 7 = Sunday) of `instant` in the given offset. */
|
|
34
|
+
declare function isoWeekday(instant: Date, offsetMinutes?: UtcOffsetMinutes): number;
|
|
35
|
+
/** Add whole days as an exact 24h × n duration (offset-agnostic, no DST skew). */
|
|
36
|
+
declare function addDays(instant: Date, days: number): Date;
|
|
37
|
+
/**
|
|
38
|
+
* Add calendar months in UTC, clamping the day so month-ends never overflow
|
|
39
|
+
* (`addMonths(Jan 31, 1)` → Feb 28/29). Use for subscription/access periods
|
|
40
|
+
* instead of local `setMonth(getMonth() + n)`.
|
|
41
|
+
*/
|
|
42
|
+
declare function addMonths(instant: Date, months: number): Date;
|
|
43
|
+
/** Add calendar years in UTC (clamps Feb 29 → Feb 28 on non-leap years). */
|
|
44
|
+
declare function addYears(instant: Date, years: number): Date;
|
|
45
|
+
//#endregion
|
|
46
|
+
export { isoWeekday as a, addYears as i, addDays as n, startOfDay as o, addMonths as r, startOfMonth as s, UtcOffsetMinutes as t };
|
package/dist/calendar.d.mts
CHANGED
|
@@ -1,46 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
/**
|
|
3
|
-
* Calendar boundaries + arithmetic — the ONE place the stack computes
|
|
4
|
-
* "start of day / month", weekday, and month/day/year addition, so no package
|
|
5
|
-
* hand-rolls it with server-LOCAL `Date` methods (`getFullYear`/`getMonth`/
|
|
6
|
-
* `getDate`/`getDay`), which silently shift with the deploy machine's `TZ` env.
|
|
7
|
-
*
|
|
8
|
-
* Every function is PURE and takes a fixed **UTC offset in minutes** (east of
|
|
9
|
-
* UTC — `360` = UTC+6 / Asia-Dhaka). Same convention as `cadence` / `sla-policy`:
|
|
10
|
-
* no IANA tz database, no DST. A fixed offset is exact for zones without DST
|
|
11
|
-
* (e.g. Bangladesh, India, most of Asia) and is what a "daily limit resets at
|
|
12
|
-
* local midnight" boundary needs. For DST-correct wall-clock math the host
|
|
13
|
-
* resolves the offset for the instant and passes it in.
|
|
14
|
-
*
|
|
15
|
-
* `Date` instants are UTC epoch and timezone-independent — these helpers only
|
|
16
|
-
* decide WHERE the day/month boundary falls, never how "now" is recorded.
|
|
17
|
-
*
|
|
18
|
-
* Resolving that offset from an IANA zone name is `/timezone`'s job — the
|
|
19
|
-
* two compose: `startOfMonth(instant, zoneOffsetMinutes(instant, zone))`.
|
|
20
|
-
* For a DST-exact DAY boundary prefer the civil-date round-trip from
|
|
21
|
-
* `/timezone`: `civilDateToInstant(civilDateOf(instant, zone), zone)`.
|
|
22
|
-
*/
|
|
23
|
-
/** Minutes east of UTC. `0` = UTC, `360` = UTC+6 (Asia/Dhaka), `-300` = UTC-5. */
|
|
24
|
-
type UtcOffsetMinutes = number;
|
|
25
|
-
/**
|
|
26
|
-
* The absolute instant of local midnight for the day CONTAINING `instant`, in
|
|
27
|
-
* the given offset. `startOfDay(2026-07-02T03:00Z, 360)` → `2026-07-01T18:00Z`
|
|
28
|
-
* (Dhaka midnight). With `offsetMinutes = 0` it's plain UTC midnight.
|
|
29
|
-
*/
|
|
30
|
-
declare function startOfDay(instant: Date, offsetMinutes?: UtcOffsetMinutes): Date;
|
|
31
|
-
/** The absolute instant of the first-of-month midnight, in the given offset. */
|
|
32
|
-
declare function startOfMonth(instant: Date, offsetMinutes?: UtcOffsetMinutes): Date;
|
|
33
|
-
/** ISO weekday (1 = Monday … 7 = Sunday) of `instant` in the given offset. */
|
|
34
|
-
declare function isoWeekday(instant: Date, offsetMinutes?: UtcOffsetMinutes): number;
|
|
35
|
-
/** Add whole days as an exact 24h × n duration (offset-agnostic, no DST skew). */
|
|
36
|
-
declare function addDays(instant: Date, days: number): Date;
|
|
37
|
-
/**
|
|
38
|
-
* Add calendar months in UTC, clamping the day so month-ends never overflow
|
|
39
|
-
* (`addMonths(Jan 31, 1)` → Feb 28/29). Use for subscription/access periods
|
|
40
|
-
* instead of local `setMonth(getMonth() + n)`.
|
|
41
|
-
*/
|
|
42
|
-
declare function addMonths(instant: Date, months: number): Date;
|
|
43
|
-
/** Add calendar years in UTC (clamps Feb 29 → Feb 28 on non-leap years). */
|
|
44
|
-
declare function addYears(instant: Date, years: number): Date;
|
|
45
|
-
//#endregion
|
|
1
|
+
import { a as isoWeekday, i as addYears, n as addDays, o as startOfDay, r as addMonths, s as startOfMonth, t as UtcOffsetMinutes } from "./calendar-DJWSbcgE.mjs";
|
|
46
2
|
export { UtcOffsetMinutes, addDays, addMonths, addYears, isoWeekday, startOfDay, startOfMonth };
|
package/dist/calendar.mjs
CHANGED
|
@@ -9,13 +9,13 @@ const DAY_MS = 864e5;
|
|
|
9
9
|
function startOfDay(instant, offsetMinutes = 0) {
|
|
10
10
|
const shifted = new Date(instant.getTime() + offsetMinutes * MINUTE_MS);
|
|
11
11
|
const localMidnightAsUtc = Date.UTC(shifted.getUTCFullYear(), shifted.getUTCMonth(), shifted.getUTCDate());
|
|
12
|
-
return new Date(localMidnightAsUtc - offsetMinutes * MINUTE_MS);
|
|
12
|
+
return /* @__PURE__ */ new Date(localMidnightAsUtc - offsetMinutes * MINUTE_MS);
|
|
13
13
|
}
|
|
14
14
|
/** The absolute instant of the first-of-month midnight, in the given offset. */
|
|
15
15
|
function startOfMonth(instant, offsetMinutes = 0) {
|
|
16
16
|
const shifted = new Date(instant.getTime() + offsetMinutes * MINUTE_MS);
|
|
17
17
|
const localFirstAsUtc = Date.UTC(shifted.getUTCFullYear(), shifted.getUTCMonth(), 1);
|
|
18
|
-
return new Date(localFirstAsUtc - offsetMinutes * MINUTE_MS);
|
|
18
|
+
return /* @__PURE__ */ new Date(localFirstAsUtc - offsetMinutes * MINUTE_MS);
|
|
19
19
|
}
|
|
20
20
|
/** ISO weekday (1 = Monday … 7 = Sunday) of `instant` in the given offset. */
|
|
21
21
|
function isoWeekday(instant, offsetMinutes = 0) {
|
package/dist/canonical.mjs
CHANGED
|
@@ -54,6 +54,23 @@ function canonicalize(value, seen) {
|
|
|
54
54
|
seen.add(obj);
|
|
55
55
|
try {
|
|
56
56
|
if (Array.isArray(value)) return `[${value.map((v) => canonicalize(v, seen)).join(",")}]`;
|
|
57
|
+
/**
|
|
58
|
+
* Reject CLASS INSTANCES — the silent-collapse hole.
|
|
59
|
+
*
|
|
60
|
+
* `Object.keys()` sees only own enumerable properties, so a `Types.ObjectId`,
|
|
61
|
+
* a `Buffer`, a `URL` or a hydrated Mongoose subdocument serializes as `{}`
|
|
62
|
+
* (or as a digit-keyed byte map). The digest then ignores the value entirely:
|
|
63
|
+
* two manifests referencing DIFFERENT documents hash identically, and the
|
|
64
|
+
* integrity check this module exists to provide reports "unchanged". That is
|
|
65
|
+
* the same failure mode the explicit `Date` branch above was written to
|
|
66
|
+
* prevent, so it gets the same treatment — an unsupported type FAILS.
|
|
67
|
+
*
|
|
68
|
+
* Callers holding ORM values project them to plain data first (see
|
|
69
|
+
* spine-accounting's `plainRef` / `plainSummary`); this makes that discipline
|
|
70
|
+
* enforced rather than remembered.
|
|
71
|
+
*/
|
|
72
|
+
const proto = Object.getPrototypeOf(value);
|
|
73
|
+
if (proto !== null && proto !== Object.prototype) throw new CanonicalizeError(`class instance '${value.constructor?.name ?? "unknown"}' is not serializable — its own enumerable keys are not its value, so it would digest as {} and silently defeat the checksum. Project it to a plain object first.`);
|
|
57
74
|
const rec = value;
|
|
58
75
|
return `{${Object.keys(rec).sort().map((k) => `${JSON.stringify(k)}:${canonicalize(rec[k], seen)}`).join(",")}}`;
|
|
59
76
|
} finally {
|
package/dist/condition.mjs
CHANGED
|
@@ -83,7 +83,7 @@ var ConditionError = class extends Error {
|
|
|
83
83
|
this.code = code;
|
|
84
84
|
}
|
|
85
85
|
};
|
|
86
|
-
const COMPARATORS = new Set([
|
|
86
|
+
const COMPARATORS = /* @__PURE__ */ new Set([
|
|
87
87
|
"eq",
|
|
88
88
|
"neq",
|
|
89
89
|
"gt",
|
|
@@ -98,7 +98,7 @@ const COMPARATORS = new Set([
|
|
|
98
98
|
"isNull",
|
|
99
99
|
"isNotNull"
|
|
100
100
|
]);
|
|
101
|
-
const UNARY_OPS = new Set(["isNull", "isNotNull"]);
|
|
101
|
+
const UNARY_OPS = /* @__PURE__ */ new Set(["isNull", "isNotNull"]);
|
|
102
102
|
/**
|
|
103
103
|
* Validate a condition shape before persisting. Recurses through composites.
|
|
104
104
|
* Throws `ConditionError` so hosts get a stable code + message in API
|
package/dist/context.d.mts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { IdLike } from "./reference.mjs";
|
|
2
|
-
|
|
3
2
|
//#region src/composition/context.d.ts
|
|
4
3
|
/**
|
|
5
4
|
* Operation context threaded through package APIs.
|
|
@@ -38,6 +37,33 @@ interface OperationContext {
|
|
|
38
37
|
/** Free-form metadata the host may attach (locale, timezone, user-agent…). */
|
|
39
38
|
metadata?: Readonly<Record<string, unknown>>;
|
|
40
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Structured logger — OBJECT-FIRST (`warn(fields, message)`), the pino / `fastify.log`
|
|
42
|
+
* convention.
|
|
43
|
+
*
|
|
44
|
+
* ## Why this is separate from `ErrorLogger` / `EventLogger`
|
|
45
|
+
*
|
|
46
|
+
* Those two (in `primitives/events`) are MESSAGE-FIRST — `error(message, ...args)`, the console
|
|
47
|
+
* convention — and `EventLogger`'s own docblock notes it mirrors arc's. The two conventions are
|
|
48
|
+
* not interchangeable: swapping them silently logs an object where a format string was expected,
|
|
49
|
+
* so a port must say which one it wants.
|
|
50
|
+
*
|
|
51
|
+
* This lives here rather than being re-declared per package because it was already declared per
|
|
52
|
+
* package: an identical object-first shape existed in `spine-kit` as `PlacementLogger`, and the
|
|
53
|
+
* next package needing one would have made a fourth. A logger is shared vocabulary, not domain.
|
|
54
|
+
*
|
|
55
|
+
* `warn`/`error` are required; `info` is OPTIONAL on purpose — a hand-written test logger or a
|
|
56
|
+
* script's two-method stub is a legitimate caller, and requiring `info` would break every one of
|
|
57
|
+
* them the day something wanted to log a healthy tick. Reach it as `logger?.info?.(…)`.
|
|
58
|
+
*
|
|
59
|
+
* `fastify.log` and a pino instance both satisfy this structurally, so hosts pass `req.log`
|
|
60
|
+
* with no cast.
|
|
61
|
+
*/
|
|
62
|
+
interface StructuredLogger {
|
|
63
|
+
warn(fields: Record<string, unknown>, message: string): void;
|
|
64
|
+
error(fields: Record<string, unknown>, message: string): void;
|
|
65
|
+
info?(fields: Record<string, unknown>, message: string): void;
|
|
66
|
+
}
|
|
41
67
|
/** An identity shape — useful on audit fields (`createdBy`, `updatedBy`). */
|
|
42
68
|
interface ActorRef {
|
|
43
69
|
id: string;
|
|
@@ -45,4 +71,4 @@ interface ActorRef {
|
|
|
45
71
|
name?: string;
|
|
46
72
|
}
|
|
47
73
|
//#endregion
|
|
48
|
-
export { ActorRef, OperationContext };
|
|
74
|
+
export { ActorRef, OperationContext, StructuredLogger };
|
package/dist/currency.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { CURRENCIES, CURRENCY_PATTERN, CurrencyCode, FxSnapshot, MINOR_UNIT_FACTOR, convertWithSnapshot, isCurrencyCode, isFxSnapshot, minorUnitFactor, reverseWithSnapshot, toCurrencyCode };
|
|
1
|
+
import { A as convertWithSnapshot, C as CURRENCY_PATTERN, D as FxSnapshot, E as FxRounding, F as minorUnitFactor, I as reverseWithSnapshot, L as toCurrencyCode, M as isCurrencyCode, N as isFxSnapshot, O as InvalidCurrencyCodeError, P as isKnownCurrency, S as CURRENCIES, T as CurrencyMismatchError, j as currencyCode, k as MINOR_UNIT_FACTOR, w as CurrencyCode } from "./money-CfAvfNiM.mjs";
|
|
2
|
+
export { CURRENCIES, CURRENCY_PATTERN, CurrencyCode, CurrencyMismatchError, FxRounding, FxSnapshot, InvalidCurrencyCodeError, MINOR_UNIT_FACTOR, convertWithSnapshot, currencyCode, isCurrencyCode, isFxSnapshot, isKnownCurrency, minorUnitFactor, reverseWithSnapshot, toCurrencyCode };
|