@classytic/primitives 0.27.0 → 0.28.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 +1119 -1081
- package/dist/bank-transaction.d.mts +1 -1
- package/dist/currency.d.mts +1 -1
- package/dist/monetization.d.mts +92 -6
- package/dist/monetization.mjs +68 -5
- package/dist/money-BBOARE5f.mjs +204 -0
- package/dist/{money-CfAvfNiM.d.mts → money-C9iLWiWR.d.mts} +21 -1
- package/dist/money.d.mts +2 -2
- package/dist/money.mjs +3 -177
- package/dist/payment-events.d.mts +1 -1
- package/dist/payment-gateway.d.mts +1 -1
- package/dist/proration.d.mts +1 -1
- package/dist/unit-cost-rate.d.mts +1 -1
- package/dist/unit-cost-rate.mjs +1 -1
- package/package.json +285 -285
package/CHANGELOG.md
CHANGED
|
@@ -1,1081 +1,1119 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
4
|
-
adhering to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
5
|
-
|
|
6
|
-
## 0.
|
|
7
|
-
|
|
8
|
-
### Added
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
## 0.
|
|
45
|
-
|
|
46
|
-
### Added — `
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
##
|
|
83
|
-
|
|
84
|
-
### Added
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
`
|
|
125
|
-
|
|
126
|
-
`
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
### Added — `./
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
`
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
`
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
- **`
|
|
236
|
-
|
|
237
|
-
- **`
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
-
|
|
268
|
-
`
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
is
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
`
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
`
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
`
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
`
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
(
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
(
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
`
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
`
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
###
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
###
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
###
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
###
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
`
|
|
990
|
-
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
- **`/
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
`
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
`
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
- **`/
|
|
1022
|
-
`
|
|
1023
|
-
|
|
1024
|
-
`
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
`
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
`
|
|
1034
|
-
`
|
|
1035
|
-
`
|
|
1036
|
-
`
|
|
1037
|
-
`
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
`
|
|
1041
|
-
|
|
1042
|
-
`
|
|
1043
|
-
`
|
|
1044
|
-
`
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
`
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
`
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
`
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
`
|
|
1075
|
-
`
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
4
|
+
adhering to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
5
|
+
|
|
6
|
+
## 0.28.0
|
|
7
|
+
|
|
8
|
+
### Added: a `pass` monetization kind (paid once, grants a term, never renews)
|
|
9
|
+
|
|
10
|
+
The union could say "charged once" (`one_time`, a single price, no span) and
|
|
11
|
+
"recurring" (`subscription`, priced spans that renew). It could not say
|
|
12
|
+
"prepaid term": a course access pass, a gym or transit pass, a parking permit,
|
|
13
|
+
a term licence. Hosts selling one reached for `subscription`, the only shape
|
|
14
|
+
that carried priced spans, and then had to explain at every read that it does
|
|
15
|
+
not actually renew. `toRevenueMonetizationType` would also have settled those
|
|
16
|
+
sales as recurring income, forecasting revenue nothing was scheduled to bill.
|
|
17
|
+
|
|
18
|
+
`PassMonetization { type: 'pass', passes: AccessPass[] }`, where an
|
|
19
|
+
`AccessPass` is a key, a label, a price, an optional "was" price, and a
|
|
20
|
+
`duration` + `durationUnit`. Shaped like `SubscriptionPlan` so a term means
|
|
21
|
+
the same thing everywhere, kept separate because a plan carries `trialDays`
|
|
22
|
+
and a renewal discount that a pass has none of.
|
|
23
|
+
|
|
24
|
+
Also: `isPassMonetization`, `defaultPassOf` (the cheapest term, ties broken on
|
|
25
|
+
the shorter SPAN so the headline is deterministic), and `approxTermDays` for
|
|
26
|
+
ordering and labels only. `unitPriceOf` returns the cheapest term's price for
|
|
27
|
+
a pass, `compareAtPriceOf` now returns the "was" of that SAME term so a card
|
|
28
|
+
can never strike one term's price through beside another's, and
|
|
29
|
+
`toRevenueMonetizationType('pass')` is `purchase`.
|
|
30
|
+
|
|
31
|
+
A perpetual sale stays `one_time`: "forever" is the absence of a term, not a
|
|
32
|
+
very long one.
|
|
33
|
+
|
|
34
|
+
### Added — `CashDestination` on `./money`
|
|
35
|
+
|
|
36
|
+
`CASH_DESTINATIONS` (`'cash' | 'petty_cash'`), `isCashDestination` and
|
|
37
|
+
`cashDestinationLabel`: WHERE received cash landed, as an intent — never a
|
|
38
|
+
chart-of-accounts code. One vocabulary for a request body, a domain event, a
|
|
39
|
+
client SDK and a settlement dialog, so the accounting tier alone maps it to the
|
|
40
|
+
active chart and a renumber cannot reach any of them. Added because a client
|
|
41
|
+
carried a literal bank code the chart had already renumbered, and every
|
|
42
|
+
settlement it sent was refused.
|
|
43
|
+
|
|
44
|
+
## 0.27.0
|
|
45
|
+
|
|
46
|
+
### Added — `./csv` subpath: one CSV cell escaper for the whole fleet
|
|
47
|
+
|
|
48
|
+
`escapeCsvCell(value)` and `neutralizeCsvFormula(value)`. Six CSV producers
|
|
49
|
+
across ledger, the spine and the hosts each hand-rolled RFC 4180 quoting, and
|
|
50
|
+
not one neutralized a leading `= + - @ \t \r` — so a customer-supplied payment
|
|
51
|
+
reference or party name reached the accountant's spreadsheet as a live formula.
|
|
52
|
+
Quoting does not help: a spreadsheet strips the quotes and evaluates what is
|
|
53
|
+
left, which is why the two defences ship as one module.
|
|
54
|
+
|
|
55
|
+
The two functions are separable on purpose. A consumer holding a real CSV
|
|
56
|
+
writer (`csv-stringify`) takes `neutralizeCsvFormula` only, so its quoting is
|
|
57
|
+
not done twice; a hand-rolled `rows.map(...).join(',')` takes `escapeCsvCell`.
|
|
58
|
+
|
|
59
|
+
**Well-formed numbers are exempt.** Prefixing `-1500` would make every negative
|
|
60
|
+
amount text and break `SUM` in every finance export — a fix that quietly
|
|
61
|
+
corrupts the product is worse than the hole. The exemption requires the WHOLE
|
|
62
|
+
cell to be a number, so `-2+3+cmd|'/c calc'!A0` is still neutralized.
|
|
63
|
+
|
|
64
|
+
Not for a machine-to-machine upload contract with a fixed column spec (a
|
|
65
|
+
courier's bulk-import CSV): there the receiver is a parser, not a spreadsheet,
|
|
66
|
+
and an added `'` corrupts the payload.
|
|
67
|
+
|
|
68
|
+
## 0.26.4
|
|
69
|
+
|
|
70
|
+
### Added — `./price-basis` subpath: measured pricing across physical dimensions
|
|
71
|
+
|
|
72
|
+
New `PriceBasis` type (`FixedPriceBasis | MeasuredPriceBasis`) models how a quoted price spans a sellable quantity — flat fixed or per-unit-of-measure (mass, duration, volume, length, area). Factory functions `fixedPriceBasis()` and `measuredPriceBasis(dimension, qtyUnit, priceUnit, qtyPerPriceUnit)` construct the two shapes; `isPriceBasis()` is a structural guard for DB/wire boundaries. `resolvePriceBasisTotal(unitPriceMinor, quantity, basis?)` converts a minor-unit price × integer quantity to a final minor-unit total using half-even rounding (`roundedDiv` internally) and throws `PriceBasisError` on unsafe inputs or overflow.
|
|
73
|
+
|
|
74
|
+
### Added — `./unit-cost-rate` re-exports `RoundingMode`
|
|
75
|
+
|
|
76
|
+
`RoundingMode` (`'half-even' | 'half-up'`) is now defined in the new internal `rounded-div.ts` and re-exported from `unit-cost-rate`. No breaking change — existing consumers who import `RoundingMode` from `@classytic/primitives/unit-cost-rate` continue to work.
|
|
77
|
+
|
|
78
|
+
### Changed — `validateAddress` / `isAddressValid` accept `Address` directly
|
|
79
|
+
|
|
80
|
+
Both functions now accept `Address | PartialAddress | null | undefined`, removing the need for a cast when validating a fully-typed value object.
|
|
81
|
+
|
|
82
|
+
## 0.26.3
|
|
83
|
+
|
|
84
|
+
### Added — `PaymentProviderPort.wantsRawBody`
|
|
85
|
+
|
|
86
|
+
Optional `readonly wantsRawBody?: boolean` on the payment gateway port.
|
|
87
|
+
Providers that verify an HMAC signature (Stripe signs the bytes it transmitted)
|
|
88
|
+
must declare `true` — the transport then hands the raw body rather than the
|
|
89
|
+
parsed object. Absent / `false` preserves the historical behaviour (parsed
|
|
90
|
+
body), which is correct for manual, cash and terminal providers that sign
|
|
91
|
+
nothing.
|
|
92
|
+
|
|
93
|
+
## 0.26.2
|
|
94
|
+
|
|
95
|
+
### Added — `stockKeyFor` (`/item-identity`): one stock-key precedence for every package
|
|
96
|
+
|
|
97
|
+
The product-level stock key, with a single precedence:
|
|
98
|
+
|
|
99
|
+
1. the variant's stamped `skuRef` — assigned once at birth, immutable
|
|
100
|
+
2. a **sole** variant with no stamp → the product's `_id`, never the label
|
|
101
|
+
(legacy writers derived `variantSku || productId`, and for one variant that
|
|
102
|
+
key IS the `_id`)
|
|
103
|
+
3. a **sibling** with no stamp → its label, which genuinely is its legacy key
|
|
104
|
+
4. nothing addressable → the product's `_id`
|
|
105
|
+
|
|
106
|
+
The caller declares only what it HAS — `{ kind: 'sole' | 'label' | 'variant' }`
|
|
107
|
+
— so the sole-vs-sibling decision is internal and a caller cannot make it wrong.
|
|
108
|
+
That choice previously lived at every call site across three packages, in two
|
|
109
|
+
functions with different fallbacks; they disagreed on a sole, unstamped,
|
|
110
|
+
labelled variant, and the read then looked up a key no quant is stored under.
|
|
111
|
+
|
|
112
|
+
It lives here, below every domain, because `@spinekit/order` and
|
|
113
|
+
`@spinekit/catalog` must not import a peer domain — the rule sitting one layer
|
|
114
|
+
too high is exactly why each package re-derived it.
|
|
115
|
+
|
|
116
|
+
## 0.26.1
|
|
117
|
+
|
|
118
|
+
- `./testing/stock-key-seam` — new subpath; `scanStockKeySeam`, `assertStockKeySeam`, `takesStockKeyFromLabel`, `STOCK_KEY_SEAM_FIXTURES` for conformance-scanning packages that resolve stock keys
|
|
119
|
+
|
|
120
|
+
## [0.26.0] — 2026-08-27
|
|
121
|
+
|
|
122
|
+
### Added
|
|
123
|
+
|
|
124
|
+
- **`./deadline` subpath** — `Deadline` value type + helpers for date-bounded
|
|
125
|
+
obligations (reservations, SLA windows, contract terms).
|
|
126
|
+
- **`./stock-level` subpath** — canonical stock-level representation and helpers.
|
|
127
|
+
- **`./item-facts` subpath** — cross-kernel item fact contract (weight, dimensions,
|
|
128
|
+
hazmat flags) that catalog declares and logistics/inventory reads.
|
|
129
|
+
- **`StockKey` branded type** (`./item-identity`) — prevents a bare merchandising
|
|
130
|
+
label or ObjectId being passed where a quant key is expected; the only source is
|
|
131
|
+
`stockKeyOf()` / `requireStockKey()`. Return types of both functions updated.
|
|
132
|
+
- **Upfront collection policy** (`./monetization`) — `UpfrontCollectionPolicy`
|
|
133
|
+
(`'full_payment' | 'deposit' | 'deferred'`) + `UPFRONT_COLLECTION_POLICIES`
|
|
134
|
+
const. Cross-kernel: catalog declares the policy, order turns it into tenders,
|
|
135
|
+
revenue settles them.
|
|
136
|
+
|
|
137
|
+
## [0.25.0] - 2026-08-24
|
|
138
|
+
|
|
139
|
+
### Added
|
|
140
|
+
|
|
141
|
+
- **`@classytic/primitives/item-identity` subpath** — `SkuFields`, `stockKeyOf`, `skuDisplay`. `sku` is a label; `skuRef` is the stock identity — the two fall back in opposite directions and getting one backwards type-checks while returning a plausible wrong string. `stockKeyOf` resolves `skuRef ?? sku ?? fallback` (the key to reserve, decrement and post against); `skuDisplay` resolves `skuLabel ?? sku ?? skuRef` (what a human reads). Canonicalises the per-package `variantSku || String(productId)` derivations into one place.
|
|
142
|
+
|
|
143
|
+
## [0.24.0] - 2026-08-24
|
|
144
|
+
|
|
145
|
+
### Added — `@classytic/primitives/address`: capture-format system
|
|
146
|
+
|
|
147
|
+
`AddressFormat`, `AddressFieldDescriptor`, `AddressFieldKey`, `PartialAddress`, `AddressFieldError` — a description of HOW a country collects a `ContactAddress`: field labels, order, requiredness, subdivision taxonomy hooks, and postal code pattern. Country packs (e.g. `@classytic/bd-areas/address-format`) live beside their taxonomies; this subpath owns only the generic shape.
|
|
148
|
+
|
|
149
|
+
`validateAddress(address, format)` — validates a partial address against a format; returns `AddressFieldError[]`. Enforces `required` on both canonical and extension fields identically, and checks `postalCodePattern` only when a value is given and a pattern is declared. `isAddressValid` and `describeAddressError` are convenience wrappers.
|
|
150
|
+
|
|
151
|
+
Every `key` in a descriptor is a field of `ContactAddress`; co-location with the value object is what makes a mismatch obvious instead of invisible.
|
|
152
|
+
|
|
153
|
+
## [0.23.0] - 2026-08-15
|
|
154
|
+
|
|
155
|
+
### Added — `./approval`: `notRequiredChain(reason)` + `isNotRequired(chain)`
|
|
156
|
+
|
|
157
|
+
The chain for a subject that needs **no** approval — zero steps, already resolved. The
|
|
158
|
+
explicit, greppable answer to "the policy engine ran and decided this document does not
|
|
159
|
+
require approval", which callers previously had no way to express.
|
|
160
|
+
|
|
161
|
+
This states a conclusion the module already reached rather than inventing one:
|
|
162
|
+
`computeChainStatus` folds an empty step list to `'approved'` — "nothing outstanding" is
|
|
163
|
+
what approved MEANS here, and that fold predates this function. `createChain` still throws
|
|
164
|
+
`EMPTY_STEPS`: building a ROUTING chain that routes nowhere is a different mistake and
|
|
165
|
+
stays refused. The two are deliberately not interchangeable, so reaching "approved with no
|
|
166
|
+
approvers" requires saying so by name.
|
|
167
|
+
|
|
168
|
+
**Why a chain rather than leaving the field unset.** Absence is the one input that occurs
|
|
169
|
+
by accident — a schema that never declared `approvals` strips it on write — and
|
|
170
|
+
`assertApproved` must keep treating that as `CHAIN_MISSING`. A present zero-step chain is
|
|
171
|
+
distinguishable from a lost chain (absent) *and* from a granted one (has steps and
|
|
172
|
+
decisions), so existing gates keep working through `isApproved` alone. No consumer needs a
|
|
173
|
+
second predicate, which is what would drift.
|
|
174
|
+
|
|
175
|
+
`isNotRequired` tests the STRUCTURE (`steps.length === 0`), not the new
|
|
176
|
+
`notRequiredReason` field: a subject schema that never declared that field drops it on
|
|
177
|
+
write, so a predicate reading it would answer differently before and after the save. The
|
|
178
|
+
reason is provenance for the audit trail only — pinned by a test that strips it and asserts
|
|
179
|
+
the predicate is unmoved.
|
|
180
|
+
|
|
181
|
+
**The hazard, stated in the docblock:** this makes `isApproved` true with no human
|
|
182
|
+
decision. It is correct only where a policy engine ASKED and got "no policy applies" —
|
|
183
|
+
never as a fallback for a lookup that failed or was never configured. First consumer:
|
|
184
|
+
`@classytic/arc-approval` 0.4.0 `whenNoPolicy: 'not-required'`.
|
|
185
|
+
|
|
186
|
+
## [0.22.0] - 2026-08-12
|
|
187
|
+
|
|
188
|
+
### Added — outbox fencing tokens (`claimPendingFenced`)
|
|
189
|
+
|
|
190
|
+
- **`OutboxStore.claimPendingFenced?`** (additive, feature-detected — mirrors repo-core's `LockAdapter.tryAcquireFenced`): same atomic semantics as `claimPending`, returning `OutboxClaimedEvent { event, fencingToken }`. The token is minted by the STORE per claim epoch — a takeover after lease expiry mints STRICTLY GREATER, so a stale ex-holder's `acknowledge`/`fail` (now accepting `fencingToken?`) throws `OutboxOwnershipError`. This closes the overlap that consumer-id ownership checks narrow but cannot close: holder ids recur; epochs do not.
|
|
191
|
+
- `MemoryOutboxStore` implements it; `claimPending` delegates to the fenced path (one claim implementation). Stale-ack rejection is mutation-proven.
|
|
192
|
+
|
|
193
|
+
### Added — `./period`: `shiftPeriod(period, n)` — calendar arithmetic without instants
|
|
194
|
+
|
|
195
|
+
Shift a MONTH or QUARTER period by `n` of its own units (negative = back); YEAR shifts by years; a `custom` (ad-hoc start/end) period has no unit to shift and throws rather than guessing one. No `Date` is constructed and no zone is consulted — shifting a month is pure `(year, month)` arithmetic, and the `timezone` rides along unchanged so the shifted period still resolves against the calendar it was defined in.
|
|
196
|
+
|
|
197
|
+
Exists because both inline shapes callers reach for are wrong the same way: `new Date(Date.UTC(y, m - 1 - n, 1))` answers a business-calendar question with a UTC instant (six hours off at both ends in Asia/Dhaka — the bug class this module's header documents five instances of), and hand-rolled `y * 12 + (m - 1) - n` integer math is a third copy of a rule `Period` already owns, with the year rollover exactly where copies go wrong. First consumer: be-prod `business-date.ts` ("two filing months ago").
|
|
198
|
+
|
|
199
|
+
**Versioning note:** this export briefly existed in a working copy labelled 0.21.0 after 0.21.0 had already been published without it — same version, different surface. 0.22.0 restores the invariant that a version number names exactly one contract; `>=0.22.0` is the correct floor for any consumer importing `shiftPeriod`.
|
|
200
|
+
|
|
201
|
+
### Docs — `./environment` docblock refinement (no API change)
|
|
202
|
+
|
|
203
|
+
The 0.21.0 entry below originally listed `isProduction` / `isDevelopment` / `isTest`; the published surface is `classifyEnv`, `EnvClass`, `isProductionEnv` only — `isProductionEnv` is deliberately the single predicate (consumers needing all three buckets build their own triple from `classifyEnv`). The entry below is corrected in place; runtime and declarations are byte-identical to published 0.21.0 apart from comments.
|
|
204
|
+
|
|
205
|
+
## [0.21.0] - 2026-08-10
|
|
206
|
+
|
|
207
|
+
### Added — `./environment`: canonical `NODE_ENV` classification
|
|
208
|
+
|
|
209
|
+
`classifyEnv(raw)` — total function mapping a raw `NODE_ENV` string to `EnvClass = 'development' | 'test' | 'production'`. Every input yields exactly one bucket; no fourth state or `undefined`. Handles both spellings per bucket (`production`/`prod`, `test`/`qa`), trims whitespace (`.env` files and CI variables routinely carry trailing newlines), and defaults unknown values to `development` — the safe-fail direction that keeps local tooling functional.
|
|
210
|
+
|
|
211
|
+
Collapses four copies across the codebase that each missed at least one spelling: `be-prod classifyEnv`, `gym businessMode()`, arc's CLI config template (`z.preprocess`), and arc's four raw comparison sites. The missed spellings were all in the dangerous direction — a `NODE_ENV=prod` instance that matched nothing fell through to `development`, opening security gates (`Secure` cookie flag, arc's dev preset, non-transactional money writes, raw 500 messages to the client) in production.
|
|
212
|
+
|
|
213
|
+
Exports: `classifyEnv`, `EnvClass`, `isProductionEnv`.
|
|
214
|
+
|
|
215
|
+
### Added — `publint` gate on build
|
|
216
|
+
|
|
217
|
+
`publint: true` in `tsdown.config.ts` — catches export-map entries pointing at non-existent dist files at build time rather than at consumer install time.
|
|
218
|
+
|
|
219
|
+
## [0.20.0] - 2026-08-05
|
|
220
|
+
|
|
221
|
+
### Added — `./period` OWNS timezone-aware period resolution (was two copies above the kernels)
|
|
222
|
+
|
|
223
|
+
`Period` was `{ year, month?, quarter? }` with no timezone and no resolver, so it
|
|
224
|
+
could not express "the business month of August in Asia/Dhaka". The resolver
|
|
225
|
+
existed TWICE, both ABOVE the kernels — `@spinekit/kit/period`
|
|
226
|
+
(`createBusinessCalendar`) and be-prod's `#lib/utils/business-date` — which meant
|
|
227
|
+
a kernel could reach neither and re-derived the boundary inline with `Date.UTC`.
|
|
228
|
+
Five silent UTC-month defects came out of that (`aggregateMonthlyVat`, the
|
|
229
|
+
sales-fact reconciler, and three in bd-tax Mushak).
|
|
230
|
+
|
|
231
|
+
This is a MOVE of the proven `createBusinessCalendar` logic, not a re-derivation:
|
|
232
|
+
same civil-date round-trip through `/timezone`, same half-open ranges, same
|
|
233
|
+
DST-exactness.
|
|
234
|
+
|
|
235
|
+
- **`Period.timezone?`** — the IANA zone a period is DEFINED in, so a persisted or
|
|
236
|
+
transported period keeps its own calendar.
|
|
237
|
+
- **`resolvePeriod(period, timezone?)`** → half-open `DateRange`. Throws when no
|
|
238
|
+
zone is available anywhere (never silently answers in UTC) and throws when the
|
|
239
|
+
period's zone and the caller's zone DISAGREE.
|
|
240
|
+
- **`resolveDay` / `resolveMonth` / `resolveQuarter` / `resolveYear` /
|
|
241
|
+
`resolveDateSpan` / `dayStart`** — the granularity-specific forms. All return
|
|
242
|
+
HALF-OPEN `[start, end)`, which tiles exactly.
|
|
243
|
+
- **`periodOf(instant, granularity, timezone)`** — the inverse: which filing
|
|
244
|
+
period an instant belongs to on the LOCAL calendar.
|
|
245
|
+
- **`parsePeriod` / `formatPeriod` / `granularityOf` / `periodTimeZone`** —
|
|
246
|
+
`'YYYY'` · `'YYYY-MM'` · `'YYYY-Qn'`. `parsePeriod` REFUSES a `'YYYY-MM-DD'`
|
|
247
|
+
rather than reading it as its month (a ~30× widening).
|
|
248
|
+
- **`inclusiveEnd(range)`** — `end - 1ms` for legacy `$lte` call sites, derived
|
|
249
|
+
from the resolved end so it stays DST-exact.
|
|
250
|
+
- **`PeriodError`** + **`PeriodErrorCode`** (`AMBIGUOUS_PERIOD` ·
|
|
251
|
+
`INCOMPLETE_RANGE` · `INVALID_FIELD` · `MISSING_TIMEZONE` · `TIMEZONE_CONFLICT`
|
|
252
|
+
· `INVALID_LABEL`).
|
|
253
|
+
|
|
254
|
+
A `Period` carrying two resolution shapes (`month` + `quarter`, or either with
|
|
255
|
+
`start`/`end`) now throws instead of preferring one.
|
|
256
|
+
|
|
257
|
+
### Added — `./idempotency`: the claim + lease CONTRACT
|
|
258
|
+
|
|
259
|
+
Three kernels hand-rolled incompatible versions of one state machine — `cart`
|
|
260
|
+
(`in_flight`/`succeeded`/`failed`, the only one with crash-window leasing),
|
|
261
|
+
`catalog` offers (`pending`/`completed`, no lease), `contract` amendments
|
|
262
|
+
(implicit). Types + PURE helpers only; no persistence, no collection, no driver.
|
|
263
|
+
|
|
264
|
+
- **`ClaimOutcome = 'claimed' | 'replayed' | 'in_flight'`** — three-valued, and
|
|
265
|
+
`in_flight` is NOT a failure. Mapping a live concurrent lease onto an error
|
|
266
|
+
licenses a retry, which is a double-apply.
|
|
267
|
+
- **`IdempotencyClaim`** (`identity` · `requestFingerprint` · `state` ·
|
|
268
|
+
`leaseToken` · `leaseExpiresAt` · `attempts` · `result`), **`ClaimResult`**,
|
|
269
|
+
**`ClaimState`**, **`IdempotencyIdentity`**.
|
|
270
|
+
- **`decideClaim(existing, request)`** — the whole state machine as one pure
|
|
271
|
+
function. Throws `FINGERPRINT_MISMATCH` (same key, different body) and
|
|
272
|
+
`MISSING_REPLAY` (terminal record with no stored result) rather than answering.
|
|
273
|
+
- **`completeClaim` / `renewLease`** — both refuse when the caller lost its lease,
|
|
274
|
+
so a superseded attempt cannot overwrite the winner's result.
|
|
275
|
+
- **`identityKey`** (canonical-JSON composite, so a delimiter inside a scope value
|
|
276
|
+
cannot collide two identities), **`fingerprintRequest`**, **`newLeaseToken`**,
|
|
277
|
+
**`isLeaseExpired`**, **`holdsLease`**, **`isInFlight`**, **`DEFAULT_LEASE_MS`**.
|
|
278
|
+
|
|
279
|
+
### Changed — BREAKING: five API decisions taken before spine wiring starts
|
|
280
|
+
|
|
281
|
+
- **`./cadence` — `Cadence.timezone` is now AUTHORITATIVE, not display-only.**
|
|
282
|
+
It was documented as decorative while `stepMonthly`/`stepYearly` used
|
|
283
|
+
`getUTCMonth()`/`getUTCDate()`, so `timezone: 'Asia/Dhaka'` + `dayOfMonth: 1`
|
|
284
|
+
with a midnight anchor placed every occurrence on the local **2nd**. All four
|
|
285
|
+
kinds now run on the zone's wall clock when one is set (daily advances LOCAL
|
|
286
|
+
days — 23h/25h across a DST transition), and stay UTC when it is absent.
|
|
287
|
+
`validateCadence` rejects an unresolvable zone with the new
|
|
288
|
+
`INVALID_TIMEZONE`. **Behavioural change for any persisted cadence that
|
|
289
|
+
already carries a `timezone`** — the occurrences move to where the field
|
|
290
|
+
always claimed they were.
|
|
291
|
+
- **`./approval` — `assertApproved` no longer passes on a `null` chain.** It now
|
|
292
|
+
throws the new `CHAIN_MISSING`. Absence is not approval, and a mongoose schema
|
|
293
|
+
that never declared `approvalChain` strips it on write, so the gate could not
|
|
294
|
+
tell "no approval configured" from "approval silently lost". The explicit
|
|
295
|
+
opt-out is the new **`assertApprovedIfPresent`**. The second parameter is now
|
|
296
|
+
an options object (`{ message }`), not a bare string.
|
|
297
|
+
- **`./payment-events` — every payload carries an `eventType` discriminant, and
|
|
298
|
+
there is a `payment.unknown` event.** `PaymentInitiatedPayload` and
|
|
299
|
+
`PaymentSucceededPayload` were structurally IDENTICAL with no discriminant on
|
|
300
|
+
the union, so a handler typed for "funds received" accepted "money in flight".
|
|
301
|
+
Adds **`PaymentUnknownPayload`** (the port contract's third outcome had no
|
|
302
|
+
event, so an unobserved result had to be forced into SUCCEEDED or FAILED),
|
|
303
|
+
**`PaymentEventPayloadMap`**, **`isPaymentEvent`**, **`isFundsReceived`**, and
|
|
304
|
+
three compile-time drift guards over the map.
|
|
305
|
+
- **`./currency` — `convertWithSnapshot` / `reverseWithSnapshot` take and return
|
|
306
|
+
`Money`.** A bare `number` meant nothing checked the amount was denominated in
|
|
307
|
+
`fx.sourceCurrency`; a USD→BDT snapshot applied to a EUR amount type-checked
|
|
308
|
+
and returned a plausible wrong number. Both now throw `CurrencyMismatchError`
|
|
309
|
+
on a mismatch, handle differing minor-unit exponents (USD 2 → JPY 0), and take
|
|
310
|
+
an explicit **`FxRounding`** (`'half-away-from-zero'` default · `'floor'` ·
|
|
311
|
+
`'ceil'`) instead of returning an unrounded float for each caller to round
|
|
312
|
+
differently.
|
|
313
|
+
- **`./money` — `Money.currency` is `CurrencyCode`, not `CurrencyCode | string`.**
|
|
314
|
+
A union with `string` collapses to `string`, so the brand was decoration on
|
|
315
|
+
the one type where it matters and `money()` validated nothing. `money()`,
|
|
316
|
+
`fromMajor()` and `sumMoney()` now validate and brand; `isMoney` format-checks
|
|
317
|
+
the code instead of `typeof === 'string'`. New **`currencyCode(value)`**
|
|
318
|
+
(throwing constructor) and **`InvalidCurrencyCodeError`**.
|
|
319
|
+
**`CurrencyMismatchError` is now DECLARED in `./currency`** (so the FX seam can
|
|
320
|
+
throw it without an ESM cycle) and re-exported from `./money`, so existing
|
|
321
|
+
imports are unaffected.
|
|
322
|
+
|
|
323
|
+
Migration: a `Money` object literal now needs a branded code —
|
|
324
|
+
`money(1999, 'USD')`, or `{ amount: 1999, currency: currencyCode('USD') }`.
|
|
325
|
+
|
|
326
|
+
### Changed — six silent-permissiveness defects now FAIL instead of answering
|
|
327
|
+
|
|
328
|
+
Every item below previously returned a plausible-looking value where it should
|
|
329
|
+
have errored. All are behaviour changes for malformed input only; correct input
|
|
330
|
+
is unaffected. Falsifying tests live in `tests/unit/fail-loud-regressions.test.ts`.
|
|
331
|
+
|
|
332
|
+
- **`./period` — an Invalid Date no longer reports "no overlap".** `rangesOverlap`,
|
|
333
|
+
`isWithin` and `rangeDurationMs` throw the new `DateRangeError` on a non-finite
|
|
334
|
+
endpoint; `isDateRange` returns `false` for one. Every comparison against `NaN`
|
|
335
|
+
is `false`, so a corrupt range used to answer "no conflict" — the PERMISSIVE
|
|
336
|
+
answer for the three documented consumers (order booking collision, flow
|
|
337
|
+
reservation-window collision, promo campaign overlap), i.e. a silent double-book.
|
|
338
|
+
- **`./currency` — the ISO 4217 exponent table is now exhaustive.** All 0-decimal
|
|
339
|
+
(`BIF CLP DJF GNF ISK KMF KRW PYG RWF UGX UYI VND VUV XAF XOF XPF`), 3-decimal
|
|
340
|
+
(`BHD IQD JOD KWD LYD OMR TND`) and 4-decimal (`CLF UYW`) currencies are listed,
|
|
341
|
+
so `minorUnitFactor`'s `?? 100` fallback is reachable only by a code that is not
|
|
342
|
+
a real ISO currency. Previously `fromMajor(1500, 'GNF')` stored 150 000 minor
|
|
343
|
+
units — a 100× error that looks like a number. New `isKnownCurrency(code)`
|
|
344
|
+
distinguishes "listed" from "assumed" for boot-time config validation.
|
|
345
|
+
- **`./proration` — a missing allocated part throws instead of becoming `0`.** The
|
|
346
|
+
`?? 0` fallbacks in `splitByPeriodFraction` / `allocateMoneyByFraction` are now
|
|
347
|
+
`ProrationError('ALLOCATION_INVARIANT')`. A zero credit and a genuinely
|
|
348
|
+
fully-consumed period were indistinguishable downstream.
|
|
349
|
+
- **`./state-machine` — `assertAndClaim` rejects an EMPTY `from` list.**
|
|
350
|
+
`validSources(to)` returns `[]` for an unreachable status, and the documented
|
|
351
|
+
`from: machine.validSources(x)` idiom then ran the assert loop zero times and
|
|
352
|
+
handed the CAS an empty allow-list. An empty allow-list must never mean "any".
|
|
353
|
+
- **`./timezone` — the offset regex is anchored.** An offset label the runtime
|
|
354
|
+
emits in an unexpected shape (e.g. an unpadded `GMT+5:30`) matched the optional
|
|
355
|
+
bare-`GMT` branch and resolved to **UTC**, silently shifting every business-day
|
|
356
|
+
boundary derived from it. Unrecognised labels now throw; `\d{1,2}` accepts the
|
|
357
|
+
unpadded variant rather than mis-reading it.
|
|
358
|
+
- **`./canonical` — class instances are rejected, not collapsed.** A
|
|
359
|
+
`Types.ObjectId`, `Buffer` or hydrated Mongoose subdocument has no own
|
|
360
|
+
enumerable keys, so it digested as `{}` and two manifests referencing DIFFERENT
|
|
361
|
+
documents hashed identically — the integrity check reported "unchanged".
|
|
362
|
+
`canonicalJson` now throws `CanonicalizeError` for any object whose prototype is
|
|
363
|
+
neither `Object.prototype` nor `null`. Plain objects, null-prototype objects,
|
|
364
|
+
arrays and `Date` are unaffected.
|
|
365
|
+
|
|
366
|
+
### Changed — the publish smoke gate now sweeps the whole export map
|
|
367
|
+
|
|
368
|
+
`tests/smoke/smoke.mjs` derives its subpath list from `package.json#exports`
|
|
369
|
+
(44/44) instead of a hand-maintained list that covered 23, and asserts each entry
|
|
370
|
+
declares both `types` and `default` and that both files exist. The gate runs in
|
|
371
|
+
`prepublishOnly`; previously a subpath that lost its tsdown entry would have
|
|
372
|
+
published an export map pointing at a missing file with the gate printing OK.
|
|
373
|
+
|
|
374
|
+
## [0.19.0] - 2026-08-01
|
|
375
|
+
|
|
376
|
+
### Added — `./monetization`: canonical pricing classification contract
|
|
377
|
+
|
|
378
|
+
- **`@classytic/primitives/monetization`** — the single home for how a product
|
|
379
|
+
is sold and priced, so the classification survives end-to-end across catalog,
|
|
380
|
+
revenue, order, and entitlement kernels without each maintaining its own enum.
|
|
381
|
+
- **`Monetization`** discriminated union — `FreeMonetization | OneTimeMonetization |
|
|
382
|
+
SubscriptionMonetization | BundleMonetization | UsageMonetization`, each
|
|
383
|
+
carrying its own nested pricing shape.
|
|
384
|
+
- **`MONETIZATION_KINDS`** + **`MonetizationKind`** + **`isMonetizationKind`** —
|
|
385
|
+
exhaustive kind list with runtime guard.
|
|
386
|
+
- Type-guard predicates: **`isFreeMonetization`**, **`isOneTimeMonetization`**,
|
|
387
|
+
**`isSubscriptionMonetization`**, **`isBundleMonetization`**,
|
|
388
|
+
**`isUsageMonetization`**.
|
|
389
|
+
- Supporting types: `PriceTier`, `OneTimePricing`, `SubscriptionPlan`,
|
|
390
|
+
`UsageTier`, `UsageRating`, `DurationUnit`, `MoneyByCurrency`.
|
|
391
|
+
|
|
392
|
+
### Added — `./proration`: pure proration arithmetic
|
|
393
|
+
|
|
394
|
+
- **`@classytic/primitives/proration`** — timezone-agnostic, policy-free
|
|
395
|
+
proration math. Holds no commercial policy (no upgrade/downgrade semantics,
|
|
396
|
+
no discount or tax logic) — those live in the contract kernel.
|
|
397
|
+
- **`periodProgress(input)`** — fraction of a billing period consumed given
|
|
398
|
+
start, end, and current instants as `Date`. Returns `PeriodFraction`.
|
|
399
|
+
- **`splitByPeriodFraction(amount, fraction, granularity?)`** — split a `Money`
|
|
400
|
+
amount into consumed/remaining parts; `granularity: 'whole_day'` (default) or
|
|
401
|
+
`'exact'`.
|
|
402
|
+
- **`allocateMoneyByFraction(amount, fraction)`** — proportional allocation
|
|
403
|
+
using the largest-remainder allocator so `consumed + remaining === amount`
|
|
404
|
+
exactly (no penny drift).
|
|
405
|
+
- **`ProrationError`** + **`ProrationErrorCode`** (`INVALID_PERIOD` |
|
|
406
|
+
`INVALID_FRACTION`) — typed errors for out-of-range inputs.
|
|
407
|
+
- Supporting types: `PeriodFraction`, `PeriodProgressInput`,
|
|
408
|
+
`ProrationGranularity`.
|
|
409
|
+
|
|
410
|
+
## [0.18.0] - 2026-07-29
|
|
411
|
+
|
|
412
|
+
### Added — `./subject`: `SubjectRef` polymorphic identity pointer
|
|
413
|
+
|
|
414
|
+
- **`@classytic/primitives/subject`** — `SubjectRef` (`{ subjectModel, subjectRef }`) — WHO a record is about, polymorphically. The pairing spans every kernel that holds data about a person without owning them (attendance, access, loyalty, credentials). Deliberately kept distinct from `ExternalRef` (provenance — "came from") even though the shapes are identical: a grant carries both at once and they mean different things.
|
|
415
|
+
- **`buildSubjectRef(model, id)`** — returns `SubjectRef | null`; `null` when `id` is absent/blank, because a malformed binding silently matches nothing (an entitlement nobody holds, a credential no revoke can find) rather than carrying `"undefined"` as a string.
|
|
416
|
+
- **`SubjectRef`** type exported from `@classytic/primitives/subject`.
|
|
417
|
+
|
|
418
|
+
### Added — `./state-diagram`: Mermaid diagram from a `StateMachine`
|
|
419
|
+
|
|
420
|
+
- **`@classytic/primitives/state-diagram`** — `renderStateDiagram(machine, opts?)` — generates a `stateDiagram-v2` Mermaid string from the same `StateMachine` object the runtime CAS enforces. Removes the class of drift where a hand-drawn prose comment diverges from the actual transition table. Pure: string in, string out, no I/O.
|
|
421
|
+
- **`StateDiagramOptions`** — `title?`, `highlight?` (mark specific states), `noteOn?` (per-state annotations).
|
|
422
|
+
|
|
423
|
+
### Added — `./suspension`: time-bounded suspension/pause policy
|
|
424
|
+
|
|
425
|
+
- **`@classytic/primitives/suspension`** — pure suspension policy computation (no persistence). Models a pause the subject is ENTITLED to take, bounded by an annual allowance, that ends by itself — distinct from `/hold` (an indefinite blocker needing manual resolution).
|
|
426
|
+
- **`evaluateSuspension(policy, history, opts)`** — always returns `autoResumeAt` (the instant the pause MUST end) so a sweep can resume it whether or not the subject ever comes back. Covers: gym membership freeze, subscription pause, unpaid leave.
|
|
427
|
+
- **`SuspensionPolicy`** — `maxDaysPerPeriod`, `minDurationDays`, `period` ('annual' | 'rolling').
|
|
428
|
+
- **`SuspensionEntry`** — a historical pause record (`startedAt`, `endedAt?`, `reason?`).
|
|
429
|
+
- **`SuspensionResult`** — `autoResumeAt`, `daysUsed`, `daysRemaining`, `status`.
|
|
430
|
+
|
|
431
|
+
### Added — `OutboxStore.transactionalSave` (`/outbox`)
|
|
432
|
+
|
|
433
|
+
- **`OutboxStore.transactionalSave?: boolean`** — declares whether `save` enlists `options.session` in the caller's transaction. Absent/`false` = "assume NOT atomic". Set `true` ONLY when `save` passes the session to the same transactional resource the caller writes (e.g. a mongoose model on the same connection). Callers that need atomicity (`@classytic/access`'s `atomically()`) check this flag and fail at boot rather than claiming a guarantee they cannot keep.
|
|
434
|
+
- **`MemoryOutboxStore.transactionalSave = false`** — explicitly declared; an in-memory store cannot participate in a Mongo transaction, so its write survives a rollback.
|
|
435
|
+
|
|
436
|
+
### Changed — `shelf-life.ts` deduplicates `addDays`
|
|
437
|
+
|
|
438
|
+
- `addDays` is now imported from `./calendar` instead of being re-defined inline. No behavior change.
|
|
439
|
+
|
|
440
|
+
## [0.16.0] - 2026-07-27
|
|
441
|
+
|
|
442
|
+
### Added — `assertApproved` + `CHAIN_INCOMPLETE` (`/approval`)
|
|
443
|
+
|
|
444
|
+
- **`ApprovalErrorCode` gains `'CHAIN_INCOMPLETE'`** — the single canonical
|
|
445
|
+
error code for "finalize/commit attempted before the chain reached `approved`".
|
|
446
|
+
Hosts route it through `rethrowApprovalError` to the `approval.chain_incomplete`
|
|
447
|
+
wire code so all commit/post/finalize boundaries raise the same structured error.
|
|
448
|
+
- **`assertApproved(chain, message?)`** — canonical finalize-time gate: asserts
|
|
449
|
+
the chain has reached `approved`, else throws `ApprovalError('CHAIN_INCOMPLETE')`.
|
|
450
|
+
A `null`/absent chain passes (no chain required). Use at every journal-entry
|
|
451
|
+
post, PO approve, transfer dispatch, etc. to replace per-domain hand-rolled
|
|
452
|
+
divergent codes with one definition.
|
|
453
|
+
|
|
454
|
+
Purely additive — no existing exports changed.
|
|
455
|
+
|
|
456
|
+
## [0.15.0] - 2026-07-25
|
|
457
|
+
|
|
458
|
+
### Added — `/retention`: `PurgeEvidence` value object
|
|
459
|
+
|
|
460
|
+
- **New `@classytic/primitives/retention` subpath** exporting **`PurgeEvidence`**
|
|
461
|
+
— the persisted evidence record for a GDPR / data-retention purge operation
|
|
462
|
+
(proof of what was purged, over what scope, by whom, why, and whether
|
|
463
|
+
analytical measures were retained). Shape: `{ id, subject: { ref, model? },
|
|
464
|
+
scope, strategy: 'hard'|'soft'|'anonymize', measuresRetained, processed,
|
|
465
|
+
occurredAt, actor: { ref, kind: 'user'|'system'|'service' }, reason,
|
|
466
|
+
legalBasis?, status: 'completed'|'partial'|'failed', operationId?,
|
|
467
|
+
startedAt?, completedAt?, results?: PurgeResourceResult[],
|
|
468
|
+
verification?: PurgeVerificationSummary }`.
|
|
469
|
+
- **`createPurgeEvidence(input)`** — pure builder (auto-fills `id` + `occurredAt`
|
|
470
|
+
when absent, defaults `status` to `'completed'`). **`isPurgeEvidence(value)`**
|
|
471
|
+
type guard + **`assertPurgeEvidence(value)`** throwing assertion.
|
|
472
|
+
|
|
473
|
+
### Added — `/canonical`: strict deterministic JSON + integrity digest
|
|
474
|
+
|
|
475
|
+
- **New `@classytic/primitives/canonical` subpath** — `canonicalJson(value)`
|
|
476
|
+
produces a deterministic string (keys sorted recursively, `Date` → `{"$date":"<iso>"}`,
|
|
477
|
+
strict rejection of `undefined`/`NaN`/`BigInt`/`Map`/`Set`/cycles with
|
|
478
|
+
`CanonicalizeError`). **`sha256Hex(input)`** and **`canonicalDigest(value)`**
|
|
479
|
+
(= `sha256Hex(canonicalJson(value))`) — integrity checksums, not tamper-proof
|
|
480
|
+
seals. Uses `node:crypto` directly (same precedent as `/otp`).
|
|
481
|
+
|
|
482
|
+
### Added — `/event-infra`: `propagateHandlerErrors` option
|
|
483
|
+
|
|
484
|
+
- **`InProcessEventBusOptions.propagateHandlerErrors?: boolean`** (default
|
|
485
|
+
`false` — existing error-isolating behavior unchanged). When `true`, `publish`
|
|
486
|
+
runs handlers sequentially and rethrows the first handler error to the
|
|
487
|
+
publisher — for single-consumer projection/relay lanes where a failed handler
|
|
488
|
+
must not be silently acked. `publishMany` maps each event's failure into its
|
|
489
|
+
`PublishManyResult` entry instead of rejecting the batch.
|
|
490
|
+
|
|
491
|
+
## [0.13.0] - 2026-07-22
|
|
492
|
+
|
|
493
|
+
### Added — `/unit-cost-rate`: exact per-unit monetary rates (bigint-safe)
|
|
494
|
+
|
|
495
|
+
- **`@classytic/primitives/unit-cost-rate`** — fractional per-unit monetary
|
|
496
|
+
rates stored as scaled integers (`scaledAmount = minorPerUnit × RATE_SCALE`,
|
|
497
|
+
`RATE_SCALE = 1_000_000`). Solves the WAC / FIFO valuation problem where
|
|
498
|
+
dividing a total by a fractional quantity yields a sub-minor-unit rate
|
|
499
|
+
(100 paisa ÷ 3 units = 33.333… paisa/unit) that must not lose precision.
|
|
500
|
+
Exports:
|
|
501
|
+
- `unitCostRate(minorPerUnit, currency)` — direct rate constructor
|
|
502
|
+
- `unitCostRateFromTotal(totalMinor, quantity, currency)` — WAC/FIFO
|
|
503
|
+
constructor; bigint-safe, QTY_SCALE grid for fractional quantities
|
|
504
|
+
- `extendedAmount(rate, qty, mode?)` — the ONE rate→total rounding boundary;
|
|
505
|
+
bigint multiplication to keep large amounts exact; default `'half-even'`
|
|
506
|
+
(banker's rounding), `'half-up'` available for legacy parity
|
|
507
|
+
- `rateMinorPerUnit(rate)` — display helper (fractional number, NOT for math)
|
|
508
|
+
- `isUnitCostRate(value)` — structural type guard
|
|
509
|
+
- `UnitCostRate<C>` interface, `UnitCostRateError`, `UnitCostRateErrorCode`,
|
|
510
|
+
`RoundingMode`, `RATE_SCALE`
|
|
511
|
+
- For distributing a top-down charge across lines with no lost minor unit,
|
|
512
|
+
compose with `allocate` from `@classytic/primitives/split-allocation`.
|
|
513
|
+
|
|
514
|
+
### Changed — `/outbox` is now the canonical contract owner
|
|
515
|
+
|
|
516
|
+
- **`@classytic/primitives/outbox` owns the transactional-outbox contract.**
|
|
517
|
+
Previously this file was documented as a mirror of `@classytic/arc`'s copy
|
|
518
|
+
("arc is source of truth, keep bit-identical") while arc's docs said the
|
|
519
|
+
opposite — a manual-sync drift risk. Settled: primitives owns pure
|
|
520
|
+
cross-package contracts (events + outbox); arc owns runtime behavior
|
|
521
|
+
(`EventOutbox` relay, stores, backoff, Fastify integration) and, from
|
|
522
|
+
arc 2.24+, imports + re-exports this contract instead of duplicating it.
|
|
523
|
+
- **Cross-package `instanceof` now works.** Because arc re-exports these exact
|
|
524
|
+
`OutboxOwnershipError` / `InvalidOutboxEventError` classes, a store built
|
|
525
|
+
against primitives throws the same class identity arc's relay catches.
|
|
526
|
+
Under the old duplication, arc's `instanceof` checks silently missed
|
|
527
|
+
primitives-thrown ownership errors.
|
|
528
|
+
- No export names or signatures changed — contract surface is identical.
|
|
529
|
+
|
|
530
|
+
## [0.11.0] - 2026-07-08
|
|
531
|
+
|
|
532
|
+
### Added — eKYC vocabulary + secure OTP primitive
|
|
533
|
+
|
|
534
|
+
- **`@classytic/primitives/identity-verification`** — cross-package eKYC data shapes:
|
|
535
|
+
`IdentityDocumentType`, `VerificationCheck`, `VerificationLevel`, `SubjectKind`,
|
|
536
|
+
`IdentitySubject`, `DocumentReference`, `IdentityProviderCapabilities`,
|
|
537
|
+
`VerificationStatus`, `CheckResult`, `CreateVerificationParams`,
|
|
538
|
+
`VerificationSession`, `VerificationWebhookEvent`. Pure helpers: `isVerified`,
|
|
539
|
+
`checksSatisfy`, `deriveStatus`. The identity twin of `payment-gateway` — shapes
|
|
540
|
+
here, contracts in engines, provider implementations anywhere. Serves `@classytic/esign`
|
|
541
|
+
(IdentityBridge port), `@classytic/payee` (ComplianceBridge), and PSP merchant KYC.
|
|
542
|
+
- **`@classytic/primitives/otp`** — secure OTP challenge lifecycle. Guarantees: code
|
|
543
|
+
never persisted (only HMAC-SHA256 digest stored), constant-time `timingSafeEqual`
|
|
544
|
+
comparison, rejection-free `randomInt` CSPRNG (no modulo bias), bounded attempt
|
|
545
|
+
counter, TTL expiry, single-use verified state. Exports: `createOtpChallenge`,
|
|
546
|
+
`verifyOtpChallenge`, `isOtpVerified`, `isOtpActive`. Delivery stays in
|
|
547
|
+
`@classytic/notifications` — this primitive owns only the verify half.
|
|
548
|
+
|
|
549
|
+
## [0.9.1] - 2026-07-04
|
|
550
|
+
|
|
551
|
+
### Added
|
|
552
|
+
|
|
553
|
+
- **`CURRENCY_PATTERN` exported from `/currency`** — the ISO 4217 regex
|
|
554
|
+
(`/^[A-Z]{3}$/`) behind `isCurrencyCode` / `toCurrencyCode`. Consumers
|
|
555
|
+
embed the SAME pattern in JSON-Schema-representable validators (zod
|
|
556
|
+
`.regex`, Mongoose `match`, OpenAPI `pattern`) instead of hand-rolling
|
|
557
|
+
copies — `.refine(isCurrencyCode)` validates at runtime but is dropped
|
|
558
|
+
by `z.toJSONSchema`, silently losing the constraint from generated API
|
|
559
|
+
docs.
|
|
560
|
+
|
|
561
|
+
## [0.9.0] - 2026-07-02
|
|
562
|
+
|
|
563
|
+
### Added — `/timezone`: IANA resolution + civil dates (the door for `/calendar`'s escape hatch)
|
|
564
|
+
|
|
565
|
+
`/calendar` owns wall-clock ARITHMETIC over a fixed `UtcOffsetMinutes` and
|
|
566
|
+
tells hosts to "resolve the offset for the instant and pass it in" — but the
|
|
567
|
+
stack had no shared resolver, so consumers hand-rolled `Intl.DateTimeFormat`
|
|
568
|
+
(three divergent copies found: catalog ×2, be-prod POS). `/timezone` is that
|
|
569
|
+
resolver — zero-dep (Intl IS the ICU tz database), Temporal-shaped names for
|
|
570
|
+
a future mechanical migration:
|
|
571
|
+
|
|
572
|
+
- **`zoneOffsetMinutes(instant, zone)`** — per-instant, DST-exact offset;
|
|
573
|
+
composes directly with `/calendar`: `startOfMonth(i, zoneOffsetMinutes(i, z))`.
|
|
574
|
+
- **`localTimeParts(instant, zone)`** — `{ isoWeekday, hour, minute,
|
|
575
|
+
minutesOfDay }` in the zone; what pricing windows / slot grids /
|
|
576
|
+
working-hours checks evaluate against (canonicalizes catalog's resolvers).
|
|
577
|
+
- **`CivilDate`** (branded `'YYYY-MM-DD'`) + `civilDate` / `isCivilDate` /
|
|
578
|
+
`civilDateOf(instant, zone)` / `civilDateToInstant(cd, zone, time?)`
|
|
579
|
+
(two-pass DST-safe) / `addCivilDays` / `civilDaysBetween` — the type for
|
|
580
|
+
hotel nights, POS business dates, VAT filing years. Lexicographic ==
|
|
581
|
+
chronological, so it's a range-queryable Mongo string key.
|
|
582
|
+
- **`isValidTimeZone(zone)`** — boot-time host-config validation;
|
|
583
|
+
**`listTimeZones()`** + **`zoneOffsetLabel(instant, zone)`** — the data
|
|
584
|
+
source + label for Google-Calendar-style zone pickers, straight from ICU
|
|
585
|
+
(`Intl.supportedValuesOf`), no bundled list to go stale.
|
|
586
|
+
- `TimeZoneError`; formatter instances cached per zone.
|
|
587
|
+
|
|
588
|
+
No arithmetic here (that stays in `/calendar`) — resolution only.
|
|
589
|
+
|
|
590
|
+
## [0.7.2] - 2026-05-30
|
|
591
|
+
|
|
592
|
+
### Added — `BankTransaction` per-account routing + pending lifecycle fields
|
|
593
|
+
|
|
594
|
+
Three additive optional fields on `BankTransaction` (`/bank-transaction`):
|
|
595
|
+
|
|
596
|
+
- **`sourceAccountId?: string`** — the vendor-side account this row belongs to
|
|
597
|
+
(Plaid `account_id`, Xero `BankAccount.AccountID`). Sync feeds return
|
|
598
|
+
transactions spanning multiple accounts in one batch; this lets consumers
|
|
599
|
+
route each row to the right account per-transaction (multi-account sync)
|
|
600
|
+
instead of forcing the whole batch onto one account. Statement formats
|
|
601
|
+
(single-account files) leave it undefined — the account lives on
|
|
602
|
+
`BankStatement.account`.
|
|
603
|
+
- **`pending?: boolean`** — the entry hasn't posted/cleared yet (real-time
|
|
604
|
+
feeds like Plaid `pending: true`; statement formats leave it undefined).
|
|
605
|
+
Consumers must treat pending rows as provisional — don't reconcile/post
|
|
606
|
+
them as final.
|
|
607
|
+
- **`supersedesExternalId?: string`** — set on a POSTED row that replaces an
|
|
608
|
+
earlier PENDING one, carrying the pending row's `externalId` (Plaid
|
|
609
|
+
`pending_transaction_id`). Pending and posted rows have different
|
|
610
|
+
`externalId`s, so consumers MUST use this back-pointer to drop/replace the
|
|
611
|
+
superseded pending row on ingest — otherwise both persist as duplicates.
|
|
612
|
+
|
|
613
|
+
Backward-compatible (both optional). Closes a fidelity gap where Plaid's
|
|
614
|
+
pending state was dropped at the canonical boundary, so downstream consumers
|
|
615
|
+
(`@classytic/fin-io` Plaid mapper, `@classytic/revenue` import) couldn't
|
|
616
|
+
distinguish provisional rows or dedupe pending→posted.
|
|
617
|
+
|
|
618
|
+
## [0.7.1] - 2026-05-26
|
|
619
|
+
|
|
620
|
+
### Added — workflow + scheduling + composition primitives
|
|
621
|
+
|
|
622
|
+
Four new flat subpaths (additive):
|
|
623
|
+
|
|
624
|
+
- **`/status-history`** — append-only status transition log primitive.
|
|
625
|
+
- **`/condition`** — declarative condition expressions for workflow gates.
|
|
626
|
+
- **`/mixin`** — composition helpers for primitive object assembly.
|
|
627
|
+
- **`/sla-policy`** — SLA policy definitions paired with the existing `/sla` runtime.
|
|
628
|
+
|
|
629
|
+
### Added — payment event coverage
|
|
630
|
+
|
|
631
|
+
Expanded `PAYMENT_EVENT_TYPE` catalogue to cover auth/capture and dispute
|
|
632
|
+
lifecycles: `AUTHORIZED`, `CAPTURED`, `AUTH_VOIDED`, `DISPUTED`,
|
|
633
|
+
`DISPUTE_WON`, `DISPUTE_LOST`, `SETTLED`.
|
|
634
|
+
|
|
635
|
+
`PaymentRefundedPayload` and `PaymentReversedPayload` now carry
|
|
636
|
+
`originalAmount` (and `reversedAmount` / `isPartial` on reversed) so
|
|
637
|
+
consumers can detect partial vs full operations without a follow-up
|
|
638
|
+
Payment lookup.
|
|
639
|
+
|
|
640
|
+
## [0.7.0] - 2026-05-26
|
|
641
|
+
|
|
642
|
+
### Added — payment domain primitives
|
|
643
|
+
|
|
644
|
+
Three new subpaths shared across every payment-aware package
|
|
645
|
+
(`@classytic/invoice`, `@classytic/revenue`, `@classytic/order`, future
|
|
646
|
+
POS / AP / subscription packages). Each ships under a flat subpath so
|
|
647
|
+
consumers tree-shake what they don't import.
|
|
648
|
+
|
|
649
|
+
- **`/payment-method-kind`** — `PaymentMethodKind` universal vocabulary
|
|
650
|
+
(kind vs host-registered `methodCode`) so packages can speak `kind`
|
|
651
|
+
while hosts speak `code`.
|
|
652
|
+
- **`/payment-allocation-status`** — orthogonal allocation +
|
|
653
|
+
reconciliation status enums for payment legs (`unallocated`,
|
|
654
|
+
`partial`, `allocated`, `over_allocated`, plus reconciliation states).
|
|
655
|
+
- **`/payment-events`** — pure TS payload contracts for payment domain
|
|
656
|
+
events. Wire-only (no Zod, no runtime). Anchored on `Money` +
|
|
657
|
+
`PaymentMethodKind`; causation/correlation stay on `EventMeta`.
|
|
658
|
+
|
|
659
|
+
No breaking changes.
|
|
660
|
+
|
|
661
|
+
## [0.6.0] - 2026-05-17
|
|
662
|
+
|
|
663
|
+
### Added — five new primitives for CRM / workflow domains
|
|
664
|
+
|
|
665
|
+
Five new primitives covering gaps the existing surface didn't address.
|
|
666
|
+
Each ships under its own flat subpath import (no breaking changes).
|
|
667
|
+
|
|
668
|
+
- **`/phone`** — `PhoneNumber` value object + `parsePhone(input)` that
|
|
669
|
+
normalizes free-form input to E.164, captures the country calling code
|
|
670
|
+
as a discrete field, and reports failures via `Result<PhoneNumber,
|
|
671
|
+
PhoneError>`. Zero external deps; ITU-T E.164 prefixes inlined. Hosts
|
|
672
|
+
that want region-of-issue / line-type detection layer on top with
|
|
673
|
+
libphonenumber-js.
|
|
674
|
+
|
|
675
|
+
- **`/status-history`** — Generic, immutable status-change log:
|
|
676
|
+
`StatusChangeEntry<TStatus>`, `appendStatus()`, `timeInStatus()`,
|
|
677
|
+
`lastTransitionTo()`. Captures `durationInPriorMs` at write time so
|
|
678
|
+
funnel-velocity dashboards ("avg time in Qualification") work without
|
|
679
|
+
re-walking timestamps. Pairs naturally with `/state-machine`.
|
|
680
|
+
|
|
681
|
+
- **`/condition`** — Tiny JSON-serializable predicate DSL: `FieldCondition`
|
|
682
|
+
(`{ field, op, value }`) composed via `AllCondition` / `AnyCondition`
|
|
683
|
+
/ `NotCondition`, plus a total `evaluate(condition, target)` and a
|
|
684
|
+
`validateCondition()` for boot-time schema checks. Powers SLA scope
|
|
685
|
+
filters, drip "if opened/clicked" branches, automation triggers, and
|
|
686
|
+
permission row-scopes — anywhere a predicate needs to round-trip
|
|
687
|
+
through a database.
|
|
688
|
+
|
|
689
|
+
- **`/mixin`** — Additive type composition without subclassing:
|
|
690
|
+
`withMixin<T, K, M>()` / `getMixin()` / `hasMixin()` / `withoutMixin()`.
|
|
691
|
+
Lets a `Contact` carry per-domain payloads (`mixins.customer`,
|
|
692
|
+
`mixins.lead-fit`) on a typed `mixins` namespace without bloating the
|
|
693
|
+
base type. Plain object composition — no decorators, no global
|
|
694
|
+
hierarchy, no proxies.
|
|
695
|
+
|
|
696
|
+
- **`/sla-policy`** — Higher-order SLA layered on the existing `/sla`:
|
|
697
|
+
priority matrix (`urgent` / `high` / `normal` → durations), first-
|
|
698
|
+
response vs rolling-response semantics, and working-hours window
|
|
699
|
+
(weekdays + start/end minute + holidays). `defineSLAPolicy()` validates
|
|
700
|
+
at boot; `evaluateSLAStatus()` produces `{ kind, responseBy,
|
|
701
|
+
remainingMs, breached }` from a policy + per-entity inputs. The
|
|
702
|
+
existing simple `/sla` interface is unchanged — `sla-policy` derives
|
|
703
|
+
concrete `SLA` instances from a policy + priority and delegates to it.
|
|
704
|
+
|
|
705
|
+
### Changed — source-tree reorganization (no public-surface impact)
|
|
706
|
+
|
|
707
|
+
The 24 primitive source files moved from a flat `src/*.ts` layout into
|
|
708
|
+
six category folders so the package is self-documenting at the file
|
|
709
|
+
system level:
|
|
710
|
+
|
|
711
|
+
```
|
|
712
|
+
src/
|
|
713
|
+
money/ money, currency, split-allocation, bank-transaction, payment-gateway
|
|
714
|
+
identity/ person, address, phone
|
|
715
|
+
scheduling/ period, cadence, sla, sla-policy
|
|
716
|
+
workflow/ state-machine, status-history, approval, hold, condition
|
|
717
|
+
events/ events, outbox
|
|
718
|
+
composition/ mixin, reference, context, brand, result
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
**Public subpath imports are unchanged** — `@classytic/primitives/money`,
|
|
722
|
+
`@classytic/primitives/sla`, etc. still resolve. tsdown emits each entry
|
|
723
|
+
to `dist/<name>.mjs` regardless of source nesting. Existing consumers
|
|
724
|
+
upgrade with `npm install @classytic/primitives@^0.6.0` and a no-op
|
|
725
|
+
diff.
|
|
726
|
+
|
|
727
|
+
### Dependencies
|
|
728
|
+
|
|
729
|
+
Still zero runtime deps. Node 22+.
|
|
730
|
+
|
|
731
|
+
## [0.5.0] - 2026-05-05
|
|
732
|
+
|
|
733
|
+
### Added — `/bank-transaction` subpath
|
|
734
|
+
|
|
735
|
+
Canonical bank-feed shapes shared across `@classytic/fin-io` (parsers
|
|
736
|
+
produce these), `@classytic/revenue` (`transactionRepository.import()`
|
|
737
|
+
consumes them), and `@classytic/ledger` (`bankStatementMapper`
|
|
738
|
+
consumes them). Single source of truth for the cross-package
|
|
739
|
+
vocabulary; eliminates the structural mirrors fin-io and revenue
|
|
740
|
+
were carrying.
|
|
741
|
+
|
|
742
|
+
Exports: `BankTransaction`, `BankCounterparty`, `BankAccount`,
|
|
743
|
+
`BankStatement`, `BankStatementSource`, `BankImportReport`,
|
|
744
|
+
`BankImportRowError`. All anchored on `Money` from `/money` (integer
|
|
745
|
+
minor units, `number`).
|
|
746
|
+
|
|
747
|
+
### Added — `/payment-gateway` subpath
|
|
748
|
+
|
|
749
|
+
Canonical payment-gateway data shapes. **Lets any provider package
|
|
750
|
+
(Stripe, Razorpay, SSLCommerz, PayPal, bKash, Nagad, manual, …)
|
|
751
|
+
implement against primitives' types without depending on revenue's
|
|
752
|
+
heavyweight runtime.** Provider authors peer-dep on
|
|
753
|
+
`@classytic/primitives` only.
|
|
754
|
+
|
|
755
|
+
Exports: `CreateIntentParams`, `PaymentIntent`, `PaymentResult`,
|
|
756
|
+
`RefundResult`, `WebhookEvent`, `ProviderCapabilities`. All `amount`
|
|
757
|
+
fields are `Money` from `/money` (no flat `amount: number, currency:
|
|
758
|
+
string` pairs — currency travels inside Money).
|
|
759
|
+
|
|
760
|
+
The `PaymentProvider` abstract class — the *contract* every provider
|
|
761
|
+
must satisfy — stays in `@classytic/revenue`. Revenue is the data
|
|
762
|
+
engine that consumes providers; provider packages live elsewhere.
|
|
763
|
+
|
|
764
|
+
### Architectural intent
|
|
765
|
+
|
|
766
|
+
Revenue 3.0 + primitives 0.5 + fin-io 0.3 establish a clean
|
|
767
|
+
three-tier seam:
|
|
768
|
+
|
|
769
|
+
1. **primitives** — pure data shapes (no runtime, no deps). Owns
|
|
770
|
+
`Money`, `BankTransaction`, `PaymentIntent`, every cross-
|
|
771
|
+
package vocabulary type.
|
|
772
|
+
2. **revenue** — the engine. Owns the abstract contracts
|
|
773
|
+
(`PaymentProvider`, `BankFeedProvider`), state machines, repos.
|
|
774
|
+
Consumes primitives shapes.
|
|
775
|
+
3. **provider packages** (`revenue-stripe`, `revenue-razorpay`,
|
|
776
|
+
`@classytic/fin-io/adapters/revenue`, …) — implementations.
|
|
777
|
+
Peer-dep on primitives. Optional peer-dep on revenue for the
|
|
778
|
+
abstract base class.
|
|
779
|
+
|
|
780
|
+
This eliminates the cycle where every provider package transitively
|
|
781
|
+
pulled mongoose + mongokit just to know what a `PaymentIntent`
|
|
782
|
+
looked like. Tree-shaking, build times, and dep-graph honesty all
|
|
783
|
+
improve.
|
|
784
|
+
|
|
785
|
+
### Migration
|
|
786
|
+
|
|
787
|
+
Consumers MUST import from primitives subpaths. No re-exports from
|
|
788
|
+
revenue or fin-io (PACKAGE_RULES P2):
|
|
789
|
+
|
|
790
|
+
```ts
|
|
791
|
+
// Before
|
|
792
|
+
import type { PaymentIntent, CreateIntentParams } from '@classytic/revenue';
|
|
793
|
+
import type { CanonicalTransaction, Money } from '@classytic/fin-io';
|
|
794
|
+
|
|
795
|
+
// After
|
|
796
|
+
import type {
|
|
797
|
+
PaymentIntent,
|
|
798
|
+
CreateIntentParams,
|
|
799
|
+
} from '@classytic/primitives/payment-gateway';
|
|
800
|
+
import type { BankTransaction } from '@classytic/primitives/bank-transaction';
|
|
801
|
+
import type { Money } from '@classytic/primitives/money';
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
Provider helper classes (`PaymentIntent`, `PaymentResult`,
|
|
805
|
+
`RefundResult`, `WebhookEvent`) are deleted from revenue — they were
|
|
806
|
+
trivial wrappers around plain objects. Provider implementations now
|
|
807
|
+
return plain object literals matching the interface.
|
|
808
|
+
|
|
809
|
+
`CreateIntentParams.amount` is now `Money` (`{ amount: number,
|
|
810
|
+
currency: string }`), not the flat `amount + currency` pair. Revenue's
|
|
811
|
+
repository wraps internally:
|
|
812
|
+
|
|
813
|
+
```ts
|
|
814
|
+
const intent = await provider.createIntent({
|
|
815
|
+
amount: { amount: params.amount, currency },
|
|
816
|
+
metadata: params.metadata,
|
|
817
|
+
...params.paymentData,
|
|
818
|
+
});
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
## [0.3.1] - 2026-05-02
|
|
822
|
+
|
|
823
|
+
### Added — state-machine ↔ mongokit/sqlitekit integration
|
|
824
|
+
|
|
825
|
+
State-machine primitives now compose directly with the kit-level CAS surface shipped in mongokit 3.13 and the sqlitekit follow-up. Three additions:
|
|
826
|
+
|
|
827
|
+
- **`StateMachine.validTargets(from)`** — every status the aggregate can move TO from `from`. Useful for UI dropdowns ("which actions are legal right now?") and for fanning a single source state into a multi-source CAS via a repo's `claim({ from: machine.validTargets(current), to })`.
|
|
828
|
+
- **`StateMachine.validSources(to)`** — every status that can transition INTO `to` (reverse adjacency). Pair with a repo's multi-source `claim({ from: machine.validSources(to), to })` when the target is fixed but the caller doesn't yet know the current state — the canonical "cancel from any non-terminal" / "error from any in-flight" pattern. Pre-computed at definition time, O(1) lookup, frozen arrays safe to expose by reference.
|
|
829
|
+
- **`assertAndClaim(machine, repo, id, args)`** — pairs `machine.assertTransition` (sync domain check) with `repo.claim()` (atomic CAS) in one call. The canonical state-machine-backed CAS pattern. Skipping either layer leaves a hole: skip `assertTransition` and bad transitions reach storage; skip `claim` and concurrent writers race.
|
|
830
|
+
|
|
831
|
+
```ts
|
|
832
|
+
import { defineStateMachine, assertAndClaim } from '@classytic/primitives/state-machine';
|
|
833
|
+
|
|
834
|
+
const ORDER_MACHINE = defineStateMachine<OrderStatus>({
|
|
835
|
+
name: 'Order',
|
|
836
|
+
transitions: {
|
|
837
|
+
draft: ['approved', 'cancelled'],
|
|
838
|
+
approved: ['shipped', 'cancelled'],
|
|
839
|
+
shipped: [],
|
|
840
|
+
cancelled: [],
|
|
841
|
+
},
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
// Single-source:
|
|
845
|
+
const updated = await assertAndClaim(ORDER_MACHINE, orderRepo, orderId, {
|
|
846
|
+
from: 'draft',
|
|
847
|
+
to: 'approved',
|
|
848
|
+
patch: { approvedAt: new Date() },
|
|
849
|
+
options: { organizationId: ctx.organizationId, session },
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
// Multi-source via reverse adjacency — "cancel from any legal predecessor":
|
|
853
|
+
await assertAndClaim(ORDER_MACHINE, orderRepo, orderId, {
|
|
854
|
+
from: ORDER_MACHINE.validSources('cancelled'), // ['draft', 'approved']
|
|
855
|
+
to: 'cancelled',
|
|
856
|
+
patch: { cancelledAt: new Date() },
|
|
857
|
+
options: { organizationId: ctx.organizationId },
|
|
858
|
+
});
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
#### `ClaimableRepo<TDoc>` — structural dependency on the kit
|
|
862
|
+
|
|
863
|
+
`assertAndClaim` accepts any object matching `ClaimableRepo<TDoc>` — a structural type that mirrors the mongokit 3.13 `Repository.claim()` signature. Primitives doesn't take a hard peer dep on mongokit; consumers wire the kit they want. Sqlitekit's `claim()` matches the same shape (verified by mongokit/sqlitekit cross-conformance).
|
|
864
|
+
|
|
865
|
+
```ts
|
|
866
|
+
export interface ClaimableRepo<TDoc> {
|
|
867
|
+
claim(
|
|
868
|
+
id: string,
|
|
869
|
+
transition: {
|
|
870
|
+
field?: string;
|
|
871
|
+
from: unknown | readonly unknown[];
|
|
872
|
+
to: unknown;
|
|
873
|
+
where?: Record<string, unknown>;
|
|
874
|
+
},
|
|
875
|
+
patch?: Record<string, unknown>,
|
|
876
|
+
options?: Record<string, unknown>,
|
|
877
|
+
): Promise<TDoc | null>;
|
|
878
|
+
}
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
#### Why a 0.3.1 (not 0.4.0)
|
|
882
|
+
|
|
883
|
+
Purely additive — no removals, no signature changes. `defineStateMachine` callers from 0.3.0 see two new methods on the returned `StateMachine` (`validTargets`, `validSources`) and one new free function (`assertAndClaim`). Backwards-compatible.
|
|
884
|
+
|
|
885
|
+
## [0.3.0] - 2026-04-29
|
|
886
|
+
|
|
887
|
+
### Removed — `/errors` subpath (BREAKING)
|
|
888
|
+
|
|
889
|
+
- Deleted `@classytic/primitives/errors` (`ErrorContract`, `ErrorDetail`, `ErrorCode`, `ERROR_CODES`).
|
|
890
|
+
- Errors are infrastructure-shaped (HTTP-coupled, repository-coupled, RFC 7807-shaped). Moved to `@classytic/repo-core/errors` where they sit next to `pagination`, `tenant`, `cache`, `hooks`, `schema` — every other repository contract — and join the existing `HttpError` throwable contract there. Single canonical home for both the wire (`ErrorContract`) and throwable (`HttpError`) shapes.
|
|
891
|
+
|
|
892
|
+
#### Migration
|
|
893
|
+
|
|
894
|
+
```diff
|
|
895
|
+
- import type { ErrorContract, ErrorDetail, ErrorCode } from '@classytic/primitives/errors';
|
|
896
|
+
- import { ERROR_CODES } from '@classytic/primitives/errors';
|
|
897
|
+
+ import type { ErrorContract, ErrorDetail, ErrorCode } from '@classytic/repo-core/errors';
|
|
898
|
+
+ import { ERROR_CODES } from '@classytic/repo-core/errors';
|
|
899
|
+
```
|
|
900
|
+
|
|
901
|
+
Field shapes and code values are unchanged — pure relocation.
|
|
902
|
+
|
|
903
|
+
### Removed — `/tenant` subpath (BREAKING)
|
|
904
|
+
|
|
905
|
+
- Deleted `@classytic/primitives/tenant` (`TenantConfig`, `TenantStrategy`, `TenantFieldType`, `DEFAULT_TENANT_CONFIG`, `resolveTenantConfig`, `ResolvedTenantConfig`).
|
|
906
|
+
- Tenant scope is **infrastructure-shaped** (describes how queries get scoped to a database / repository), not a domain primitive like Money or Address. Moved to `@classytic/repo-core/tenant` where it sits next to `context`, `filter`, `hooks`, `schema`, `cache` — every other repository contract.
|
|
907
|
+
- Mongokit and sqlitekit already peer-dep `@classytic/repo-core`, so consuming `TenantConfig` from there costs zero extra peer deps for those kits.
|
|
908
|
+
|
|
909
|
+
#### Migration
|
|
910
|
+
|
|
911
|
+
```diff
|
|
912
|
+
- import type { TenantConfig } from '@classytic/primitives/tenant';
|
|
913
|
+
- import { resolveTenantConfig, DEFAULT_TENANT_CONFIG } from '@classytic/primitives/tenant';
|
|
914
|
+
+ import type { TenantConfig } from '@classytic/repo-core/tenant';
|
|
915
|
+
+ import { resolveTenantConfig, DEFAULT_TENANT_CONFIG } from '@classytic/repo-core/tenant';
|
|
916
|
+
```
|
|
917
|
+
|
|
918
|
+
Field shapes, runtime semantics, and defaults are unchanged. The custom-strategy escape hatch (`strategy: 'custom'` + `resolve: (ctx) => filterShape`) works identically.
|
|
919
|
+
|
|
920
|
+
### Removed — `/pagination` subpath (BREAKING)
|
|
921
|
+
|
|
922
|
+
- Deleted `@classytic/primitives/pagination` (`PageParams`, `KeysetParams`, `SortSpec`, `SortDirection`, `OffsetPage<T>`, `KeysetPage<T>`, `AggregatePage<T>`, `emptyOffsetPage`, `emptyKeysetPage`).
|
|
923
|
+
- Pagination shapes are owned by **`@classytic/repo-core/pagination`** — that package ships both the types AND the cursor encoding/skip-math/keyset-validation algorithms. Primitives' duplicate type-only declarations existed before the contract was canonised in repo-core; keeping them invited drift.
|
|
924
|
+
|
|
925
|
+
### Migration
|
|
926
|
+
|
|
927
|
+
```diff
|
|
928
|
+
- import type { OffsetPage, KeysetPage, AggregatePage, SortSpec } from '@classytic/primitives/pagination';
|
|
929
|
+
+ import type {
|
|
930
|
+
+ OffsetPaginationResult,
|
|
931
|
+
+ KeysetPaginationResult,
|
|
932
|
+
+ AggregatePaginationResult,
|
|
933
|
+
+ SortSpec,
|
|
934
|
+
+ } from '@classytic/repo-core/pagination';
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
Note: shape names also changed (`OffsetPage` → `OffsetPaginationResult`, etc.) to match repo-core's naming. Field names align with what mongokit, sqlitekit, and arc already produced — those packages always returned repo-core shapes; primitives' types were a parallel, never-quite-aligned vocabulary.
|
|
938
|
+
|
|
939
|
+
If you also need HTTP wire envelopes (`{ success: true } & Result`), repo-core 0.3.0 ships `OffsetPaginationResponse`, `KeysetPaginationResponse`, `AggregatePaginationResponse`, `BareListResponse`, `PaginatedResponse`, plus a `toCanonicalList()` runtime normalizer.
|
|
940
|
+
|
|
941
|
+
### Why now (and why this is the only breaking change in this release)
|
|
942
|
+
|
|
943
|
+
Two layers of pagination shapes drifted across the org: primitives declared one set, repo-core declared a different set, mongokit + sqlitekit + arc all consumed repo-core's. Removing primitives' set is purely subtractive — nobody internally imports from `@classytic/primitives/pagination`, and external consumers get a clear migration path to repo-core. No new ground to break later.
|
|
944
|
+
|
|
945
|
+
## [0.2.0]
|
|
946
|
+
|
|
947
|
+
### Added
|
|
948
|
+
|
|
949
|
+
- **`/state-machine` subpath** — `defineStateMachine<TStatus>({ name, transitions, errorFactory? })`
|
|
950
|
+
declarative state-transition primitive. Replaces hand-rolled
|
|
951
|
+
`if (status === 'X') throw` blocks across kernels:
|
|
952
|
+
|
|
953
|
+
```typescript
|
|
954
|
+
import { defineStateMachine } from '@classytic/primitives/state-machine';
|
|
955
|
+
|
|
956
|
+
const ORDER_MACHINE = defineStateMachine<OrderStatus>({
|
|
957
|
+
name: 'Order',
|
|
958
|
+
transitions: {
|
|
959
|
+
draft: ['approved', 'cancelled'],
|
|
960
|
+
approved: ['shipped', 'cancelled'],
|
|
961
|
+
shipped: [],
|
|
962
|
+
cancelled: [],
|
|
963
|
+
},
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
ORDER_MACHINE.assertTransition('order-1', 'draft', 'approved'); // OK
|
|
967
|
+
ORDER_MACHINE.assertTransition('order-1', 'shipped', 'draft'); // throws
|
|
968
|
+
ORDER_MACHINE.canTransition('draft', 'shipped'); // false
|
|
969
|
+
ORDER_MACHINE.isTerminal('shipped'); // true
|
|
970
|
+
```
|
|
971
|
+
|
|
972
|
+
Generic over the status string-union — `Record<TStatus, readonly TStatus[]>`
|
|
973
|
+
on `transitions` enforces exhaustiveness at the call site. Kernels with their
|
|
974
|
+
own typed transition error class (e.g. flow's `InvalidTransitionError`) wire
|
|
975
|
+
it via `errorFactory` so the thrown type stays domain-consistent. Standalone
|
|
976
|
+
callers get the bundled `IllegalTransitionError` (carries `entityType` /
|
|
977
|
+
`entityId` / `from` / `to` + `code: 'illegal_transition'` + `status: 422`).
|
|
978
|
+
|
|
979
|
+
First consumer: `@classytic/flow` `PROCUREMENT_MACHINE` (procurement-order
|
|
980
|
+
approve / cancel transitions).
|
|
981
|
+
|
|
982
|
+
## [0.1.1]
|
|
983
|
+
|
|
984
|
+
### Added
|
|
985
|
+
|
|
986
|
+
- **`MinorUnits<C>` branded type on `/money`** — wire-format monetary amount
|
|
987
|
+
(integer minor units, branded with phantom currency) for HTTP/JSON fields
|
|
988
|
+
where a full `Money` struct is overkill. Erases at runtime; `MinorUnits<'BDT'>`
|
|
989
|
+
vs `MinorUnits<'USD'>` collide at compile time. Pair with structured `Money`
|
|
990
|
+
for in-process arithmetic. Constructor: `MinorUnits(amount, currency?)`.
|
|
991
|
+
|
|
992
|
+
### Changed
|
|
993
|
+
|
|
994
|
+
- **`/tenant` `contextKey` cascade default** — `resolveTenantConfig({ tenantField: 'branchId' })`
|
|
995
|
+
now defaults `contextKey` to `'branchId'` instead of `'organizationId'`.
|
|
996
|
+
When a host renames `tenantField`, their `OperationContext` carries the id
|
|
997
|
+
under the same key in the overwhelming majority of cases; mirroring the
|
|
998
|
+
rename is the least-surprise default. Explicit `contextKey` still overrides
|
|
999
|
+
(set when the doc field and context key genuinely diverge). Existing call
|
|
1000
|
+
sites that supplied both `tenantField` and `contextKey` are unaffected.
|
|
1001
|
+
|
|
1002
|
+
## [0.1.0]
|
|
1003
|
+
|
|
1004
|
+
Initial release. Framework-agnostic TypeScript primitives shared across
|
|
1005
|
+
Classytic packages. Zero runtime dependencies. Node 22+. ESM only.
|
|
1006
|
+
260 tests (unit + integration) across 20 files.
|
|
1007
|
+
|
|
1008
|
+
All subpaths are importable independently; there is **no root barrel
|
|
1009
|
+
re-export** — consumers must use subpath imports (`@classytic/primitives/money`,
|
|
1010
|
+
`/events`, `/tenant`, …) to keep the Node ESM loader + TypeScript compiler
|
|
1011
|
+
from walking the full module graph on any convenience import.
|
|
1012
|
+
|
|
1013
|
+
### Subpaths
|
|
1014
|
+
|
|
1015
|
+
- **`/money`** — `Money` type + integer-minor-unit arithmetic (`money`,
|
|
1016
|
+
`addMoney`, `subtractMoney`, `multiplyMoney`, `sumMoney`, `negateMoney`,
|
|
1017
|
+
`absMoney`, `compareMoney`, `equalsMoney`, `isZeroMoney`, `isPositiveMoney`,
|
|
1018
|
+
`isNegativeMoney`, `isMoney`, `fromMajor`, `toMajor`); `CurrencyMismatchError`.
|
|
1019
|
+
- **`/currency`** — `CurrencyCode` brand; `CURRENCIES` table; `MINOR_UNIT_FACTOR`,
|
|
1020
|
+
`minorUnitFactor`, `isCurrencyCode`, `toCurrencyCode`.
|
|
1021
|
+
- **`/address`** — `Address`, `ContactAddress`, `GeoPoint`, `GeoJsonPoint`;
|
|
1022
|
+
`toGeoJsonPoint`, `fromGeoJsonPoint`.
|
|
1023
|
+
- **`/period`** — `DateRange`, `Period`; `isDateRange`, `isWithin`,
|
|
1024
|
+
`rangeDurationMs`.
|
|
1025
|
+
- **`/pagination`** — `PageParams`, `OffsetPage<T>`, `KeysetPage<T>`,
|
|
1026
|
+
`AggregatePage`, `KeysetParams`, `SortSpec`, `SortDirection`;
|
|
1027
|
+
`emptyOffsetPage`, `emptyKeysetPage`.
|
|
1028
|
+
- **`/reference`** — `ExternalRef`, `DocumentRef`, `ObjectIdLike`, `IdLike`;
|
|
1029
|
+
`idToString`, `isExternalRef`.
|
|
1030
|
+
- **`/context`** — `OperationContext` (the identity + tracing bag every
|
|
1031
|
+
Classytic package accepts); `ActorRef`.
|
|
1032
|
+
- **`/events`** — `EventMeta` (arc v2.9-aligned — 13 fields including
|
|
1033
|
+
`schemaVersion`, `causationId`, `partitionKey`, `source`, `idempotencyKey`,
|
|
1034
|
+
`aggregate`), `DomainEvent<T>`, `EventTransport`, `EventHandler`,
|
|
1035
|
+
`EventLogger`, `DeadLetteredEvent<T>`, `PublishManyResult`; `createEvent`,
|
|
1036
|
+
`createChildEvent` (auto-chains `causationId`, inherits `correlationId` /
|
|
1037
|
+
`userId` / `organizationId` / `source` / `idempotencyKey` — NOT
|
|
1038
|
+
`aggregate`), `matchEventPattern`.
|
|
1039
|
+
- **`/outbox`** — transactional outbox **contract** mirrored from arc v2.9:
|
|
1040
|
+
`OutboxStore`, `OutboxWriteOptions`, `OutboxClaimOptions`,
|
|
1041
|
+
`OutboxAcknowledgeOptions`, `OutboxFailOptions`, `OutboxErrorInfo`,
|
|
1042
|
+
`OutboxFailureContext`, `OutboxFailureDecision`, `OutboxFailurePolicy`;
|
|
1043
|
+
`OutboxOwnershipError`, `InvalidOutboxEventError`. **Contract only** —
|
|
1044
|
+
runtime (`EventOutbox`, `MemoryOutboxStore`, `MongoOutboxStore`,
|
|
1045
|
+
`exponentialBackoff`) ships in `@classytic/arc/events` and
|
|
1046
|
+
`@classytic/arc/events/mongo`. Domain packages implement `OutboxStore`
|
|
1047
|
+
without peer-depping arc.
|
|
1048
|
+
- **`/tenant`** — `TenantConfig` (with `strategy: 'field' | 'none' | 'custom'`
|
|
1049
|
+
and optional `resolve(ctx)` for custom strategy), `TenantFieldType`,
|
|
1050
|
+
`TenantStrategy`, `ResolvedTenantConfig`; `DEFAULT_TENANT_CONFIG`,
|
|
1051
|
+
`resolveTenantConfig`. Field names match `@classytic/mongokit`'s
|
|
1052
|
+
`MultiTenantOptions` so a resolved config threads into
|
|
1053
|
+
`multiTenantPlugin({ ... })` without translation.
|
|
1054
|
+
- **`/result`** — `Result<T, E>`; `ok`, `err`, `isOk`, `isErr`, `unwrap`,
|
|
1055
|
+
`mapResult`, `mapError`.
|
|
1056
|
+
- **`/brand`** — `Brand<T, B>`, `Prettify`, `DeepPartial`, `DeepReadonly`,
|
|
1057
|
+
`RequireKeys`, `OptionalKeys`, `NonEmptyArray`.
|
|
1058
|
+
- **`/errors`** — `ErrorContract`, `ErrorDetail`, `ErrorCode`, `ERROR_CODES`.
|
|
1059
|
+
- **`/split-allocation`** — `allocate(total, subjects, method)` returns a
|
|
1060
|
+
`SplitResult` whose parts sum exactly to `total` via the largest-remainder
|
|
1061
|
+
method. Methods: `equal`, `by-qty`, `by-weight`, `by-volume`, `by-value`,
|
|
1062
|
+
`by-percent`. Deterministic — identical inputs produce identical outputs;
|
|
1063
|
+
ties broken by input order. Handles negative totals by sign-flipping.
|
|
1064
|
+
Exports: `allocate`, `isBalanced`, `SplitAllocationError`, types
|
|
1065
|
+
`SplitMethod`, `SplitSubject`, `SplitPart`, `SplitResult`,
|
|
1066
|
+
`SplitAllocationErrorCode`.
|
|
1067
|
+
- **`/approval`** — `ApprovalChain` value object with pure FSM helpers.
|
|
1068
|
+
Supports sequential and parallel chains, per-step quorum
|
|
1069
|
+
(`requiredApprovals`), threshold-gated skips. Any step-level rejection
|
|
1070
|
+
rejects the chain; all steps `approved`|`skipped` approves it. Exports:
|
|
1071
|
+
`createChain`, `applyDecision`, `skipStep`, `nextPendingStep`,
|
|
1072
|
+
`pendingSteps`, `isApproved`, `isRejected`, `isPending`, `decisionCount`,
|
|
1073
|
+
`ApprovalError`, types `ApprovalChain`, `ApprovalStep`, `ApprovalDecision`,
|
|
1074
|
+
`Approver`, `ApprovalStatus`, `ChainOrder`, `ApprovalThreshold`,
|
|
1075
|
+
`CreateChainInput`, `CreateStepInput`, `DecisionInput`, `ApprovalErrorCode`.
|
|
1076
|
+
- **`/cadence`** — recurrence spec with pure UTC date arithmetic. Kinds:
|
|
1077
|
+
`daily`, `weekly`, `monthly`, `yearly`, `cron` (opaque — host parses).
|
|
1078
|
+
`dayOfMonth: 31` snaps to the last day of shorter months; Feb 29 snaps
|
|
1079
|
+
to Feb 28 in non-leap years. Exports: `nextOccurrence`,
|
|
1080
|
+
`occurrencesBetween`, `validateCadence`, `CadenceError`, types `Cadence`,
|
|
1081
|
+
`DailyCadence`, `WeeklyCadence`, `MonthlyCadence`, `YearlyCadence`,
|
|
1082
|
+
`CronCadence`, `CadenceKind`, `IsoWeekday`, `CadenceErrorCode`.
|
|
1083
|
+
- **`/hold`** — `HoldReason` + collection helpers. Host embeds
|
|
1084
|
+
`HoldReason[]` on documents. Exports: `addHold`, `resolveHold`,
|
|
1085
|
+
`activeHolds`, `resolvedHolds`, `isOnHold`, `hasActiveHoldOfCode`,
|
|
1086
|
+
`HoldError`, types `HoldReason`, `HoldActor`, `HoldActorKind`,
|
|
1087
|
+
`AddHoldInput`, `ResolveHoldInput`, `HoldErrorCode`.
|
|
1088
|
+
- **`/sla`** — target duration + breach-detection arithmetic. Policy is
|
|
1089
|
+
carried but not executed — host reads `breachPolicy` (`warn` | `escalate`
|
|
1090
|
+
| `block`) and acts. Exports: `breachedAt`, `remainingMs`, `elapsedMs`,
|
|
1091
|
+
`isBreached`, `consumedFraction`, `validateSLA`, `SLAError`, types
|
|
1092
|
+
`SLA`, `BreachPolicy`, `SLAErrorCode`.
|
|
1093
|
+
- **`/person`** — person/contact shapes shared across order, crm, hr.
|
|
1094
|
+
`PersonName`, `ContactInfo`, `EmergencyContact`, `Gender`; formatters
|
|
1095
|
+
`formatFullName`, `formatDisplayName`.
|
|
1096
|
+
|
|
1097
|
+
### Design principles
|
|
1098
|
+
|
|
1099
|
+
- Pure functions on plain data. No I/O, no Mongo, no events.
|
|
1100
|
+
- Immutable return values. Helpers return new collections; originals are never mutated.
|
|
1101
|
+
- Deterministic. Same inputs → same outputs. Clock sources injected
|
|
1102
|
+
explicitly so tests don't flake.
|
|
1103
|
+
- Errors are typed. Every module ships an `<Name>Error` class with a
|
|
1104
|
+
discriminated `code: <Name>ErrorCode` string union.
|
|
1105
|
+
- Subpath-imports only — no root barrel.
|
|
1106
|
+
|
|
1107
|
+
### Ownership direction
|
|
1108
|
+
|
|
1109
|
+
Arc is the source of truth for event types. Primitives **mirrors** arc
|
|
1110
|
+
(`@classytic/arc/events/EventTransport.ts`, `@classytic/arc/events/outbox.ts`)
|
|
1111
|
+
verbatim for `EventMeta`, `DomainEvent`, `EventTransport`,
|
|
1112
|
+
`DeadLetteredEvent`, `EventLogger`, `PublishManyResult`, and the full
|
|
1113
|
+
`OutboxStore` contract surface. Helpers (`createEvent`, `createChildEvent`)
|
|
1114
|
+
are allowed to be more defensive but match arc's signatures.
|
|
1115
|
+
|
|
1116
|
+
Packages that peer-dep `@classytic/primitives` can write
|
|
1117
|
+
`class FooOutboxRepository implements OutboxStore` without pulling arc into
|
|
1118
|
+
their dependency graph; hosts import `EventOutbox` + `MongoOutboxStore` from
|
|
1119
|
+
arc and wire them against package-provided stores.
|