@heavybit/shared-lib 2.1.0 → 2.7.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 (296) hide show
  1. package/README.md +163 -2
  2. package/dist/cjs/access/index.js +9 -1
  3. package/dist/cjs/access/index.js.map +1 -1
  4. package/dist/cjs/approvals/index.js +137 -0
  5. package/dist/cjs/approvals/index.js.map +1 -0
  6. package/dist/cjs/audit/index.js +74 -0
  7. package/dist/cjs/audit/index.js.map +1 -0
  8. package/dist/cjs/cache/index.js +90 -16
  9. package/dist/cjs/cache/index.js.map +1 -1
  10. package/dist/cjs/cache/keyspace.js +101 -0
  11. package/dist/cjs/cache/keyspace.js.map +1 -0
  12. package/dist/cjs/common/circuit-breaker.js +74 -0
  13. package/dist/cjs/common/circuit-breaker.js.map +1 -0
  14. package/dist/cjs/common/index.js +4 -0
  15. package/dist/cjs/common/index.js.map +1 -1
  16. package/dist/cjs/common/internal-client.js +177 -0
  17. package/dist/cjs/common/internal-client.js.map +1 -0
  18. package/dist/cjs/common/internal-constants.js +12 -0
  19. package/dist/cjs/common/internal-constants.js.map +1 -0
  20. package/dist/cjs/common/internal-service-auth.js +160 -0
  21. package/dist/cjs/common/internal-service-auth.js.map +1 -0
  22. package/dist/cjs/common/money.js +215 -0
  23. package/dist/cjs/common/money.js.map +1 -0
  24. package/dist/cjs/common/phone.js +41 -22
  25. package/dist/cjs/common/phone.js.map +1 -1
  26. package/dist/cjs/common/time.js +28 -2
  27. package/dist/cjs/common/time.js.map +1 -1
  28. package/dist/cjs/config/index.js +139 -19
  29. package/dist/cjs/config/index.js.map +1 -1
  30. package/dist/cjs/contracts/notification-dispatch.js +1 -1
  31. package/dist/cjs/database/db.js +37 -0
  32. package/dist/cjs/database/db.js.map +1 -0
  33. package/dist/cjs/database/index.js +22 -51
  34. package/dist/cjs/database/index.js.map +1 -1
  35. package/dist/cjs/database/pagination.js +88 -0
  36. package/dist/cjs/database/pagination.js.map +1 -0
  37. package/dist/cjs/database/pool.js +60 -0
  38. package/dist/cjs/database/pool.js.map +1 -0
  39. package/dist/cjs/database/scoped-repository.js +132 -0
  40. package/dist/cjs/database/scoped-repository.js.map +1 -0
  41. package/dist/cjs/database/scopes.js +166 -0
  42. package/dist/cjs/database/scopes.js.map +1 -0
  43. package/dist/cjs/database/table-names.js +14 -0
  44. package/dist/cjs/database/table-names.js.map +1 -0
  45. package/dist/cjs/database/tables.js +71 -0
  46. package/dist/cjs/database/tables.js.map +1 -0
  47. package/dist/cjs/express/auth.js +64 -50
  48. package/dist/cjs/express/auth.js.map +1 -1
  49. package/dist/cjs/express/index.js +2 -0
  50. package/dist/cjs/express/index.js.map +1 -1
  51. package/dist/cjs/express/internal-caller.js +52 -0
  52. package/dist/cjs/express/internal-caller.js.map +1 -0
  53. package/dist/cjs/express/step-up.js +41 -0
  54. package/dist/cjs/express/step-up.js.map +1 -0
  55. package/dist/cjs/http/index.js +3 -71
  56. package/dist/cjs/http/index.js.map +1 -1
  57. package/dist/cjs/identity/index.js +269 -67
  58. package/dist/cjs/identity/index.js.map +1 -1
  59. package/dist/cjs/identity/step-up.js +52 -0
  60. package/dist/cjs/identity/step-up.js.map +1 -0
  61. package/dist/cjs/index.js +6 -0
  62. package/dist/cjs/index.js.map +1 -1
  63. package/dist/cjs/knex/columns.js +36 -0
  64. package/dist/cjs/knex/columns.js.map +1 -0
  65. package/dist/cjs/knex/index.js +26 -43
  66. package/dist/cjs/knex/index.js.map +1 -1
  67. package/dist/cjs/knex/migration-helpers.js +57 -0
  68. package/dist/cjs/knex/migration-helpers.js.map +1 -0
  69. package/dist/cjs/logging/redact.js +6 -11
  70. package/dist/cjs/logging/redact.js.map +1 -1
  71. package/dist/cjs/messaging/base-consumer.js +36 -0
  72. package/dist/cjs/messaging/base-consumer.js.map +1 -0
  73. package/dist/cjs/messaging/consumer.js +142 -73
  74. package/dist/cjs/messaging/consumer.js.map +1 -1
  75. package/dist/cjs/messaging/envelope.js +53 -10
  76. package/dist/cjs/messaging/envelope.js.map +1 -1
  77. package/dist/cjs/messaging/idempotency.js +95 -8
  78. package/dist/cjs/messaging/idempotency.js.map +1 -1
  79. package/dist/cjs/messaging/index.js +2 -1
  80. package/dist/cjs/messaging/index.js.map +1 -1
  81. package/dist/cjs/messaging/outbox.js +97 -55
  82. package/dist/cjs/messaging/outbox.js.map +1 -1
  83. package/dist/cjs/messaging/publisher.js +86 -38
  84. package/dist/cjs/messaging/publisher.js.map +1 -1
  85. package/dist/cjs/messaging/staff-notifier.js +4 -4
  86. package/dist/cjs/messaging/staff-notifier.js.map +1 -1
  87. package/dist/cjs/messaging/topology.js +41 -5
  88. package/dist/cjs/messaging/topology.js.map +1 -1
  89. package/dist/cjs/rate-limit/index.js +57 -6
  90. package/dist/cjs/rate-limit/index.js.map +1 -1
  91. package/dist/cjs/rate-limit/keys.js +47 -0
  92. package/dist/cjs/rate-limit/keys.js.map +1 -0
  93. package/dist/cjs/socket/index.js +127 -16
  94. package/dist/cjs/socket/index.js.map +1 -1
  95. package/dist/cjs/socket/ticket.js +124 -0
  96. package/dist/cjs/socket/ticket.js.map +1 -0
  97. package/dist/cjs/testing/index.js +23 -14
  98. package/dist/cjs/testing/index.js.map +1 -1
  99. package/dist/esm/access/index.js +9 -1
  100. package/dist/esm/access/index.js.map +1 -1
  101. package/dist/esm/approvals/index.js +127 -0
  102. package/dist/esm/approvals/index.js.map +1 -0
  103. package/dist/esm/audit/index.js +70 -0
  104. package/dist/esm/audit/index.js.map +1 -0
  105. package/dist/esm/cache/index.js +70 -13
  106. package/dist/esm/cache/index.js.map +1 -1
  107. package/dist/esm/cache/keyspace.js +95 -0
  108. package/dist/esm/cache/keyspace.js.map +1 -0
  109. package/dist/esm/common/circuit-breaker.js +70 -0
  110. package/dist/esm/common/circuit-breaker.js.map +1 -0
  111. package/dist/esm/common/index.js +4 -0
  112. package/dist/esm/common/index.js.map +1 -1
  113. package/dist/esm/common/internal-client.js +173 -0
  114. package/dist/esm/common/internal-client.js.map +1 -0
  115. package/dist/esm/common/internal-constants.js +9 -0
  116. package/dist/esm/common/internal-constants.js.map +1 -0
  117. package/dist/esm/common/internal-service-auth.js +150 -0
  118. package/dist/esm/common/internal-service-auth.js.map +1 -0
  119. package/dist/esm/common/money.js +207 -0
  120. package/dist/esm/common/money.js.map +1 -0
  121. package/dist/esm/common/phone.js +41 -22
  122. package/dist/esm/common/phone.js.map +1 -1
  123. package/dist/esm/common/time.js +27 -2
  124. package/dist/esm/common/time.js.map +1 -1
  125. package/dist/esm/config/index.js +135 -18
  126. package/dist/esm/config/index.js.map +1 -1
  127. package/dist/esm/contracts/notification-dispatch.js +1 -1
  128. package/dist/esm/database/db.js +32 -0
  129. package/dist/esm/database/db.js.map +1 -0
  130. package/dist/esm/database/index.js +9 -45
  131. package/dist/esm/database/index.js.map +1 -1
  132. package/dist/esm/database/pagination.js +81 -0
  133. package/dist/esm/database/pagination.js.map +1 -0
  134. package/dist/esm/database/pool.js +51 -0
  135. package/dist/esm/database/pool.js.map +1 -0
  136. package/dist/esm/database/scoped-repository.js +128 -0
  137. package/dist/esm/database/scoped-repository.js.map +1 -0
  138. package/dist/esm/database/scopes.js +157 -0
  139. package/dist/esm/database/scopes.js.map +1 -0
  140. package/dist/esm/database/table-names.js +11 -0
  141. package/dist/esm/database/table-names.js.map +1 -0
  142. package/dist/esm/database/tables.js +61 -0
  143. package/dist/esm/database/tables.js.map +1 -0
  144. package/dist/esm/express/auth.js +62 -48
  145. package/dist/esm/express/auth.js.map +1 -1
  146. package/dist/esm/express/index.js +2 -0
  147. package/dist/esm/express/index.js.map +1 -1
  148. package/dist/esm/express/internal-caller.js +49 -0
  149. package/dist/esm/express/internal-caller.js.map +1 -0
  150. package/dist/esm/express/step-up.js +38 -0
  151. package/dist/esm/express/step-up.js.map +1 -0
  152. package/dist/esm/http/index.js +3 -70
  153. package/dist/esm/http/index.js.map +1 -1
  154. package/dist/esm/identity/index.js +246 -64
  155. package/dist/esm/identity/index.js.map +1 -1
  156. package/dist/esm/identity/step-up.js +47 -0
  157. package/dist/esm/identity/step-up.js.map +1 -0
  158. package/dist/esm/index.js +6 -0
  159. package/dist/esm/index.js.map +1 -1
  160. package/dist/esm/knex/columns.js +28 -0
  161. package/dist/esm/knex/columns.js.map +1 -0
  162. package/dist/esm/knex/index.js +8 -34
  163. package/dist/esm/knex/index.js.map +1 -1
  164. package/dist/esm/knex/migration-helpers.js +51 -0
  165. package/dist/esm/knex/migration-helpers.js.map +1 -0
  166. package/dist/esm/logging/redact.js +3 -7
  167. package/dist/esm/logging/redact.js.map +1 -1
  168. package/dist/esm/messaging/base-consumer.js +32 -0
  169. package/dist/esm/messaging/base-consumer.js.map +1 -0
  170. package/dist/esm/messaging/consumer.js +139 -71
  171. package/dist/esm/messaging/consumer.js.map +1 -1
  172. package/dist/esm/messaging/envelope.js +50 -10
  173. package/dist/esm/messaging/envelope.js.map +1 -1
  174. package/dist/esm/messaging/idempotency.js +91 -7
  175. package/dist/esm/messaging/idempotency.js.map +1 -1
  176. package/dist/esm/messaging/index.js +2 -1
  177. package/dist/esm/messaging/index.js.map +1 -1
  178. package/dist/esm/messaging/outbox.js +97 -54
  179. package/dist/esm/messaging/outbox.js.map +1 -1
  180. package/dist/esm/messaging/publisher.js +86 -38
  181. package/dist/esm/messaging/publisher.js.map +1 -1
  182. package/dist/esm/messaging/staff-notifier.js +4 -4
  183. package/dist/esm/messaging/staff-notifier.js.map +1 -1
  184. package/dist/esm/messaging/topology.js +39 -4
  185. package/dist/esm/messaging/topology.js.map +1 -1
  186. package/dist/esm/rate-limit/index.js +41 -6
  187. package/dist/esm/rate-limit/index.js.map +1 -1
  188. package/dist/esm/rate-limit/keys.js +43 -0
  189. package/dist/esm/rate-limit/keys.js.map +1 -0
  190. package/dist/esm/socket/index.js +112 -17
  191. package/dist/esm/socket/index.js.map +1 -1
  192. package/dist/esm/socket/ticket.js +117 -0
  193. package/dist/esm/socket/ticket.js.map +1 -0
  194. package/dist/esm/testing/index.js +24 -15
  195. package/dist/esm/testing/index.js.map +1 -1
  196. package/dist/types/access/index.d.ts +9 -3
  197. package/dist/types/access/index.d.ts.map +1 -1
  198. package/dist/types/approvals/index.d.ts +99 -0
  199. package/dist/types/approvals/index.d.ts.map +1 -0
  200. package/dist/types/audit/index.d.ts +82 -0
  201. package/dist/types/audit/index.d.ts.map +1 -0
  202. package/dist/types/cache/index.d.ts +64 -14
  203. package/dist/types/cache/index.d.ts.map +1 -1
  204. package/dist/types/cache/keyspace.d.ts +84 -0
  205. package/dist/types/cache/keyspace.d.ts.map +1 -0
  206. package/dist/types/common/circuit-breaker.d.ts +51 -0
  207. package/dist/types/common/circuit-breaker.d.ts.map +1 -0
  208. package/dist/types/common/index.d.ts +4 -0
  209. package/dist/types/common/index.d.ts.map +1 -1
  210. package/dist/types/common/internal-client.d.ts +116 -0
  211. package/dist/types/common/internal-client.d.ts.map +1 -0
  212. package/dist/types/common/internal-constants.d.ts +9 -0
  213. package/dist/types/common/internal-constants.d.ts.map +1 -0
  214. package/dist/types/common/internal-service-auth.d.ts +102 -0
  215. package/dist/types/common/internal-service-auth.d.ts.map +1 -0
  216. package/dist/types/common/money.d.ts +104 -0
  217. package/dist/types/common/money.d.ts.map +1 -0
  218. package/dist/types/common/phone.d.ts +25 -15
  219. package/dist/types/common/phone.d.ts.map +1 -1
  220. package/dist/types/common/time.d.ts +20 -2
  221. package/dist/types/common/time.d.ts.map +1 -1
  222. package/dist/types/config/index.d.ts +251 -27
  223. package/dist/types/config/index.d.ts.map +1 -1
  224. package/dist/types/contracts/notification-dispatch.d.ts +13 -13
  225. package/dist/types/database/db.d.ts +46 -0
  226. package/dist/types/database/db.d.ts.map +1 -0
  227. package/dist/types/database/index.d.ts +9 -22
  228. package/dist/types/database/index.d.ts.map +1 -1
  229. package/dist/types/database/pagination.d.ts +68 -0
  230. package/dist/types/database/pagination.d.ts.map +1 -0
  231. package/dist/types/database/pool.d.ts +39 -0
  232. package/dist/types/database/pool.d.ts.map +1 -0
  233. package/dist/types/database/scoped-repository.d.ts +92 -0
  234. package/dist/types/database/scoped-repository.d.ts.map +1 -0
  235. package/dist/types/database/scopes.d.ts +159 -0
  236. package/dist/types/database/scopes.d.ts.map +1 -0
  237. package/dist/types/database/table-names.d.ts +13 -0
  238. package/dist/types/database/table-names.d.ts.map +1 -0
  239. package/dist/types/database/tables.d.ts +748 -0
  240. package/dist/types/database/tables.d.ts.map +1 -0
  241. package/dist/types/express/auth.d.ts +40 -24
  242. package/dist/types/express/auth.d.ts.map +1 -1
  243. package/dist/types/express/index.d.ts +2 -0
  244. package/dist/types/express/index.d.ts.map +1 -1
  245. package/dist/types/express/internal-caller.d.ts +53 -0
  246. package/dist/types/express/internal-caller.d.ts.map +1 -0
  247. package/dist/types/express/step-up.d.ts +38 -0
  248. package/dist/types/express/step-up.d.ts.map +1 -0
  249. package/dist/types/http/index.d.ts +3 -31
  250. package/dist/types/http/index.d.ts.map +1 -1
  251. package/dist/types/identity/index.d.ts +159 -21
  252. package/dist/types/identity/index.d.ts.map +1 -1
  253. package/dist/types/identity/step-up.d.ts +41 -0
  254. package/dist/types/identity/step-up.d.ts.map +1 -0
  255. package/dist/types/index.d.ts +6 -0
  256. package/dist/types/index.d.ts.map +1 -1
  257. package/dist/types/knex/columns.d.ts +15 -0
  258. package/dist/types/knex/columns.d.ts.map +1 -0
  259. package/dist/types/knex/index.d.ts +3 -11
  260. package/dist/types/knex/index.d.ts.map +1 -1
  261. package/dist/types/knex/migration-helpers.d.ts +28 -0
  262. package/dist/types/knex/migration-helpers.d.ts.map +1 -0
  263. package/dist/types/logging/redact.d.ts +2 -2
  264. package/dist/types/logging/redact.d.ts.map +1 -1
  265. package/dist/types/messaging/base-consumer.d.ts +33 -0
  266. package/dist/types/messaging/base-consumer.d.ts.map +1 -0
  267. package/dist/types/messaging/consumer.d.ts +59 -21
  268. package/dist/types/messaging/consumer.d.ts.map +1 -1
  269. package/dist/types/messaging/envelope.d.ts +50 -11
  270. package/dist/types/messaging/envelope.d.ts.map +1 -1
  271. package/dist/types/messaging/idempotency.d.ts +60 -3
  272. package/dist/types/messaging/idempotency.d.ts.map +1 -1
  273. package/dist/types/messaging/index.d.ts +7 -0
  274. package/dist/types/messaging/index.d.ts.map +1 -1
  275. package/dist/types/messaging/outbox.d.ts +62 -22
  276. package/dist/types/messaging/outbox.d.ts.map +1 -1
  277. package/dist/types/messaging/publisher.d.ts +54 -21
  278. package/dist/types/messaging/publisher.d.ts.map +1 -1
  279. package/dist/types/messaging/staff-notifier.d.ts +9 -9
  280. package/dist/types/messaging/staff-notifier.d.ts.map +1 -1
  281. package/dist/types/messaging/staff-recipients.d.ts +1 -1
  282. package/dist/types/messaging/topology.d.ts +40 -8
  283. package/dist/types/messaging/topology.d.ts.map +1 -1
  284. package/dist/types/rate-limit/index.d.ts +40 -4
  285. package/dist/types/rate-limit/index.d.ts.map +1 -1
  286. package/dist/types/rate-limit/keys.d.ts +52 -0
  287. package/dist/types/rate-limit/keys.d.ts.map +1 -0
  288. package/dist/types/socket/index.d.ts +86 -2
  289. package/dist/types/socket/index.d.ts.map +1 -1
  290. package/dist/types/socket/ticket.d.ts +90 -0
  291. package/dist/types/socket/ticket.d.ts.map +1 -0
  292. package/dist/types/testing/index.d.ts +31 -10
  293. package/dist/types/testing/index.d.ts.map +1 -1
  294. package/dist/types/types/index.d.ts +26 -14
  295. package/dist/types/types/index.d.ts.map +1 -1
  296. package/package.json +3 -1
@@ -0,0 +1,207 @@
1
+ import Decimal from 'decimal.js';
2
+ /**
3
+ * Exact money arithmetic. Amounts travel and persist as decimal strings (`"1250.00"`) and are only ever
4
+ * computed with a decimal type, never JS numbers. The currency is always passed in: shared-lib has no
5
+ * default currency.
6
+ *
7
+ * Rounding is half-up (half away from zero) to the currency's minor unit, after every operation that can
8
+ * produce extra digits (`mul`), so a `Money` value always holds a whole number of minor units.
9
+ */
10
+ /** A decimal.js constructor with enough precision for any realistic amount and half-up rounding. */
11
+ const Dec = Decimal.clone({ precision: 40, rounding: Decimal.ROUND_HALF_UP, toExpNeg: -40, toExpPos: 40 });
12
+ const CODE_PATTERN = /^[A-Z]{3}$/;
13
+ const AMOUNT_PATTERN = /^-?\d+(\.\d+)?$/;
14
+ /**
15
+ * Resolves a currency code or spec, validating both.
16
+ *
17
+ * @param currency ISO 4217 code (minor units from `Intl`) or `{ code, minorUnits }`.
18
+ * @returns The spec.
19
+ * @throws RangeError for a malformed code or minor-unit count.
20
+ * @example resolveCurrency('USD') // { code: 'USD', minorUnits: 2 }
21
+ */
22
+ export function resolveCurrency(currency) {
23
+ if (typeof currency !== 'string') {
24
+ if (!CODE_PATTERN.test(currency.code))
25
+ throw new RangeError(`Invalid currency code "${currency.code}"`);
26
+ if (!Number.isInteger(currency.minorUnits) || currency.minorUnits < 0 || currency.minorUnits > 6) {
27
+ throw new RangeError(`Invalid minor units ${currency.minorUnits} for ${currency.code}`);
28
+ }
29
+ return { code: currency.code, minorUnits: currency.minorUnits };
30
+ }
31
+ if (!CODE_PATTERN.test(currency))
32
+ throw new RangeError(`Invalid currency code "${currency}"`);
33
+ const minorUnits = new Intl.NumberFormat('en', { style: 'currency', currency }).resolvedOptions().maximumFractionDigits ?? 2;
34
+ return { code: currency, minorUnits };
35
+ }
36
+ function toDecimal(value, what) {
37
+ if (typeof value === 'number' && !Number.isFinite(value))
38
+ throw new RangeError(`${what} must be finite`);
39
+ const text = String(value).trim();
40
+ if (!AMOUNT_PATTERN.test(text))
41
+ throw new RangeError(`${what} must be a decimal number, got "${value}"`);
42
+ return new Dec(text);
43
+ }
44
+ /** An immutable amount of one currency, always a whole number of minor units. */
45
+ export class Money {
46
+ value;
47
+ currency;
48
+ constructor(value,
49
+ /** The amount's currency. */
50
+ currency) {
51
+ this.value = value;
52
+ this.currency = currency;
53
+ Object.freeze(this);
54
+ }
55
+ /**
56
+ * Parses a decimal string. Strict: no thousands separators, symbols or more decimals than the
57
+ * currency allows (D5), so a stored value is never silently rounded on the way in.
58
+ *
59
+ * @param amount e.g. `"1250.5"` or `"-3.25"`.
60
+ * @param currency Code or spec.
61
+ * @returns The amount.
62
+ * @throws RangeError for anything else.
63
+ * @example Money.fromDecimalString('19.99', 'USD')
64
+ */
65
+ static fromDecimalString(amount, currency) {
66
+ const spec = resolveCurrency(currency);
67
+ if (typeof amount !== 'string')
68
+ throw new RangeError('Amount must be a decimal string');
69
+ const value = toDecimal(amount, 'Amount');
70
+ if (value.decimalPlaces() > spec.minorUnits) {
71
+ throw new RangeError(`Amount "${amount}" has more than ${spec.minorUnits} decimal places for ${spec.code}`);
72
+ }
73
+ return new Money(value, spec);
74
+ }
75
+ /**
76
+ * Builds an amount from an integer count of minor units (e.g. cents).
77
+ *
78
+ * @param units Integer (number within the safe range, or bigint).
79
+ * @param currency Code or spec.
80
+ * @returns The amount.
81
+ * @example Money.fromMinorUnits(1999, 'USD').toDecimalString() // '19.99'
82
+ */
83
+ static fromMinorUnits(units, currency) {
84
+ const spec = resolveCurrency(currency);
85
+ if (typeof units === 'number' && !Number.isSafeInteger(units))
86
+ throw new RangeError('Minor units must be a safe integer');
87
+ return new Money(new Dec(units.toString()).div(new Dec(10).pow(spec.minorUnits)), spec);
88
+ }
89
+ /**
90
+ * Zero in a currency.
91
+ *
92
+ * @param currency Code or spec.
93
+ * @returns `0` of that currency.
94
+ */
95
+ static zero(currency) {
96
+ return new Money(new Dec(0), resolveCurrency(currency));
97
+ }
98
+ /** The amount with exactly the currency's decimal places, e.g. `"1250.00"` — the wire and storage form. */
99
+ toDecimalString() {
100
+ return this.value.toFixed(this.currency.minorUnits);
101
+ }
102
+ /** The amount as an integer count of minor units. */
103
+ toMinorUnits() {
104
+ return BigInt(this.value.times(new Dec(10).pow(this.currency.minorUnits)).toFixed(0));
105
+ }
106
+ /** Serialises as the decimal string, so an amount never becomes a JSON number by accident. */
107
+ toJSON() {
108
+ return this.toDecimalString();
109
+ }
110
+ /** Same as {@link toDecimalString}. */
111
+ toString() {
112
+ return this.toDecimalString();
113
+ }
114
+ /** Sum of two amounts of the same currency. */
115
+ add(other) {
116
+ return new Money(this.value.plus(this.same(other).value), this.currency);
117
+ }
118
+ /** Difference of two amounts of the same currency. */
119
+ sub(other) {
120
+ return new Money(this.value.minus(this.same(other).value), this.currency);
121
+ }
122
+ /**
123
+ * Multiplies by a rate (tax, fee, quantity), rounding half-up to the minor unit.
124
+ *
125
+ * @param rate e.g. `"0.16"` or `3`.
126
+ * @returns The rounded product.
127
+ * @example Money.fromDecimalString('10.05', 'USD').mul('0.5').toDecimalString() // '5.03'
128
+ */
129
+ mul(rate) {
130
+ return new Money(this.round(this.value.times(toDecimal(rate, 'Rate'))), this.currency);
131
+ }
132
+ /**
133
+ * Splits the amount in proportion to `ratios`; the parts always sum to the amount exactly. Each part is
134
+ * rounded down to the minor unit and the left-over units go, one each, to the parts with the largest
135
+ * remainders (earlier parts win ties). A negative amount is split like its absolute value, then negated.
136
+ *
137
+ * @param ratios Non-negative weights with a positive sum, e.g. `['1', '1', '1']` or `[70, 30]`.
138
+ * @returns One amount per ratio, in order.
139
+ * @throws RangeError for an empty list, a negative ratio or a zero sum.
140
+ * @example Money.fromDecimalString('100.00', 'USD').allocate([1, 1, 1]).map(String) // ['33.34', '33.33', '33.33']
141
+ */
142
+ allocate(ratios) {
143
+ if (ratios.length === 0)
144
+ throw new RangeError('allocate needs at least one ratio');
145
+ const weights = ratios.map((r) => toDecimal(r, 'Ratio'));
146
+ if (weights.some((w) => w.isNegative()))
147
+ throw new RangeError('Ratios must not be negative');
148
+ const sum = weights.reduce((acc, w) => acc.plus(w), new Dec(0));
149
+ if (sum.isZero())
150
+ throw new RangeError('Ratios must not all be zero');
151
+ const scale = new Dec(10).pow(this.currency.minorUnits);
152
+ const total = this.value.abs().times(scale);
153
+ const exact = weights.map((w) => total.times(w).div(sum));
154
+ const floors = exact.map((e) => e.floor());
155
+ let leftover = total.minus(floors.reduce((acc, f) => acc.plus(f), new Dec(0))).toNumber();
156
+ const order = exact.map((e, i) => ({ i, rem: e.minus(floors[i]) })).sort((a, b) => b.rem.comparedTo(a.rem) || a.i - b.i);
157
+ for (const { i } of order) {
158
+ if (leftover <= 0)
159
+ break;
160
+ floors[i] = floors[i].plus(1);
161
+ leftover -= 1;
162
+ }
163
+ const sign = this.value.isNegative() ? -1 : 1;
164
+ return floors.map((units) => new Money(units.div(scale).times(sign), this.currency));
165
+ }
166
+ /**
167
+ * Rounds up (toward +∞) to a multiple of `unit`, e.g. whole units for a payment rail that rejects cents.
168
+ *
169
+ * @param unit Positive step in major units, e.g. `"1"` or `"0.05"`; defaults to `"1"`.
170
+ * @returns The rounded-up amount.
171
+ * @example Money.fromDecimalString('99.01', 'USD').ceilToUnit().toDecimalString() // '100.00'
172
+ */
173
+ ceilToUnit(unit = '1') {
174
+ const step = toDecimal(unit, 'Unit');
175
+ if (step.lte(0))
176
+ throw new RangeError('Unit must be positive');
177
+ if (step.decimalPlaces() > this.currency.minorUnits)
178
+ throw new RangeError(`Unit is finer than the ${this.currency.code} minor unit`);
179
+ return new Money(this.value.div(step).ceil().times(step), this.currency);
180
+ }
181
+ /** -1, 0 or 1 comparing this amount with `other` (same currency). */
182
+ compare(other) {
183
+ return this.value.comparedTo(this.same(other).value);
184
+ }
185
+ /** True when both amounts have the same currency and value. */
186
+ equals(other) {
187
+ return this.currency.code === other.currency.code && this.value.equals(other.value);
188
+ }
189
+ /** True for zero. */
190
+ isZero() {
191
+ return this.value.isZero();
192
+ }
193
+ /** True below zero. */
194
+ isNegative() {
195
+ return this.value.isNegative() && !this.value.isZero();
196
+ }
197
+ same(other) {
198
+ if (other.currency.code !== this.currency.code) {
199
+ throw new RangeError(`Currency mismatch: ${this.currency.code} vs ${other.currency.code}`);
200
+ }
201
+ return other;
202
+ }
203
+ round(value) {
204
+ return value.toDecimalPlaces(this.currency.minorUnits, Decimal.ROUND_HALF_UP);
205
+ }
206
+ }
207
+ //# sourceMappingURL=money.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"money.js","sourceRoot":"","sources":["../../../src/common/money.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,YAAY,CAAC;AAEjC;;;;;;;GAOG;AAEH,oGAAoG;AACpG,MAAM,GAAG,GAAG,OAAO,CAAC,KAAK,CAAC,EAAE,SAAS,EAAE,EAAE,EAAE,QAAQ,EAAE,OAAO,CAAC,aAAa,EAAE,QAAQ,EAAE,CAAC,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC;AAiB3G,MAAM,YAAY,GAAG,YAAY,CAAC;AAClC,MAAM,cAAc,GAAG,iBAAiB,CAAC;AAEzC;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,QAAuB;IACrD,IAAI,OAAO,QAAQ,KAAK,QAAQ,EAAE,CAAC;QACjC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,MAAM,IAAI,UAAU,CAAC,0BAA0B,QAAQ,CAAC,IAAI,GAAG,CAAC,CAAC;QACxG,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,QAAQ,CAAC,UAAU,GAAG,CAAC,IAAI,QAAQ,CAAC,UAAU,GAAG,CAAC,EAAE,CAAC;YACjG,MAAM,IAAI,UAAU,CAAC,uBAAuB,QAAQ,CAAC,UAAU,QAAQ,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;QAC1F,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,EAAE,QAAQ,CAAC,UAAU,EAAE,CAAC;IAClE,CAAC;IACD,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,0BAA0B,QAAQ,GAAG,CAAC,CAAC;IAC9F,MAAM,UAAU,GAAG,IAAI,IAAI,CAAC,YAAY,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,CAAC,eAAe,EAAE,CAAC,qBAAqB,IAAI,CAAC,CAAC;IAC7H,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC;AACxC,CAAC;AAED,SAAS,SAAS,CAAC,KAAmB,EAAE,IAAY;IAClD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,GAAG,IAAI,iBAAiB,CAAC,CAAC;IACzG,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC;IAClC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,GAAG,IAAI,mCAAmC,KAAK,GAAG,CAAC,CAAC;IACzG,OAAO,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC;AACvB,CAAC;AAED,iFAAiF;AACjF,MAAM,OAAO,KAAK;IAEG;IAER;IAHX,YACmB,KAAU;IAC3B,6BAA6B;IACpB,QAAsB;QAFd,UAAK,GAAL,KAAK,CAAK;QAElB,aAAQ,GAAR,QAAQ,CAAc;QAE/B,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACtB,CAAC;IAED;;;;;;;;;OASG;IACH,MAAM,CAAC,iBAAiB,CAAC,MAAc,EAAE,QAAuB;QAC9D,MAAM,IAAI,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;QACvC,IAAI,OAAO,MAAM,KAAK,QAAQ;YAAE,MAAM,IAAI,UAAU,CAAC,iCAAiC,CAAC,CAAC;QACxF,MAAM,KAAK,GAAG,SAAS,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QAC1C,IAAI,KAAK,CAAC,aAAa,EAAE,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC;YAC5C,MAAM,IAAI,UAAU,CAAC,WAAW,MAAM,mBAAmB,IAAI,CAAC,UAAU,uBAAuB,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QAC9G,CAAC;QACD,OAAO,IAAI,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IAChC,CAAC;IAED;;;;;;;OAOG;IACH,MAAM,CAAC,cAAc,CAAC,KAAsB,EAAE,QAAuB;QACnE,MAAM,IAAI,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;QACvC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC;YAAE,MAAM,IAAI,UAAU,CAAC,oCAAoC,CAAC,CAAC;QAC1H,OAAO,IAAI,KAAK,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;IAC1F,CAAC;IAED;;;;;OAKG;IACH,MAAM,CAAC,IAAI,CAAC,QAAuB;QACjC,OAAO,IAAI,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,eAAe,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC1D,CAAC;IAED,2GAA2G;IAC3G,eAAe;QACb,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;IACtD,CAAC;IAED,qDAAqD;IACrD,YAAY;QACV,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IACxF,CAAC;IAED,8FAA8F;IAC9F,MAAM;QACJ,OAAO,IAAI,CAAC,eAAe,EAAE,CAAC;IAChC,CAAC;IAED,uCAAuC;IACvC,QAAQ;QACN,OAAO,IAAI,CAAC,eAAe,EAAE,CAAC;IAChC,CAAC;IAED,+CAA+C;IAC/C,GAAG,CAAC,KAAY;QACd,OAAO,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC3E,CAAC;IAED,sDAAsD;IACtD,GAAG,CAAC,KAAY;QACd,OAAO,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC5E,CAAC;IAED;;;;;;OAMG;IACH,GAAG,CAAC,IAAkB;QACpB,OAAO,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IACzF,CAAC;IAED;;;;;;;;;OASG;IACH,QAAQ,CAAC,MAA+B;QACtC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,MAAM,IAAI,UAAU,CAAC,mCAAmC,CAAC,CAAC;QACnF,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;QACzD,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC;YAAE,MAAM,IAAI,UAAU,CAAC,6BAA6B,CAAC,CAAC;QAC7F,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAChE,IAAI,GAAG,CAAC,MAAM,EAAE;YAAE,MAAM,IAAI,UAAU,CAAC,6BAA6B,CAAC,CAAC;QAEtE,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;QACxD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC5C,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;QAC1D,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;QAC3C,IAAI,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;QAC1F,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QACzH,KAAK,MAAM,EAAE,CAAC,EAAE,IAAI,KAAK,EAAE,CAAC;YAC1B,IAAI,QAAQ,IAAI,CAAC;gBAAE,MAAM;YACzB,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAC9B,QAAQ,IAAI,CAAC,CAAC;QAChB,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC9C,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;IACvF,CAAC;IAED;;;;;;OAMG;IACH,UAAU,CAAC,OAAqB,GAAG;QACjC,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACrC,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;YAAE,MAAM,IAAI,UAAU,CAAC,uBAAuB,CAAC,CAAC;QAC/D,IAAI,IAAI,CAAC,aAAa,EAAE,GAAG,IAAI,CAAC,QAAQ,CAAC,UAAU;YAAE,MAAM,IAAI,UAAU,CAAC,0BAA0B,IAAI,CAAC,QAAQ,CAAC,IAAI,aAAa,CAAC,CAAC;QACrI,OAAO,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC3E,CAAC;IAED,qEAAqE;IACrE,OAAO,CAAC,KAAY;QAClB,OAAO,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAe,CAAC;IACrE,CAAC;IAED,+DAA+D;IAC/D,MAAM,CAAC,KAAY;QACjB,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,KAAK,KAAK,CAAC,QAAQ,CAAC,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACtF,CAAC;IAED,qBAAqB;IACrB,MAAM;QACJ,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;IAC7B,CAAC;IAED,uBAAuB;IACvB,UAAU;QACR,OAAO,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;IACzD,CAAC;IAEO,IAAI,CAAC,KAAY;QACvB,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,KAAK,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;YAC/C,MAAM,IAAI,UAAU,CAAC,sBAAsB,IAAI,CAAC,QAAQ,CAAC,IAAI,OAAO,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;QAC7F,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAEO,KAAK,CAAC,KAAU;QACtB,OAAO,KAAK,CAAC,eAAe,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;IAChF,CAAC;CACF"}
@@ -1,4 +1,5 @@
1
1
  import libphonenumber from 'google-libphonenumber';
2
+ import { BadRequestError } from './errors.js';
2
3
  const util = libphonenumber.PhoneNumberUtil.getInstance();
3
4
  const { PhoneNumberFormat } = libphonenumber;
4
5
  /**
@@ -6,9 +7,9 @@ const { PhoneNumberFormat } = libphonenumber;
6
7
  * `0712345678` only make sense against a country, and that country is app configuration.
7
8
  *
8
9
  * @param phone Number as typed (local or international format).
9
- * @param region ISO 3166-1 alpha-2 region used for local formats (e.g. `KE`, `GB`).
10
- * @returns E.164 (`+254712345678`), or null when not a valid number for that region.
11
- * @example toE164('0712345678', 'KE') // '+254712345678'
10
+ * @param region ISO 3166-1 alpha-2 region used for local formats (e.g. `GB`, `US`).
11
+ * @returns E.164 (`+447400123456`), or null when not a valid number for that region.
12
+ * @example toE164('07400 123456', 'GB') // '+447400123456'
12
13
  */
13
14
  export function toE164(phone, region) {
14
15
  try {
@@ -19,17 +20,36 @@ export function toE164(phone, region) {
19
20
  return null;
20
21
  }
21
22
  }
23
+ const MOBILE_TYPES = new Set([libphonenumber.PhoneNumberType.MOBILE, libphonenumber.PhoneNumberType.FIXED_LINE_OR_MOBILE]);
22
24
  /**
23
- * E.164 without the plus sign, as most SMS gateways expect.
25
+ * E.164 without the plus sign (an MSISDN), as SMS gateways and mobile-money rails expect. Throws instead of
26
+ * returning null so a bad number can never be stored or sent by mistake; use {@link toE164} to test input.
24
27
  *
25
- * @param phone Number as typed.
26
- * @param region ISO region for local formats.
27
- * @returns Digits only, or null when invalid.
28
- * @example toMsisdn('0712345678', 'KE') // '254712345678'
28
+ * @param phone Number as typed (local or international format).
29
+ * @param region ISO 3166-1 alpha-2 region used for local formats (required; no app default).
30
+ * @param options Mobile-only and same-region checks.
31
+ * @returns Digits only, country code first.
32
+ * @throws BadRequestError (`INVALID_PHONE`) when the number is invalid or fails an option.
33
+ * @example toMsisdn('07400 123456', 'GB') // '447400123456'
29
34
  */
30
- export function toMsisdn(phone, region) {
31
- const e164 = toE164(phone, region);
32
- return e164 ? e164.slice(1) : null;
35
+ export function toMsisdn(phone, region, options = {}) {
36
+ const invalid = () => new BadRequestError('Enter a valid phone number', { code: 'INVALID_PHONE' });
37
+ if (typeof phone !== 'string' || !region)
38
+ throw invalid();
39
+ let parsed;
40
+ try {
41
+ parsed = util.parseAndKeepRawInput(phone.trim(), region);
42
+ }
43
+ catch {
44
+ throw invalid();
45
+ }
46
+ if (!util.isValidNumber(parsed))
47
+ throw invalid();
48
+ if (options.regionOnly && util.getRegionCodeForNumber(parsed) !== region.toUpperCase())
49
+ throw invalid();
50
+ if (options.mobileOnly && !MOBILE_TYPES.has(util.getNumberType(parsed)))
51
+ throw invalid();
52
+ return util.format(parsed, PhoneNumberFormat.E164).slice(1);
33
53
  }
34
54
  /**
35
55
  * The region a number belongs to.
@@ -37,7 +57,7 @@ export function toMsisdn(phone, region) {
37
57
  * @param phone Number as typed.
38
58
  * @param region ISO region used to interpret local formats.
39
59
  * @returns ISO region code, or null when it cannot be determined.
40
- * @example phoneRegion('+447400123456', 'KE') // 'GB'
60
+ * @example phoneRegion('+447400123456', 'US') // 'GB'
41
61
  */
42
62
  export function phoneRegion(phone, region) {
43
63
  try {
@@ -48,19 +68,18 @@ export function phoneRegion(phone, region) {
48
68
  }
49
69
  }
50
70
  /**
51
- * Masks a phone number for logs and UI, so a full number never lands in a log line or screen.
52
- * Never reveals more than about a third of the input: numbers of 10+ characters keep the first 4 and
53
- * last 2; shorter inputs keep only the last 2; 4 characters or fewer are fully masked.
71
+ * Masks a phone number for logs, audit metadata and UI: every digit except the last 4 becomes `*`, so a
72
+ * full number never lands in a log line or on a screen. Fewer than 5 digits are fully masked
73
+ * (the same rule the logger applies to phone-like values).
54
74
  *
55
75
  * @param phone Any phone string (normalised or not).
56
- * @returns The masked value.
57
- * @example maskPhone('254712345678') // '2547****78'
76
+ * @returns The masked digits.
77
+ * @example maskPhone('+44 7400 123456') // '********3456'
58
78
  */
59
79
  export function maskPhone(phone) {
60
- if (phone.length <= 4)
61
- return '****';
62
- if (phone.length < 10)
63
- return `****${phone.slice(-2)}`;
64
- return `${phone.slice(0, 4)}****${phone.slice(-2)}`;
80
+ const digits = String(phone ?? '').replace(/\D/g, '');
81
+ if (digits.length < 5)
82
+ return '*'.repeat(digits.length || 4);
83
+ return `${'*'.repeat(digits.length - 4)}${digits.slice(-4)}`;
65
84
  }
66
85
  //# sourceMappingURL=phone.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"phone.js","sourceRoot":"","sources":["../../../src/common/phone.ts"],"names":[],"mappings":"AAAA,OAAO,cAAc,MAAM,uBAAuB,CAAC;AAEnD,MAAM,IAAI,GAAG,cAAc,CAAC,eAAe,CAAC,WAAW,EAAE,CAAC;AAC1D,MAAM,EAAE,iBAAiB,EAAE,GAAG,cAAc,CAAC;AAE7C;;;;;;;;GAQG;AACH,MAAM,UAAU,MAAM,CAAC,KAAa,EAAE,MAAc;IAClD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,oBAAoB,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,MAAM,CAAC,CAAC;QAC/D,OAAO,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACzF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,QAAQ,CAAC,KAAa,EAAE,MAAc;IACpD,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IACnC,OAAO,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACrC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW,CAAC,KAAa,EAAE,MAAc;IACvD,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,sBAAsB,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,IAAI,IAAI,CAAC;IACxE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,SAAS,CAAC,KAAa;IACrC,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC;QAAE,OAAO,MAAM,CAAC;IACrC,IAAI,KAAK,CAAC,MAAM,GAAG,EAAE;QAAE,OAAO,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACvD,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AACtD,CAAC"}
1
+ {"version":3,"file":"phone.js","sourceRoot":"","sources":["../../../src/common/phone.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- /** True for IANA zones the runtime knows (`Africa/Nairobi`), false for garbage. */
1
+ /** True for IANA zones the runtime knows (`Europe/Berlin`), false for garbage. */
2
2
  export function isValidTimeZone(zone) {
3
3
  try {
4
4
  new Intl.DateTimeFormat('en-US', { timeZone: zone });
@@ -68,8 +68,19 @@ export function zonedWallTimeToInstant(year, month, day, minutesOfDay, zone) {
68
68
  const estimate = wall - timeZoneOffsetMinutes(zone, new Date(wall)) * MS_PER_MINUTE;
69
69
  return new Date(wall - timeZoneOffsetMinutes(zone, new Date(estimate)) * MS_PER_MINUTE);
70
70
  }
71
- /** The local calendar day containing `at` in `zone`, as a half-open `[start, end)` UTC range (23–25 h on DST days). */
71
+ /**
72
+ * The local calendar day containing `at` in `zone`, as a half-open `[start, end)` UTC range (23–25 h on
73
+ * DST days). Use it for "today" in reports, business days and daily limits.
74
+ *
75
+ * @param at Any instant in the day.
76
+ * @param zone IANA zone (required; no app default).
77
+ * @returns `{ start, end }` with `start` inclusive and `end` exclusive.
78
+ * @throws RangeError for an unknown zone.
79
+ * @example zonedDayBounds(new Date('2026-10-11T22:30:00Z'), 'Asia/Riyadh') // 2026-10-11T21:00Z → 2026-10-12T21:00Z
80
+ */
72
81
  export function zonedDayBounds(at, zone) {
82
+ if (!isValidTimeZone(zone))
83
+ throw new RangeError(`Unknown time zone "${zone}"`);
73
84
  const { year, month, day } = localDate(at, zone);
74
85
  return { start: zonedWallTimeToInstant(year, month, day, 0, zone), end: zonedWallTimeToInstant(year, month, day + 1, 0, zone) };
75
86
  }
@@ -93,4 +104,18 @@ export function quietWindowEndAt(at, zone, start, end) {
93
104
  const endsTomorrow = crossesMidnight && msOfDay >= startMs;
94
105
  return zonedWallTimeToInstant(year, month, day + (endsTomorrow ? 1 : 0), endMin, zone);
95
106
  }
107
+ /**
108
+ * The instant as a UTC ISO-8601 string (`2026-11-13T09:00:00.000Z`), the form every timestamp travels in.
109
+ *
110
+ * @param instant A Date, an epoch-milliseconds number or a string `Date` can parse.
111
+ * @returns The `toISOString()` form.
112
+ * @throws RangeError when the input is not a valid instant (never returns `"Invalid Date"`).
113
+ * @example toUtcIso('2026-11-13T12:00:00+03:00') // '2026-11-13T09:00:00.000Z'
114
+ */
115
+ export function toUtcIso(instant) {
116
+ const date = instant instanceof Date ? instant : new Date(instant);
117
+ if (Number.isNaN(date.getTime()))
118
+ throw new RangeError(`Not a valid instant: "${String(instant)}"`);
119
+ return date.toISOString();
120
+ }
96
121
  //# sourceMappingURL=time.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"time.js","sourceRoot":"","sources":["../../../src/common/time.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"time.js","sourceRoot":"","sources":["../../../src/common/time.ts"],"names":[],"mappings":"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"}
@@ -1,11 +1,14 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
3
  import { z } from 'zod';
4
- /**
5
- * Environment building blocks. Services compose the pieces they need:
4
+ import { MIN_INTERNAL_CREDENTIAL_LENGTH, SERVICE_NAME_PATTERN } from '../common/internal-constants.js';
5
+ /*
6
+ * Environment building blocks. `loadEnv(schema)` merges a service's schema onto `baseEnv`;
7
+ * `parseEnv(schema)` validates any composition of the blocks without the base:
6
8
  *
7
- * const env = loadEnv(serviceEnv.merge(databaseEnv).merge(messagingEnv).extend({ MY_VAR: z.string() }));
9
+ * const config = loadEnv(z.object({ MY_VAR: z.string() }));
8
10
  */
11
+ /** Service identity, logging, CORS and the gateway signing secret. */
9
12
  export const serviceEnv = z.object({
10
13
  NODE_ENV: z.enum(['development', 'test', 'staging', 'production']).default('development'),
11
14
  PORT: z.coerce.number().int().positive(),
@@ -14,11 +17,22 @@ export const serviceEnv = z.object({
14
17
  APP_NAME: z.string().min(1).default('app'),
15
18
  LOG_LEVEL: z.enum(['error', 'warn', 'info', 'http', 'debug']).optional(),
16
19
  CORS_ORIGINS: z.string().default(''),
17
- /** Verifies gateway-signed identity headers. */
18
- GATEWAY_SHARED_SECRET: z.string().min(16),
19
- /** Authenticates service-to-service `/internal/*` calls. */
20
- INTERNAL_SERVICE_SECRET: z.string().min(16),
20
+ /** Verifies gateway-signed identity headers (v2); at least 32 characters, same value on the gateway and every service. */
21
+ GATEWAY_SIGNING_SECRET: z.string().min(32),
21
22
  });
23
+ /**
24
+ * Service-to-service credentials. Optional here because development may run without them;
25
+ * `assertInternalServiceAuthConfigured` enforces them in staging and production.
26
+ */
27
+ export const internalServiceEnv = z.object({
28
+ /** This service's own name (its compose service name), sent with every internal call. */
29
+ INTERNAL_SERVICE_NAME: z.string().regex(SERVICE_NAME_PATTERN).optional(),
30
+ /** This service's own secret (at least 32 characters), sent with every internal call. */
31
+ INTERNAL_SERVICE_CREDENTIAL: z.string().min(MIN_INTERNAL_CREDENTIAL_LENGTH).optional(),
32
+ /** JSON map of caller name → secret this service accepts from that caller. */
33
+ INTERNAL_SERVICE_CREDENTIALS: z.string().optional(),
34
+ });
35
+ /** Database connection env vars (MySQL defaults). */
22
36
  export const databaseEnv = z.object({
23
37
  DB_HOST: z.string().default('127.0.0.1'),
24
38
  DB_PORT: z.coerce.number().int().positive().default(3306),
@@ -27,6 +41,7 @@ export const databaseEnv = z.object({
27
41
  DB_NAME: z.string().min(1),
28
42
  DB_POOL_MAX: z.coerce.number().int().positive().default(10),
29
43
  });
44
+ /** RabbitMQ connection and queue-prefix env vars. */
30
45
  export const messagingEnv = z.object({
31
46
  RABBITMQ_URL: z.string().optional(),
32
47
  RABBITMQ_HOST: z.string().default('127.0.0.1'),
@@ -38,20 +53,39 @@ export const messagingEnv = z.object({
38
53
  QUEUE_PREFIX: z.string().default('dev'),
39
54
  RABBITMQ_ENABLED: z.enum(['true', 'false']).default('true').transform((v) => v === 'true'),
40
55
  });
56
+ /** Redis connection env vars. Keys are prefixed by the keyspace (`createRedisKeyspace`), never by the client. */
41
57
  export const redisEnv = z.object({
58
+ /** Full connection URL; overrides host/port/password when set. */
42
59
  REDIS_URL: z.string().optional(),
60
+ REDIS_HOST: z.string().default('127.0.0.1'),
61
+ REDIS_PORT: z.coerce.number().int().positive().default(6379),
62
+ REDIS_PASSWORD: z.string().optional(),
43
63
  REDIS_DB: z.coerce.number().int().min(0).default(0),
44
- /** Key prefix, e.g. `app:dev:` — never share one between apps or environments. */
45
- REDIS_PREFIX: z.string().default(''),
64
+ /**
65
+ * Environment segment of every Redis key (`{app}:{env}:{service}:…`), e.g. `dev`, `staging`, `prod`.
66
+ * Required in staging and production; never share one between environments.
67
+ */
68
+ CACHE_KEY_PREFIX: z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/).optional(),
46
69
  });
47
- /** Builds an AMQP URL from discrete env vars (or returns RABBITMQ_URL when set). */
70
+ /**
71
+ * Builds an AMQP URL from discrete env vars (or returns RABBITMQ_URL when set). User, password and vhost are URL-encoded.
72
+ *
73
+ * @param env Parsed {@link messagingEnv} values.
74
+ * @returns An `amqp://` URL.
75
+ * @example const url = amqpUrlFromEnv(parseEnv(messagingEnv));
76
+ */
48
77
  export function amqpUrlFromEnv(env) {
49
78
  if (env.RABBITMQ_URL)
50
79
  return env.RABBITMQ_URL;
51
80
  const vhost = env.RABBITMQ_VHOST === '/' ? '%2F' : encodeURIComponent(env.RABBITMQ_VHOST.replace(/^\//, ''));
52
81
  return `amqp://${encodeURIComponent(env.RABBITMQ_USER)}:${encodeURIComponent(env.RABBITMQ_PASSWORD)}@${env.RABBITMQ_HOST}:${env.RABBITMQ_PORT}/${vhost}`;
53
82
  }
54
- /** Reads `KEY=value` lines from a dotenv file into process.env without overriding set values. */
83
+ /**
84
+ * Reads `KEY=value` lines from a dotenv file into process.env without overriding set values. Missing file is a no-op.
85
+ *
86
+ * @param path Dotenv file (default `./.env`).
87
+ * @example loadDotEnv(resolve(__dirname, '../.env.test'));
88
+ */
55
89
  export function loadDotEnv(path = resolve(process.cwd(), '.env')) {
56
90
  if (!existsSync(path))
57
91
  return;
@@ -70,19 +104,102 @@ export function loadDotEnv(path = resolve(process.cwd(), '.env')) {
70
104
  process.env[key] = value;
71
105
  }
72
106
  }
107
+ /** Thrown by the env loader when required variables are missing or invalid; lists every problem. */
73
108
  export class ConfigError extends Error {
74
109
  }
75
- /** Loads `.env` (if present) and validates process.env. Throws ConfigError listing every problem. */
76
- export function loadEnv(schema, source = process.env, options = {}) {
110
+ /**
111
+ * The base environment every service validates: service identity, gateway secret, database, Redis,
112
+ * RabbitMQ and internal-service credentials. `loadEnv` merges a service's own schema onto it.
113
+ */
114
+ export const baseEnv = serviceEnv.merge(databaseEnv).merge(redisEnv).merge(messagingEnv).merge(internalServiceEnv);
115
+ /** Variables that have a development default but must be set explicitly in staging and production. */
116
+ export const DEPLOYED_REQUIRED_ENV = Object.freeze(['CACHE_KEY_PREFIX', 'QUEUE_PREFIX']);
117
+ const DEPLOYED_NODE_ENVS = new Set(['staging', 'production']);
118
+ /**
119
+ * True for NODE_ENV values that must name their environments explicitly (staging, production).
120
+ *
121
+ * @param nodeEnv NODE_ENV value (default `process.env.NODE_ENV`).
122
+ * @returns Whether it is a deployed environment.
123
+ * @example isDeployedNodeEnv('production') // true
124
+ */
125
+ export function isDeployedNodeEnv(nodeEnv = process.env.NODE_ENV) {
126
+ return DEPLOYED_NODE_ENVS.has((nodeEnv ?? '').trim());
127
+ }
128
+ function collectProblems(issues, source) {
129
+ const byName = new Map();
130
+ for (const issue of issues) {
131
+ const name = String(issue.path[0] ?? '(root)');
132
+ const raw = source[name];
133
+ byName.set(name, { name, problem: raw === undefined || raw === '' ? 'missing' : 'invalid' });
134
+ }
135
+ return [...byName.values()];
136
+ }
137
+ function deployedProblems(source, required) {
138
+ if (!isDeployedNodeEnv(source.NODE_ENV))
139
+ return [];
140
+ return required.filter((name) => !source[name]?.trim()).map((name) => ({ name, problem: 'missing' }));
141
+ }
142
+ /**
143
+ * Formats problems as `NAME: missing|invalid` lines (names only — values are never printed, because
144
+ * Zod's own messages can echo them).
145
+ *
146
+ * @param problems Problems from validation.
147
+ * @returns A multi-line message.
148
+ * @example formatEnvProblems([{ name: 'DB_NAME', problem: 'missing' }]) // 'Invalid environment configuration:\n DB_NAME: missing'
149
+ */
150
+ export function formatEnvProblems(problems) {
151
+ return `Invalid environment configuration:\n${problems.map((p) => ` ${p.name}: ${p.problem}`).join('\n')}`;
152
+ }
153
+ /**
154
+ * Validates `source` against `schema` without merging the base or exiting. Use for partial configs
155
+ * (scripts, tests) and wherever the caller handles the error.
156
+ *
157
+ * @param schema Any Zod schema over the env object.
158
+ * @param source Values to validate (default process.env).
159
+ * @param options Dotenv and deployed-environment checks.
160
+ * @returns The parsed, typed config.
161
+ * @throws ConfigError listing every missing or invalid variable by name (never its value).
162
+ * @example const env = parseEnv(databaseEnv, { DB_NAME: 'app_test' });
163
+ */
164
+ export function parseEnv(schema, source = process.env, options = {}) {
77
165
  if (options.dotenv !== false && source === process.env)
78
166
  loadDotEnv();
79
167
  const result = schema.safeParse(source);
80
- if (!result.success) {
81
- const lines = result.error.issues.map((i) => ` ${i.path.join('.')}: ${i.message}`).join('\n');
82
- throw new ConfigError(`Invalid environment configuration:\n${lines}`);
83
- }
168
+ const problems = [...(result.success ? [] : collectProblems(result.error.issues, source)), ...deployedProblems(source, options.deployedRequired ?? DEPLOYED_REQUIRED_ENV)];
169
+ const unique = [...new Map(problems.map((p) => [p.name, p])).values()];
170
+ if (unique.length || !result.success)
171
+ throw new ConfigError(formatEnvProblems(unique));
84
172
  return result.data;
85
173
  }
86
- /** Comma-separated env value → trimmed, non-empty list. */
174
+ /**
175
+ * Service startup config: merges the base env schema with the service's own, validates, and on
176
+ * failure prints the missing/invalid variable **names** (never values) and exits non-zero.
177
+ *
178
+ * @param schema The service's own variables (default none).
179
+ * @param options Source, base override and injectable write/exit for tests.
180
+ * @returns The parsed, typed config (base + service variables).
181
+ * @example
182
+ * export const config = loadEnv(z.object({ UPSTREAM_INTERNAL_URL: z.string().url() }));
183
+ */
184
+ export function loadEnv(schema, options = {}) {
185
+ const base = (options.base ?? baseEnv);
186
+ const merged = schema ? base.merge(schema) : base;
187
+ try {
188
+ return parseEnv(merged, options.source ?? process.env, options);
189
+ }
190
+ catch (error) {
191
+ if (!(error instanceof ConfigError))
192
+ throw error;
193
+ (options.write ?? ((text) => process.stderr.write(text)))(`${error.message}\n`);
194
+ return (options.exit ?? ((code) => process.exit(code)))(1);
195
+ }
196
+ }
197
+ /**
198
+ * Comma-separated env value → trimmed, non-empty list.
199
+ *
200
+ * @param value Raw env value.
201
+ * @returns The list (empty when unset).
202
+ * @example listFromEnv(' a, b ,,c') // ['a', 'b', 'c']
203
+ */
87
204
  export const listFromEnv = (value) => (value ?? '').split(',').map((s) => s.trim()).filter(Boolean);
88
205
  //# sourceMappingURL=index.js.map