@classytic/primitives 0.4.0 → 0.6.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +162 -0
  2. package/dist/address.d.mts +1 -1
  3. package/dist/address.mjs +1 -1
  4. package/dist/approval.d.mts +1 -1
  5. package/dist/approval.mjs +1 -1
  6. package/dist/bank-transaction.d.mts +167 -0
  7. package/dist/bank-transaction.mjs +1 -0
  8. package/dist/{brand-DApaegoR.d.mts → brand-LLXqoh5J.d.mts} +1 -1
  9. package/dist/brand.d.mts +1 -1
  10. package/dist/cadence.d.mts +1 -1
  11. package/dist/cadence.mjs +1 -1
  12. package/dist/condition.d.mts +76 -0
  13. package/dist/condition.mjs +131 -0
  14. package/dist/context.d.mts +1 -1
  15. package/dist/currency-B4rSdN7D.d.mts +110 -0
  16. package/dist/currency.d.mts +1 -109
  17. package/dist/currency.mjs +1 -1
  18. package/dist/events.d.mts +1 -1
  19. package/dist/events.mjs +1 -1
  20. package/dist/hold.d.mts +1 -1
  21. package/dist/hold.mjs +1 -1
  22. package/dist/mixin.d.mts +73 -0
  23. package/dist/mixin.mjs +47 -0
  24. package/dist/money.d.mts +2 -2
  25. package/dist/money.mjs +1 -1
  26. package/dist/outbox.d.mts +1 -1
  27. package/dist/outbox.mjs +1 -1
  28. package/dist/payment-gateway.d.mts +168 -0
  29. package/dist/payment-gateway.mjs +1 -0
  30. package/dist/period.d.mts +1 -1
  31. package/dist/period.mjs +1 -1
  32. package/dist/person.d.mts +1 -1
  33. package/dist/person.mjs +1 -1
  34. package/dist/phone.d.mts +47 -0
  35. package/dist/phone.mjs +310 -0
  36. package/dist/reference.d.mts +1 -1
  37. package/dist/reference.mjs +1 -1
  38. package/dist/result.d.mts +1 -1
  39. package/dist/result.mjs +1 -1
  40. package/dist/sla-policy.d.mts +116 -0
  41. package/dist/sla-policy.mjs +120 -0
  42. package/dist/sla.d.mts +1 -1
  43. package/dist/sla.mjs +1 -1
  44. package/dist/split-allocation.d.mts +1 -1
  45. package/dist/split-allocation.mjs +1 -1
  46. package/dist/state-machine.d.mts +1 -1
  47. package/dist/state-machine.mjs +1 -1
  48. package/dist/status-history.d.mts +86 -0
  49. package/dist/status-history.mjs +48 -0
  50. package/package.json +29 -1
@@ -0,0 +1,86 @@
1
+ //#region src/workflow/status-history.d.ts
2
+ /**
3
+ * Generic, immutable status-change log.
4
+ *
5
+ * Every domain aggregate that has a state machine (Order, Lead, Opportunity,
6
+ * Campaign, Subscription, Approval, Shipment, …) ends up reinventing the same
7
+ * triple: `{ status, occurredAt, by? }` + an append-only array on the entity.
8
+ * This module gives that pattern one canonical type and one zero-allocation
9
+ * helper, so domains stay focused on their *transitions* not the bookkeeping.
10
+ *
11
+ * Pairs naturally with `state-machine`: the machine decides if a transition
12
+ * is legal; this helper records the move that happened.
13
+ *
14
+ * Frappe's "status change log" with `duration_in_prior_status` was the
15
+ * inspiration — funnel velocity reports (avg time spent in "Qualification")
16
+ * are free when you store the duration at write time instead of recomputing
17
+ * from raw timestamps later.
18
+ *
19
+ * @example
20
+ * import { appendStatus, type StatusHistory } from '@classytic/primitives/status-history';
21
+ * import { defineStateMachine } from '@classytic/primitives/state-machine';
22
+ *
23
+ * type LeadStatus = 'new' | 'qualified' | 'converted' | 'disqualified';
24
+ * const MACHINE = defineStateMachine<LeadStatus>({ ... });
25
+ *
26
+ * function qualify(lead: Lead, by: string): Lead {
27
+ * MACHINE.assertTransition(lead.id, lead.status, 'qualified');
28
+ * return {
29
+ * ...lead,
30
+ * status: 'qualified',
31
+ * statusHistory: appendStatus(lead.statusHistory, 'qualified', { by }),
32
+ * };
33
+ * }
34
+ */
35
+ /**
36
+ * One row of the immutable history. Generic over the status enum so each
37
+ * domain stays type-safe (`StatusChangeEntry<LeadStatus>` rejects
38
+ * `'shipped'` at compile time).
39
+ */
40
+ interface StatusChangeEntry<TStatus extends string> {
41
+ readonly status: TStatus;
42
+ readonly occurredAt: Date;
43
+ /** Who triggered the transition (user id, system actor name, etc.). */
44
+ readonly by?: string;
45
+ /** Free-form note — disqualify reason, comments, etc. */
46
+ readonly note?: string;
47
+ /**
48
+ * Milliseconds spent in the previous status. `0` for the first entry.
49
+ * Computed at write time so downstream funnel-velocity / SLA aggregates
50
+ * never need to walk timestamps.
51
+ */
52
+ readonly durationInPriorMs: number;
53
+ }
54
+ /**
55
+ * Convenience alias for entity fields: `statusHistory: StatusHistory<LeadStatus>`.
56
+ */
57
+ type StatusHistory<TStatus extends string> = readonly StatusChangeEntry<TStatus>[];
58
+ interface AppendOptions {
59
+ by?: string;
60
+ note?: string;
61
+ /** Override the timestamp — useful for replay / migration. Defaults to `new Date()`. */
62
+ at?: Date;
63
+ }
64
+ /**
65
+ * Append a new status entry, computing `durationInPriorMs` from the last
66
+ * entry (or `0` for the first row). Returns a fresh array — never mutates.
67
+ *
68
+ * The previous-entry lookup is `O(1)` (last element), so this is safe to
69
+ * call from hot paths.
70
+ */
71
+ declare function appendStatus<TStatus extends string>(history: StatusHistory<TStatus>, status: TStatus, options?: AppendOptions): StatusHistory<TStatus>;
72
+ /**
73
+ * Time spent in a status across the entire history. Used for funnel-velocity
74
+ * dashboards: "how long did this lead sit in `qualified` before it converted?".
75
+ *
76
+ * Counts duration on the entries that EXITED `status`. If the entity is
77
+ * currently in `status` and hasn't transitioned out, the still-elapsing time
78
+ * is added from `now`.
79
+ */
80
+ declare function timeInStatus<TStatus extends string>(history: StatusHistory<TStatus>, status: TStatus, now?: Date): number;
81
+ /** Most recent entry, or `null` for an empty history. */
82
+ declare function latestEntry<TStatus extends string>(history: StatusHistory<TStatus>): StatusChangeEntry<TStatus> | null;
83
+ /** Most recent occurrence of `status`, or `null` if it never appeared. */
84
+ declare function lastTransitionTo<TStatus extends string>(history: StatusHistory<TStatus>, status: TStatus): StatusChangeEntry<TStatus> | null;
85
+ //#endregion
86
+ export { AppendOptions, StatusChangeEntry, StatusHistory, appendStatus, lastTransitionTo, latestEntry, timeInStatus };
@@ -0,0 +1,48 @@
1
+ //#region src/workflow/status-history.ts
2
+ /**
3
+ * Append a new status entry, computing `durationInPriorMs` from the last
4
+ * entry (or `0` for the first row). Returns a fresh array — never mutates.
5
+ *
6
+ * The previous-entry lookup is `O(1)` (last element), so this is safe to
7
+ * call from hot paths.
8
+ */
9
+ function appendStatus(history, status, options = {}) {
10
+ const now = options.at ?? /* @__PURE__ */ new Date();
11
+ const prior = history.length === 0 ? void 0 : history[history.length - 1];
12
+ const entry = {
13
+ status,
14
+ occurredAt: now,
15
+ durationInPriorMs: prior === void 0 ? 0 : Math.max(0, now.getTime() - prior.occurredAt.getTime()),
16
+ ...options.by !== void 0 ? { by: options.by } : {},
17
+ ...options.note !== void 0 ? { note: options.note } : {}
18
+ };
19
+ return [...history, entry];
20
+ }
21
+ /**
22
+ * Time spent in a status across the entire history. Used for funnel-velocity
23
+ * dashboards: "how long did this lead sit in `qualified` before it converted?".
24
+ *
25
+ * Counts duration on the entries that EXITED `status`. If the entity is
26
+ * currently in `status` and hasn't transitioned out, the still-elapsing time
27
+ * is added from `now`.
28
+ */
29
+ function timeInStatus(history, status, now = /* @__PURE__ */ new Date()) {
30
+ let total = 0;
31
+ for (let i = 0; i < history.length; i++) {
32
+ const entry = history[i];
33
+ if (i > 0 && history[i - 1].status === status) total += entry.durationInPriorMs;
34
+ }
35
+ if (history.length > 0 && history[history.length - 1].status === status) total += Math.max(0, now.getTime() - history[history.length - 1].occurredAt.getTime());
36
+ return total;
37
+ }
38
+ /** Most recent entry, or `null` for an empty history. */
39
+ function latestEntry(history) {
40
+ return history.length === 0 ? null : history[history.length - 1];
41
+ }
42
+ /** Most recent occurrence of `status`, or `null` if it never appeared. */
43
+ function lastTransitionTo(history, status) {
44
+ for (let i = history.length - 1; i >= 0; i--) if (history[i].status === status) return history[i];
45
+ return null;
46
+ }
47
+ //#endregion
48
+ export { appendStatus, lastTransitionTo, latestEntry, timeInStatus };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/primitives",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Framework-agnostic TypeScript primitives shared across Classytic packages — Money, events, pagination, tenant config, and other building blocks.",
5
5
  "author": "Classytic",
6
6
  "license": "MIT",
@@ -89,6 +89,34 @@
89
89
  "types": "./dist/state-machine.d.mts",
90
90
  "default": "./dist/state-machine.mjs"
91
91
  },
92
+ "./bank-transaction": {
93
+ "types": "./dist/bank-transaction.d.mts",
94
+ "default": "./dist/bank-transaction.mjs"
95
+ },
96
+ "./payment-gateway": {
97
+ "types": "./dist/payment-gateway.d.mts",
98
+ "default": "./dist/payment-gateway.mjs"
99
+ },
100
+ "./phone": {
101
+ "types": "./dist/phone.d.mts",
102
+ "default": "./dist/phone.mjs"
103
+ },
104
+ "./status-history": {
105
+ "types": "./dist/status-history.d.mts",
106
+ "default": "./dist/status-history.mjs"
107
+ },
108
+ "./condition": {
109
+ "types": "./dist/condition.d.mts",
110
+ "default": "./dist/condition.mjs"
111
+ },
112
+ "./mixin": {
113
+ "types": "./dist/mixin.d.mts",
114
+ "default": "./dist/mixin.mjs"
115
+ },
116
+ "./sla-policy": {
117
+ "types": "./dist/sla-policy.d.mts",
118
+ "default": "./dist/sla-policy.mjs"
119
+ },
92
120
  "./package.json": "./package.json"
93
121
  },
94
122
  "files": [