grm-shared-library 1.1.237 → 1.1.238

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 (234) hide show
  1. package/README.md +369 -369
  2. package/dist/cjs/index.js +1 -0
  3. package/dist/cjs/index.js.map +1 -1
  4. package/dist/cjs/kafka/topics.js +11 -0
  5. package/dist/cjs/kafka/topics.js.map +1 -1
  6. package/dist/cjs/modules/dispatch/constants/response-unit-dispatch-defaults.const.js +1 -0
  7. package/dist/cjs/modules/dispatch/constants/response-unit-dispatch-defaults.const.js.map +1 -1
  8. package/dist/cjs/modules/dispatch/dtos/response-unit-dispatch-notification.dto.js +5 -0
  9. package/dist/cjs/modules/dispatch/dtos/response-unit-dispatch-notification.dto.js.map +1 -1
  10. package/dist/cjs/modules/dispatch/dtos/update-response-unit-dispatch-status.dto.js +1 -0
  11. package/dist/cjs/modules/dispatch/dtos/update-response-unit-dispatch-status.dto.js.map +1 -1
  12. package/dist/cjs/modules/dispatch/enums/response-unit-dispatch-status.enum.js +6 -0
  13. package/dist/cjs/modules/dispatch/enums/response-unit-dispatch-status.enum.js.map +1 -1
  14. package/dist/cjs/modules/dispatch/enums/response-unit-dispatch-transition-source.enum.js +3 -1
  15. package/dist/cjs/modules/dispatch/enums/response-unit-dispatch-transition-source.enum.js.map +1 -1
  16. package/dist/cjs/modules/dispatch/utils/response-unit-dispatch-status.util.js +6 -2
  17. package/dist/cjs/modules/dispatch/utils/response-unit-dispatch-status.util.js.map +1 -1
  18. package/dist/cjs/modules/incident/enums/incident-actions.enum.js +4 -0
  19. package/dist/cjs/modules/incident/enums/incident-actions.enum.js.map +1 -1
  20. package/dist/cjs/modules/incident-sla/constants/emergency-incident-workflow.const.js +53 -0
  21. package/dist/cjs/modules/incident-sla/constants/emergency-incident-workflow.const.js.map +1 -0
  22. package/dist/cjs/modules/incident-sla/constants/incident-sla-defaults.const.js +258 -0
  23. package/dist/cjs/modules/incident-sla/constants/incident-sla-defaults.const.js.map +1 -0
  24. package/dist/cjs/modules/incident-sla/constants/incident-sla-kafka.const.js +27 -0
  25. package/dist/cjs/modules/incident-sla/constants/incident-sla-kafka.const.js.map +1 -0
  26. package/dist/cjs/modules/incident-sla/dtos/emergency-notification.dto.js +419 -0
  27. package/dist/cjs/modules/incident-sla/dtos/emergency-notification.dto.js.map +1 -0
  28. package/dist/cjs/modules/incident-sla/dtos/emergency-operations.dto.js +155 -0
  29. package/dist/cjs/modules/incident-sla/dtos/emergency-operations.dto.js.map +1 -0
  30. package/dist/cjs/modules/incident-sla/dtos/emergency-workflow.dto.js +216 -0
  31. package/dist/cjs/modules/incident-sla/dtos/emergency-workflow.dto.js.map +1 -0
  32. package/dist/cjs/modules/incident-sla/dtos/incident-sla-analytics-query.dto.js +86 -0
  33. package/dist/cjs/modules/incident-sla/dtos/incident-sla-analytics-query.dto.js.map +1 -0
  34. package/dist/cjs/modules/incident-sla/dtos/incident-sla-policy.dto.js +509 -0
  35. package/dist/cjs/modules/incident-sla/dtos/incident-sla-policy.dto.js.map +1 -0
  36. package/dist/cjs/modules/incident-sla/enums/emergency-incident-stage.enum.js +32 -0
  37. package/dist/cjs/modules/incident-sla/enums/emergency-incident-stage.enum.js.map +1 -0
  38. package/dist/cjs/modules/incident-sla/enums/emergency-notification.enum.js +44 -0
  39. package/dist/cjs/modules/incident-sla/enums/emergency-notification.enum.js.map +1 -0
  40. package/dist/cjs/modules/incident-sla/enums/eta-status.enum.js +32 -0
  41. package/dist/cjs/modules/incident-sla/enums/eta-status.enum.js.map +1 -0
  42. package/dist/cjs/modules/incident-sla/enums/incident-closure.enum.js +23 -0
  43. package/dist/cjs/modules/incident-sla/enums/incident-closure.enum.js.map +1 -0
  44. package/dist/cjs/modules/incident-sla/enums/incident-sla-actor.enum.js +46 -0
  45. package/dist/cjs/modules/incident-sla/enums/incident-sla-actor.enum.js.map +1 -0
  46. package/dist/cjs/modules/incident-sla/enums/incident-sla-error-code.enum.js +26 -0
  47. package/dist/cjs/modules/incident-sla/enums/incident-sla-error-code.enum.js.map +1 -0
  48. package/dist/cjs/modules/incident-sla/enums/incident-sla-event-type.enum.js +45 -0
  49. package/dist/cjs/modules/incident-sla/enums/incident-sla-event-type.enum.js.map +1 -0
  50. package/dist/cjs/modules/incident-sla/enums/incident-sla-stage.enum.js +26 -0
  51. package/dist/cjs/modules/incident-sla/enums/incident-sla-stage.enum.js.map +1 -0
  52. package/dist/cjs/modules/incident-sla/enums/incident-sla-status.enum.js +38 -0
  53. package/dist/cjs/modules/incident-sla/enums/incident-sla-status.enum.js.map +1 -0
  54. package/dist/cjs/modules/incident-sla/enums/sla-escalation.enum.js +50 -0
  55. package/dist/cjs/modules/incident-sla/enums/sla-escalation.enum.js.map +1 -0
  56. package/dist/cjs/modules/incident-sla/enums/sla-exception.enum.js +27 -0
  57. package/dist/cjs/modules/incident-sla/enums/sla-exception.enum.js.map +1 -0
  58. package/dist/cjs/modules/incident-sla/enums/sla-overall-state.enum.js +28 -0
  59. package/dist/cjs/modules/incident-sla/enums/sla-overall-state.enum.js.map +1 -0
  60. package/dist/cjs/modules/incident-sla/enums/sla-pause.enum.js +28 -0
  61. package/dist/cjs/modules/incident-sla/enums/sla-pause.enum.js.map +1 -0
  62. package/dist/cjs/modules/incident-sla/enums/sla-policy-scope.enum.js +19 -0
  63. package/dist/cjs/modules/incident-sla/enums/sla-policy-scope.enum.js.map +1 -0
  64. package/dist/cjs/modules/incident-sla/index.js +53 -0
  65. package/dist/cjs/modules/incident-sla/index.js.map +1 -0
  66. package/dist/cjs/modules/incident-sla/interfaces/emergency-workflow.js +3 -0
  67. package/dist/cjs/modules/incident-sla/interfaces/emergency-workflow.js.map +1 -0
  68. package/dist/cjs/modules/incident-sla/interfaces/incident-sla-analytics.js +3 -0
  69. package/dist/cjs/modules/incident-sla/interfaces/incident-sla-analytics.js.map +1 -0
  70. package/dist/cjs/modules/incident-sla/interfaces/incident-sla-events.js +3 -0
  71. package/dist/cjs/modules/incident-sla/interfaces/incident-sla-events.js.map +1 -0
  72. package/dist/cjs/modules/incident-sla/interfaces/incident-sla-policy.js +3 -0
  73. package/dist/cjs/modules/incident-sla/interfaces/incident-sla-policy.js.map +1 -0
  74. package/dist/cjs/modules/incident-sla/interfaces/incident-sla-views.js +3 -0
  75. package/dist/cjs/modules/incident-sla/interfaces/incident-sla-views.js.map +1 -0
  76. package/dist/cjs/modules/incident-sla/utils/incident-sla-eta.util.js +40 -0
  77. package/dist/cjs/modules/incident-sla/utils/incident-sla-eta.util.js.map +1 -0
  78. package/dist/cjs/modules/incident-sla/utils/incident-sla-event-id.util.js +18 -0
  79. package/dist/cjs/modules/incident-sla/utils/incident-sla-event-id.util.js.map +1 -0
  80. package/dist/cjs/modules/incident-sla/utils/incident-sla-priority.util.js +31 -0
  81. package/dist/cjs/modules/incident-sla/utils/incident-sla-priority.util.js.map +1 -0
  82. package/dist/cjs/modules/incident-sla/utils/incident-sla-time.util.js +129 -0
  83. package/dist/cjs/modules/incident-sla/utils/incident-sla-time.util.js.map +1 -0
  84. package/dist/cjs/modules/permission/data/permissions.data.js +11 -0
  85. package/dist/cjs/modules/permission/data/permissions.data.js.map +1 -1
  86. package/dist/cjs/modules/permission/enums/permission-actions.enum.js +21 -0
  87. package/dist/cjs/modules/permission/enums/permission-actions.enum.js.map +1 -1
  88. package/dist/cjs/modules/permission/enums/permissions-module.enum.js +1 -0
  89. package/dist/cjs/modules/permission/enums/permissions-module.enum.js.map +1 -1
  90. package/dist/cjs/modules/role/data/roles.data.js +37 -0
  91. package/dist/cjs/modules/role/data/roles.data.js.map +1 -1
  92. package/dist/cjs/modules/sms/constants/sms-templates.config.js +27 -1
  93. package/dist/cjs/modules/sms/constants/sms-templates.config.js.map +1 -1
  94. package/dist/cjs/modules/sms/enums/sms-template.enum.js +2 -0
  95. package/dist/cjs/modules/sms/enums/sms-template.enum.js.map +1 -1
  96. package/dist/esm/index.js +1 -0
  97. package/dist/esm/index.js.map +1 -1
  98. package/dist/esm/kafka/topics.js +11 -0
  99. package/dist/esm/kafka/topics.js.map +1 -1
  100. package/dist/esm/modules/dispatch/constants/response-unit-dispatch-defaults.const.js +1 -0
  101. package/dist/esm/modules/dispatch/constants/response-unit-dispatch-defaults.const.js.map +1 -1
  102. package/dist/esm/modules/dispatch/dtos/response-unit-dispatch-notification.dto.js +5 -0
  103. package/dist/esm/modules/dispatch/dtos/response-unit-dispatch-notification.dto.js.map +1 -1
  104. package/dist/esm/modules/dispatch/dtos/update-response-unit-dispatch-status.dto.js +1 -0
  105. package/dist/esm/modules/dispatch/dtos/update-response-unit-dispatch-status.dto.js.map +1 -1
  106. package/dist/esm/modules/dispatch/enums/response-unit-dispatch-status.enum.js +6 -0
  107. package/dist/esm/modules/dispatch/enums/response-unit-dispatch-status.enum.js.map +1 -1
  108. package/dist/esm/modules/dispatch/enums/response-unit-dispatch-transition-source.enum.js +3 -1
  109. package/dist/esm/modules/dispatch/enums/response-unit-dispatch-transition-source.enum.js.map +1 -1
  110. package/dist/esm/modules/dispatch/utils/response-unit-dispatch-status.util.js +6 -2
  111. package/dist/esm/modules/dispatch/utils/response-unit-dispatch-status.util.js.map +1 -1
  112. package/dist/esm/modules/incident/enums/incident-actions.enum.js +4 -0
  113. package/dist/esm/modules/incident/enums/incident-actions.enum.js.map +1 -1
  114. package/dist/esm/modules/incident-sla/constants/emergency-incident-workflow.const.js +49 -0
  115. package/dist/esm/modules/incident-sla/constants/emergency-incident-workflow.const.js.map +1 -0
  116. package/dist/esm/modules/incident-sla/constants/incident-sla-defaults.const.js +255 -0
  117. package/dist/esm/modules/incident-sla/constants/incident-sla-defaults.const.js.map +1 -0
  118. package/dist/esm/modules/incident-sla/constants/incident-sla-kafka.const.js +24 -0
  119. package/dist/esm/modules/incident-sla/constants/incident-sla-kafka.const.js.map +1 -0
  120. package/dist/esm/modules/incident-sla/dtos/emergency-notification.dto.js +405 -0
  121. package/dist/esm/modules/incident-sla/dtos/emergency-notification.dto.js.map +1 -0
  122. package/dist/esm/modules/incident-sla/dtos/emergency-operations.dto.js +144 -0
  123. package/dist/esm/modules/incident-sla/dtos/emergency-operations.dto.js.map +1 -0
  124. package/dist/esm/modules/incident-sla/dtos/emergency-workflow.dto.js +206 -0
  125. package/dist/esm/modules/incident-sla/dtos/emergency-workflow.dto.js.map +1 -0
  126. package/dist/esm/modules/incident-sla/dtos/incident-sla-analytics-query.dto.js +82 -0
  127. package/dist/esm/modules/incident-sla/dtos/incident-sla-analytics-query.dto.js.map +1 -0
  128. package/dist/esm/modules/incident-sla/dtos/incident-sla-policy.dto.js +495 -0
  129. package/dist/esm/modules/incident-sla/dtos/incident-sla-policy.dto.js.map +1 -0
  130. package/dist/esm/modules/incident-sla/enums/emergency-incident-stage.enum.js +29 -0
  131. package/dist/esm/modules/incident-sla/enums/emergency-incident-stage.enum.js.map +1 -0
  132. package/dist/esm/modules/incident-sla/enums/emergency-notification.enum.js +41 -0
  133. package/dist/esm/modules/incident-sla/enums/emergency-notification.enum.js.map +1 -0
  134. package/dist/esm/modules/incident-sla/enums/eta-status.enum.js +29 -0
  135. package/dist/esm/modules/incident-sla/enums/eta-status.enum.js.map +1 -0
  136. package/dist/esm/modules/incident-sla/enums/incident-closure.enum.js +20 -0
  137. package/dist/esm/modules/incident-sla/enums/incident-closure.enum.js.map +1 -0
  138. package/dist/esm/modules/incident-sla/enums/incident-sla-actor.enum.js +43 -0
  139. package/dist/esm/modules/incident-sla/enums/incident-sla-actor.enum.js.map +1 -0
  140. package/dist/esm/modules/incident-sla/enums/incident-sla-error-code.enum.js +23 -0
  141. package/dist/esm/modules/incident-sla/enums/incident-sla-error-code.enum.js.map +1 -0
  142. package/dist/esm/modules/incident-sla/enums/incident-sla-event-type.enum.js +42 -0
  143. package/dist/esm/modules/incident-sla/enums/incident-sla-event-type.enum.js.map +1 -0
  144. package/dist/esm/modules/incident-sla/enums/incident-sla-stage.enum.js +23 -0
  145. package/dist/esm/modules/incident-sla/enums/incident-sla-stage.enum.js.map +1 -0
  146. package/dist/esm/modules/incident-sla/enums/incident-sla-status.enum.js +35 -0
  147. package/dist/esm/modules/incident-sla/enums/incident-sla-status.enum.js.map +1 -0
  148. package/dist/esm/modules/incident-sla/enums/sla-escalation.enum.js +47 -0
  149. package/dist/esm/modules/incident-sla/enums/sla-escalation.enum.js.map +1 -0
  150. package/dist/esm/modules/incident-sla/enums/sla-exception.enum.js +24 -0
  151. package/dist/esm/modules/incident-sla/enums/sla-exception.enum.js.map +1 -0
  152. package/dist/esm/modules/incident-sla/enums/sla-overall-state.enum.js +25 -0
  153. package/dist/esm/modules/incident-sla/enums/sla-overall-state.enum.js.map +1 -0
  154. package/dist/esm/modules/incident-sla/enums/sla-pause.enum.js +25 -0
  155. package/dist/esm/modules/incident-sla/enums/sla-pause.enum.js.map +1 -0
  156. package/dist/esm/modules/incident-sla/enums/sla-policy-scope.enum.js +16 -0
  157. package/dist/esm/modules/incident-sla/enums/sla-policy-scope.enum.js.map +1 -0
  158. package/dist/esm/modules/incident-sla/index.js +37 -0
  159. package/dist/esm/modules/incident-sla/index.js.map +1 -0
  160. package/dist/esm/modules/incident-sla/interfaces/emergency-workflow.js +2 -0
  161. package/dist/esm/modules/incident-sla/interfaces/emergency-workflow.js.map +1 -0
  162. package/dist/esm/modules/incident-sla/interfaces/incident-sla-analytics.js +2 -0
  163. package/dist/esm/modules/incident-sla/interfaces/incident-sla-analytics.js.map +1 -0
  164. package/dist/esm/modules/incident-sla/interfaces/incident-sla-events.js +2 -0
  165. package/dist/esm/modules/incident-sla/interfaces/incident-sla-events.js.map +1 -0
  166. package/dist/esm/modules/incident-sla/interfaces/incident-sla-policy.js +2 -0
  167. package/dist/esm/modules/incident-sla/interfaces/incident-sla-policy.js.map +1 -0
  168. package/dist/esm/modules/incident-sla/interfaces/incident-sla-views.js +2 -0
  169. package/dist/esm/modules/incident-sla/interfaces/incident-sla-views.js.map +1 -0
  170. package/dist/esm/modules/incident-sla/utils/incident-sla-eta.util.js +35 -0
  171. package/dist/esm/modules/incident-sla/utils/incident-sla-eta.util.js.map +1 -0
  172. package/dist/esm/modules/incident-sla/utils/incident-sla-event-id.util.js +14 -0
  173. package/dist/esm/modules/incident-sla/utils/incident-sla-event-id.util.js.map +1 -0
  174. package/dist/esm/modules/incident-sla/utils/incident-sla-priority.util.js +25 -0
  175. package/dist/esm/modules/incident-sla/utils/incident-sla-priority.util.js.map +1 -0
  176. package/dist/esm/modules/incident-sla/utils/incident-sla-time.util.js +119 -0
  177. package/dist/esm/modules/incident-sla/utils/incident-sla-time.util.js.map +1 -0
  178. package/dist/esm/modules/permission/data/permissions.data.js +11 -0
  179. package/dist/esm/modules/permission/data/permissions.data.js.map +1 -1
  180. package/dist/esm/modules/permission/enums/permission-actions.enum.js +21 -0
  181. package/dist/esm/modules/permission/enums/permission-actions.enum.js.map +1 -1
  182. package/dist/esm/modules/permission/enums/permissions-module.enum.js +1 -0
  183. package/dist/esm/modules/permission/enums/permissions-module.enum.js.map +1 -1
  184. package/dist/esm/modules/role/data/roles.data.js +37 -0
  185. package/dist/esm/modules/role/data/roles.data.js.map +1 -1
  186. package/dist/esm/modules/sms/constants/sms-templates.config.js +27 -1
  187. package/dist/esm/modules/sms/constants/sms-templates.config.js.map +1 -1
  188. package/dist/esm/modules/sms/enums/sms-template.enum.js +2 -0
  189. package/dist/esm/modules/sms/enums/sms-template.enum.js.map +1 -1
  190. package/dist/types/index.d.ts +1 -0
  191. package/dist/types/kafka/topics.d.ts +5 -0
  192. package/dist/types/modules/dispatch/dtos/response-unit-dispatch-notification.dto.d.ts +1 -0
  193. package/dist/types/modules/dispatch/enums/response-unit-dispatch-status.enum.d.ts +7 -1
  194. package/dist/types/modules/dispatch/enums/response-unit-dispatch-transition-source.enum.d.ts +4 -2
  195. package/dist/types/modules/dispatch/interfaces/incident-response-unit-dispatch.d.ts +5 -0
  196. package/dist/types/modules/dispatch/interfaces/response-unit-dispatch-events.d.ts +2 -0
  197. package/dist/types/modules/dispatch/utils/response-unit-dispatch-status.util.d.ts +1 -1
  198. package/dist/types/modules/incident/enums/incident-actions.enum.d.ts +4 -0
  199. package/dist/types/modules/incident-sla/constants/emergency-incident-workflow.const.d.ts +46 -0
  200. package/dist/types/modules/incident-sla/constants/incident-sla-defaults.const.d.ts +81 -0
  201. package/dist/types/modules/incident-sla/constants/incident-sla-kafka.const.d.ts +22 -0
  202. package/dist/types/modules/incident-sla/dtos/emergency-notification.dto.d.ts +99 -0
  203. package/dist/types/modules/incident-sla/dtos/emergency-operations.dto.d.ts +52 -0
  204. package/dist/types/modules/incident-sla/dtos/emergency-workflow.dto.d.ts +55 -0
  205. package/dist/types/modules/incident-sla/dtos/incident-sla-analytics-query.dto.d.ts +15 -0
  206. package/dist/types/modules/incident-sla/dtos/incident-sla-policy.dto.d.ts +104 -0
  207. package/dist/types/modules/incident-sla/enums/emergency-incident-stage.enum.d.ts +27 -0
  208. package/dist/types/modules/incident-sla/enums/emergency-notification.enum.d.ts +37 -0
  209. package/dist/types/modules/incident-sla/enums/eta-status.enum.d.ts +26 -0
  210. package/dist/types/modules/incident-sla/enums/incident-closure.enum.d.ts +17 -0
  211. package/dist/types/modules/incident-sla/enums/incident-sla-actor.enum.d.ts +40 -0
  212. package/dist/types/modules/incident-sla/enums/incident-sla-error-code.enum.d.ts +21 -0
  213. package/dist/types/modules/incident-sla/enums/incident-sla-event-type.enum.d.ts +36 -0
  214. package/dist/types/modules/incident-sla/enums/incident-sla-stage.enum.d.ts +21 -0
  215. package/dist/types/modules/incident-sla/enums/incident-sla-status.enum.d.ts +33 -0
  216. package/dist/types/modules/incident-sla/enums/sla-escalation.enum.d.ts +42 -0
  217. package/dist/types/modules/incident-sla/enums/sla-exception.enum.d.ts +21 -0
  218. package/dist/types/modules/incident-sla/enums/sla-overall-state.enum.d.ts +22 -0
  219. package/dist/types/modules/incident-sla/enums/sla-pause.enum.d.ts +22 -0
  220. package/dist/types/modules/incident-sla/enums/sla-policy-scope.enum.d.ts +14 -0
  221. package/dist/types/modules/incident-sla/index.d.ts +31 -0
  222. package/dist/types/modules/incident-sla/interfaces/emergency-workflow.d.ts +131 -0
  223. package/dist/types/modules/incident-sla/interfaces/incident-sla-analytics.d.ts +35 -0
  224. package/dist/types/modules/incident-sla/interfaces/incident-sla-events.d.ts +173 -0
  225. package/dist/types/modules/incident-sla/interfaces/incident-sla-policy.d.ts +147 -0
  226. package/dist/types/modules/incident-sla/interfaces/incident-sla-views.d.ts +224 -0
  227. package/dist/types/modules/incident-sla/utils/incident-sla-eta.util.d.ts +15 -0
  228. package/dist/types/modules/incident-sla/utils/incident-sla-event-id.util.d.ts +8 -0
  229. package/dist/types/modules/incident-sla/utils/incident-sla-priority.util.d.ts +14 -0
  230. package/dist/types/modules/incident-sla/utils/incident-sla-time.util.d.ts +65 -0
  231. package/dist/types/modules/permission/enums/permission-actions.enum.d.ts +6 -0
  232. package/dist/types/modules/permission/enums/permissions-module.enum.d.ts +2 -1
  233. package/dist/types/modules/sms/enums/sms-template.enum.d.ts +2 -0
  234. package/package.json +72 -72
package/README.md CHANGED
@@ -1,369 +1,369 @@
1
- # grm-shared-library
2
-
3
- Common code for GRM — shared types, enums, interfaces, DTOs and constants for the
4
- Clarion / Upesy backend NestJS microservices.
5
-
6
- ## Calendar dates
7
-
8
- One definition of what a date is, because there used to be four and they had
9
- drifted apart.
10
-
11
- ```ts
12
- import {
13
- ISO_CALENDAR_DATE_PATTERN, // the one regex - never write another
14
- isCalendarDate, // a real YYYY-MM-DD day; rejects 2026-02-30
15
- toCalendarDate, // Date | string -> YYYY-MM-DD | undefined
16
- isPlausibleBirthDate, // not future, within 120 years, NO minimum age
17
- calculateAge, isMinor, // whole years, UTC throughout
18
- AGE_OF_MAJORITY_YEARS, // 18 - for COPY ONLY, never a validator
19
- IsCalendarDate, IsBirthDate // class-validator decorators over the above
20
- } from 'grm-shared-library';
21
- ```
22
-
23
- ### Why it refuses things
24
-
25
- A calendar date has no time, no zone and no offset: somebody born on 17 April
26
- 1990 was born on that day in Nairobi, in Kampala and in London alike. Every bug
27
- this module exists to prevent comes from treating it as a moment.
28
-
29
- The old conversion did `new Date(freeText)` and then `.toISOString()`. Under the
30
- production pods` own `TZ=Africa/Nairobi`:
31
-
32
- | input | old result | |
33
- |---|---|---|
34
- | `04/17/1990` | `1990-04-16` | a day early |
35
- | `new Date(1990,3,17)` | `1990-04-16` | a day early - and this is what a date picker hands you |
36
- | `17/04/1990` | `undefined` | the East African spelling, refused |
37
- | `1990-04-17` | `1990-04-17` | correct |
38
-
39
- `toCalendarDate` now accepts exactly three things and refuses everything else:
40
-
41
- 1. `YYYY-MM-DD`
42
- 2. a full ISO-8601 instant in **UTC** (`1990-04-17T00:00:00.000Z`)
43
- 3. a `Date`, read in UTC - so build one with `Date.UTC`, never `new Date(y,m,d)`
44
-
45
- An **offset** timestamp is refused too: `1990-04-17T00:00:00+03:00` is 16 April
46
- in UTC, and only the sender knows which day they meant.
47
-
48
- Free text is refused rather than guessed at. `04/17/1990` and `17/04/1990` are
49
- the same eight characters in two orders, this platform serves Kenya, Uganda and
50
- Tanzania where the second is the common spelling, and there is no reading of
51
- that string that is safe to infer. A refused date surfaces as a field somebody
52
- is asked to collect; a drifted one is a permanently wrong birthday at a partner
53
- that cannot delete a member, on the record a paramedic uses to size a drug dose.
54
-
55
- ### No minimum age
56
-
57
- `isPlausibleBirthDate` refuses a future date and one beyond a human lifespan,
58
- and imposes no lower bound. Dependants on a family emergency-cover plan are
59
- children by design - that is the whole point of registering them with a
60
- responder - so a minimum-age rule would refuse exactly the people the feature
61
- exists for. `AGE_OF_MAJORITY_YEARS` is there for wording, not for gating.
62
-
63
- ### A decorator alone guarantees nothing
64
-
65
- `@IsCalendarDate` / `@IsBirthDate` are real validators, but on this platform
66
- they are not enforcement on their own: clarion-authentication registers **no**
67
- global `ValidationPipe`, the spreadsheet importer builds plain objects a pipe
68
- never sees, and internal service-to-service calls bypass HTTP entirely. Any
69
- service that OWNS a date must re-check it in the service layer, the way
70
- `UserService.updateCountry` re-checks `isSupportedCountryCode`.
71
-
72
- ### Postgres round trip
73
-
74
- A `type: date` column returns a **string** through this stack, not a `Date`:
75
- `pg` parses OID 1082 into a local-midnight `Date` and TypeORM reads it back with
76
- local components, so the two cancel out. It is correct, but correct by
77
- cancellation - never pass a value from a date column into `new Date()`.
78
-
79
- ---
80
-
81
- ## Country & Currency (multi-country foundation)
82
-
83
- Platform-level primitives for country-aware emergency response, newsfeed,
84
- notifications, billing, maps and admin. This is the shared source of truth so
85
- services stop hard-coding Kenya-specific values.
86
-
87
- **Supported countries** (ISO 3166-1 alpha-2):
88
-
89
- | Code | Country | Calling code | Currency | Symbol | Decimals | Timezone | Locale |
90
- |------|----------|--------------|----------|--------|----------|------------------------|---------|
91
- | `KE` | Kenya | `+254` | `KES` | `KSh` | 2 | `Africa/Nairobi` | `en-KE` |
92
- | `UG` | Uganda | `+256` | `UGX` | `USh` | 0 | `Africa/Kampala` | `en-UG` |
93
- | `TZ` | Tanzania | `+255` | `TZS` | `TSh` | 0 | `Africa/Dar_es_Salaam` | `en-TZ` |
94
-
95
- **Supported currencies** (ISO 4217): `KES`, `UGX`, `TZS`.
96
-
97
- ### Importing
98
-
99
- Everything is re-exported from the package root:
100
-
101
- ```ts
102
- import {
103
- CountryCode,
104
- CurrencyCode,
105
- CallingCode,
106
- SupportedCountryConfig,
107
- SupportedCurrencyConfig,
108
- SUPPORTED_COUNTRIES, // readonly SupportedCountryConfig[]
109
- SUPPORTED_COUNTRIES_BY_CODE, // Record<CountryCode, SupportedCountryConfig>
110
- SUPPORTED_CURRENCIES,
111
- SUPPORTED_CURRENCIES_BY_CODE,
112
- DEFAULT_COUNTRY_CODE, // CountryCode.KE
113
- DEFAULT_CURRENCY_CODE, // CurrencyCode.KES
114
- CountryScopeDto, // optional { countryCode?, currencyCode? } request scope
115
- getSupportedCountryConfig,
116
- getSupportedCurrencyConfig,
117
- getCurrencyForCountry,
118
- getCallingCodeForCountry,
119
- getCountryByCallingCode,
120
- isSupportedCountryCode,
121
- isSupportedCurrencyCode,
122
- } from 'grm-shared-library';
123
-
124
- getCurrencyForCountry(CountryCode.UG); // CurrencyCode.UGX
125
- getCallingCodeForCountry(CountryCode.TZ); // '+255'
126
- isSupportedCountryCode('US'); // false
127
- ```
128
-
129
- ### Backward compatibility
130
-
131
- **Kenya (`KE` / `KES`) is the default market.** Wherever a country or currency is
132
- not explicit, fall back to `DEFAULT_COUNTRY_CODE` / `DEFAULT_CURRENCY_CODE` /
133
- `DEFAULT_TIMEZONE` / `DEFAULT_LOCALE`. Existing Kenyan users, packages, posts,
134
- incidents, OTPs, notifications and billing flows are unaffected.
135
-
136
- ### Scope
137
-
138
- This package provides the country/currency **primitives only**. Country
139
- detection (GPS / IP / phone-number inference) and country-aware business logic
140
- (phone login, newsfeed filtering, multi-currency billing, SMS routing, map
141
- filtering) are delivered in later phases and should build on top of these types.
142
-
143
- ### Tests
144
-
145
- ```bash
146
- npm test # compiles src/modules/country and runs the country/currency suite
147
- ```
148
-
149
- ## Organization notification settings
150
-
151
- Clarion is multi-tenant, so what a customer sees in a notification belongs to the
152
- **organization** that serves them. `modules/notification` carries the contract for
153
- that, shared by clarion-notification (owner), the gateway (proxy) and
154
- clarion-authentication (emitter):
155
-
156
- - **`organizationId` is the key.** The four auth event payloads (`CreateOTPPayloadDto`,
157
- `CreateUserRegisteredPayloadDto`, `PasswordResetPayloadDto`,
158
- `AccountDeletionRequestPayloadDto`) gained an optional `organizationId`, and
159
- `EmailSendRequest` / `SmsSendRequest` carry one. `appId` stays on every payload, but
160
- only as the fallback for an emitter that has not been upgraded. The notification
161
- service validates with `whitelist: true`, so it must be on a version that declares
162
- the field or the field is stripped before anything reads it.
163
- - **Branding resolves field by field** - `NotificationBrandingSource`: the
164
- organization's profile, then its record in clarion-organization, then the legacy
165
- per-app defaults, then the platform. `UpsertOrganizationNotificationBrandingDto`
166
- turns `""` into `null`, and null means *inherit again*, not *blank*.
167
- - **Email senders are verified per provider** (`EmailSenderProviderVerification`). A
168
- provider only sends from an address it has `VERIFIED`; otherwise that provider uses
169
- the platform sender and keeps the organization's display name and reply-to.
170
- - **SMS senders are requested, then approved** (`NotificationSenderStatus`). Only
171
- `ACTIVE` senders are used; everything else falls back to the country-aware default.
172
- `validateSmsSenderId(type, value)` is the one definition of a valid sender.
173
- - **Providers describe themselves** (`SmsProviderDescriptor`), like tracking providers
174
- do. `SmsProviderName` only reserves a code; the catalogue says what is usable.
175
- - Every send result records `sender` and `senderSource`
176
- (`ORGANIZATION` | `PLATFORM_DEFAULT`), so "why was this Upesy-branded?" is a query.
177
-
178
- ### clarion-billing → clarion-notification (HTTP)
179
-
180
- `BillingEmailNotificationDto`, `BillingSmsNotificationDto`, `BillingPushNotificationDto`
181
- and their result types are the contract for `POST /billing/notifications/email | sms |
182
- push` — the synchronous path clarion-billing uses when it wants to know a message left.
183
- clarion-billing's `NotificationClientService` types its requests from these; do not
184
- redeclare the fields there.
185
-
186
- - `BillingEmailTemplate` is a **closed set**. The route renders a template by name on
187
- another service's say-so, so an open string would let a caller render any template.
188
- A new value needs copy defaults in clarion-notification (`api/billing`).
189
- - Attachments go **by reference** (`documents[].signedUrl`), because billing PDFs are
190
- private and their signed URLs expire within the hour. `signedUrl` must be `https`,
191
- file names must be bare names, and a subject may not contain a line break.
192
- - Every list and string is bounded; `billing-notification.spec.ts` pins the limits.
193
-
194
- Values that end up inside email CSS or headers are validated as such: colours are
195
- `#RRGGBB` only, logos are `https` only, and a sender display name may not contain a
196
- line break. `notification-sender.spec.ts` pins each of those.
197
-
198
- ## Response-unit dispatch (`modules/dispatch`)
199
-
200
- The contracts of response-unit recommendation and dispatch (design: clarion-incident
201
- `docs/RESPONSE_UNIT_DISPATCH.md`).
202
-
203
- - **Capabilities** (`modules/response-unit`): `ResponseCapability` and
204
- `effectiveResponseCapabilities` - configured on the unit, else implied by its type (an
205
- ambulance is MEDICAL_RESPONSE). `resolveResponseUnitAvailability` reads the manual status.
206
- - **Recommendation**: `ResponseUnitRecommendation` / `ResponseUnitCandidate` - facts and
207
- reasons, never scores; `matchResponseCapabilities`, `resolveResponseCapabilityRequirement`
208
- (type's list, else category's), `resolveIncidentDispatchBlockers`.
209
- - **Dispatch**: `IncidentResponseUnitDispatch`, `DispatchResponseUnitDto`,
210
- `UpdateResponseUnitDispatchStatusDto`, the state machine (`RESPONSE_UNIT_DISPATCH_TRANSITIONS`,
211
- `ACTIVE_RESPONSE_UNIT_DISPATCH_STATUSES` - which MUST equal the statuses in clarion-incident's
212
- `UQ_ru_dispatch_active_unit` index), `ResponseUnitDispatchErrorCode`.
213
- - **Cross-service reads**: `ResponseUnitDispatchProfile` (clarion-organization's catalogue),
214
- `ResponseUnitNearbyResult` (clarion-response-tracking's GEO search).
215
- - **Events**: `RESPONSE_UNIT_DISPATCH_KAFKA_TOPICS` - created / status-changed (identifiers
216
- only), notification-requested (the one message with a phone number) and notification-result;
217
- class DTOs for the two notification events so consumers can validate with `whitelist: true`.
218
- - **Permissions**: the `Response Unit Dispatch` module - `read|create|update:response-unit-dispatch`
219
- (never `update:incident`, which every app user holds).
220
- - **SMS**: the `RESPONSE_UNIT_DISPATCH` template.
221
-
222
- ## Roles and permissions
223
-
224
- This package is the authority on both. clarion-authentication seeds them into its
225
- database on boot; every other service reads them from here.
226
-
227
- | File | Holds |
228
- |---|---|
229
- | `role/enums/role.enum.ts` | `Roles` - the role names. |
230
- | `role/constants/roles-by-scope.ts` | `ORGANIZATION_ROLES` / `CONTROL_CENTRE_ROLES` / `MOBILE_ROLES` - each role's **scope**. |
231
- | `role/data/roles.data.ts` | `ROLES_DATA` - the permissions each role is seeded with. |
232
- | `permission/enums/permission-actions.enum.ts` | `PermissionActions` - every action, as `verb:resource`. |
233
- | `permission/data/permissions.data.ts` | `PERMISSIONS_DATA` - the catalogue role management shows, grouped by `PermissionsModule`. |
234
-
235
- **WHAT a role may do and WHERE it may do it are separate.** Permissions answer the
236
- first and are checked by the gateway's `PermissionGuard` (ANY-of). Scope answers the
237
- second and comes only from which list in `roles-by-scope.ts` the role is in: the
238
- gateway (`AccessScopeService`), clarion-authentication (`AuthorizationService`) and
239
- clarion-socket (tracking rooms) all read those lists, and downstream services confine
240
- a caller to the scope the gateway built. A role in no list gets no scope at all and
241
- every tenant-scoped call it makes fails closed.
242
-
243
- That is how one job exists at two scopes. `Organization:Response-Units-Admin` and
244
- `Control-Centre:Response-Units-Admin` are seeded from the **same** permission list
245
- (`RESPONSE_UNITS_ADMIN_PERMISSIONS`) - all of the `Response Unit` and `Tracking`
246
- modules, plus `read:control-centre` and `read:organization`, which their own screens
247
- call - and differ only in the list they are in.
248
-
249
- ### The Tracking permission module
250
-
251
- clarion-response-tracking's routes used to be guarded by the `Response Unit` actions.
252
- Every mobile user holds `read:response-unit`, so every app user could read their
253
- organization's tracking provider connections, and nobody could be given tracking
254
- without being given the response units. They now have their own:
255
-
256
- | Action | Covers |
257
- |---|---|
258
- | `create` / `read` / `update` / `delete:tracking-provider-connection` | The link to a tracking platform (Telox, Wialon). `update` also covers test, enable, disable, reconnect. |
259
- | `create` / `read` / `update` / `delete:tracking-device-binding` | Which provider unit is which response unit. `read` also covers the unmapped-units worklist; `update`, resolving / reopening an entry on it. |
260
- | `read:tracking-position` | Where units are now - the incident map and live tracking. |
261
- | `read:tracking-history` | Where a unit has been. Separate on purpose: a movement history is a record of somebody's working day. |
262
- | `read:tracking-operations` | The tracking operations dashboard. |
263
- | `manage:tracking-operations` | Reconciling the tracking service's copy of the response units (the dry run included). |
264
-
265
- Who holds them: Owner, Admin, `Control-Centre:Admin` and both Response-Units-Admins -
266
- all twelve (everybody who could administer tracking before the split still can);
267
- `Organization:User` - the five `read` actions; `Control-Centre:User` and `Responder` -
268
- position and history; `Mobile-User` - **none**.
269
-
270
- ### The Notification permission module
271
-
272
- An organization's notification settings (clarion-notification) decide what its
273
- **customers** see: how emails are branded, the address they come from, and the sender
274
- ID its SMS arrive under. They have actions of their own rather than reusing
275
- `update:organization`, because a wrong logo or sender goes out on every OTP and alert.
276
-
277
- | Action | Covers |
278
- |---|---|
279
- | `read:notification-settings` | Reading the settings, the effective (resolved) branding and the SMS provider catalogue. |
280
- | `manage:notification-settings` | Saving branding; adding, verifying and removing email senders; **requesting** SMS senders; sending a test email. |
281
- | `approve:notification-sender` | Moving an SMS sender to `ACTIVE` / `SUSPENDED` / `REJECTED`. |
282
-
283
- Owner and Admin hold the first two. **No role holds `approve:notification-sender`** -
284
- only Super-Admin (`*`) passes it, and `roles.spec.ts` pins that. Activating a sender
285
- asserts that it is registered with the provider and the telco; an organization able to
286
- approve its own could send SMS under any name it typed.
287
-
288
- ### The Billing permission module and Organization:Billing-Admin
289
-
290
- clarion-billing's routes used to be guarded by the INCIDENT actions (and 45 of the
291
- gateway's 116 billing routes by nothing but a sign-in). Every app user holds
292
- `create/read/update:incident` to raise an SOS, so every handset could reach almost
293
- every billing management route - approving a subscription without payment and
294
- granting premium trials among them. They now use the Billing module, whose actions
295
- come in two kinds:
296
-
297
- | Kind | Actions | Who holds them |
298
- |---|---|---|
299
- | **Organization** - an organization's own billing, scoped by clarion-billing | packages, individual and corporate clients, corporate members, beneficiaries, subscriptions (incl. manual approval, cancel, `renew:subscription`), orders, invoices (incl. `mark:invoice-paid`), receipts, payments, `read:settlement`, `read:billing-config` | Owner, Admin and `Organization:Billing-Admin` all 44; `Organization:User` the 13 reads |
300
- | **Platform** - the platform's own business, or not yet scoped to one organization | billing config writes (revenue share and tax terms), settlement create / send-invoice / mark-paid, settlement batches, `read:billing-operations`, premium trials, `read:entitlement` | **no role** - only Super-Admin (`*`) |
301
-
302
- `roles.spec.ts` lists the platform actions by hand and fails on any Billing action
303
- that is in neither list, so a new one has to be placed deliberately.
304
-
305
- `Organization:Billing-Admin` holds the organization set plus exactly what its screens
306
- need from other modules: `read:user` (an individual client is a platform user; the
307
- client pages fetch the name and phone), `read:incident-category-type` (the package
308
- form lists the incident types a package covers) and `read:organization`. No partner
309
- permission: billing asks clarion-partner-services service-to-service, and no billing
310
- screen calls a partner route.
311
-
312
- `Mobile-User` holds the three billing actions the app uses -
313
- `initialize:individual-subscription`, `renew:subscription`, `verify:payment`. The
314
- storefront (`GET /billing/mobile/packages`) and the self-scoped "me" routes need only
315
- a sign-in; `read:package` is withheld because it would also open the admin's package
316
- list, which returns every organization's packages to a caller with no organization
317
- scope. Control-room roles hold no billing action.
318
-
319
- `BILLING_MANAGER_ROLES` (Owner, Admin, Billing-Admin) and `BILLING_VIEWER_ROLES`
320
- (+ Organization:User) are for the places that still decide by role NAME -
321
- clarion-billing's checks on manual approval, marking an invoice paid and renewing on
322
- a subscriber's behalf, and the admin's billing menu and buttons. The spec asserts each
323
- manager holds the organization set in full and each viewer every read.
324
-
325
- ### The Newsfeed permission module and Organization:Newsfeed-Admin
326
-
327
- clarion-newsfeed's admin routes were guarded by the ORGANIZATION actions and its
328
- member routes by nothing but a sign-in. `read:organization` is held by every app
329
- user, so any of them could list their organization's drafts; only the Owner held
330
- `delete:organization`, so an Admin could not delete a post; and clarion-newsfeed let
331
- any organization-scoped role moderate comments. The module now has two audiences:
332
-
333
- | Actions | Audience | Held by |
334
- |---|---|---|
335
- | `create`/`read`/`update`/`publish`/`archive`/`delete:newsfeed-post`, `moderate:newsfeed-comment` | STAFF running the posts (`read:newsfeed-post` includes drafts and archived posts) | Owner, Admin, Newsfeed-Admin; Organization:User only `read:newsfeed-post` |
336
- | `read:newsfeed-feed` (the published feed, a post, its comments), `engage:newsfeed-post` (like, comment, share, change one's own comment) | MEMBERS | Mobile-User, Control-Centre Admin/User, Responder (who could read the feed in the app before), plus the staff above (Organization:User reads only) |
337
-
338
- `Organization:Newsfeed-Admin` holds the module in full and nothing else - its screens
339
- call no other module. `NEWSFEED_MANAGER_ROLES` / `NEWSFEED_VIEWER_ROLES` are the
340
- role-NAME lists clarion-newsfeed checks for managing/moderating and for seeing drafts.
341
-
342
- ### Adding a role: every step fails silently on its own
343
-
344
- 1. **Here** - the `Roles` value, a scope list, and a `ROLES_DATA` entry. `roles.spec.ts`
345
- fails if a scoped role is unseeded, a seeded role is not in the enum, or a role
346
- holds an action the catalogue does not list.
347
-
348
- The same spec holds the rule for **actions**: every `PermissionActions` value must
349
- be in `PERMISSIONS_DATA`, under exactly one module. An action outside the catalogue
350
- still *works* - the gateway matches on the string - but role management can neither
351
- show nor assign it, which is how the four `zone` actions went unnoticed for so long
352
- (they now have a `Zone` module). Role and module names are also pinned to 50
353
- characters, the width of the columns clarion-authentication stores them in.
354
- 2. **clarion-authentication** - `users.roles` is a Postgres ENUM array, so the label
355
- needs an `ALTER TYPE "users_roles_enum" ADD VALUE IF NOT EXISTS` migration. Without
356
- it the role is seeded, offered by the admin, and rejected at the INSERT.
357
- 3. **Every service that reads the role lists** - clarion-api-gateway,
358
- clarion-authentication, clarion-socket - must INSTALL the new version (the lockfile
359
- decides, not the caret) and redeploy. The gateway also validates `roles` with
360
- `@IsEnum(Roles)`.
361
- 4. **clarion-socket** names the organization roles admitted to tracking rooms one by
362
- one; an organization role that should see the fleet has to be added there.
363
- 5. **clarion-admin** shows menu entries by role NAME (`requiresAnyRole` in
364
- `sidebar-data.ts`), from `clarion-shared-types`' mirror of `Roles`.
365
-
366
- Deploy clarion-authentication first and let it reseed. The gateway caches role
367
- permissions for five minutes, so a gateway that starts REQUIRING a new action should
368
- go out after that window - or the roles that were just granted it are refused until
369
- their cache entry expires.
1
+ # grm-shared-library
2
+
3
+ Common code for GRM — shared types, enums, interfaces, DTOs and constants for the
4
+ Clarion / Upesy backend NestJS microservices.
5
+
6
+ ## Calendar dates
7
+
8
+ One definition of what a date is, because there used to be four and they had
9
+ drifted apart.
10
+
11
+ ```ts
12
+ import {
13
+ ISO_CALENDAR_DATE_PATTERN, // the one regex - never write another
14
+ isCalendarDate, // a real YYYY-MM-DD day; rejects 2026-02-30
15
+ toCalendarDate, // Date | string -> YYYY-MM-DD | undefined
16
+ isPlausibleBirthDate, // not future, within 120 years, NO minimum age
17
+ calculateAge, isMinor, // whole years, UTC throughout
18
+ AGE_OF_MAJORITY_YEARS, // 18 - for COPY ONLY, never a validator
19
+ IsCalendarDate, IsBirthDate // class-validator decorators over the above
20
+ } from 'grm-shared-library';
21
+ ```
22
+
23
+ ### Why it refuses things
24
+
25
+ A calendar date has no time, no zone and no offset: somebody born on 17 April
26
+ 1990 was born on that day in Nairobi, in Kampala and in London alike. Every bug
27
+ this module exists to prevent comes from treating it as a moment.
28
+
29
+ The old conversion did `new Date(freeText)` and then `.toISOString()`. Under the
30
+ production pods` own `TZ=Africa/Nairobi`:
31
+
32
+ | input | old result | |
33
+ |---|---|---|
34
+ | `04/17/1990` | `1990-04-16` | a day early |
35
+ | `new Date(1990,3,17)` | `1990-04-16` | a day early - and this is what a date picker hands you |
36
+ | `17/04/1990` | `undefined` | the East African spelling, refused |
37
+ | `1990-04-17` | `1990-04-17` | correct |
38
+
39
+ `toCalendarDate` now accepts exactly three things and refuses everything else:
40
+
41
+ 1. `YYYY-MM-DD`
42
+ 2. a full ISO-8601 instant in **UTC** (`1990-04-17T00:00:00.000Z`)
43
+ 3. a `Date`, read in UTC - so build one with `Date.UTC`, never `new Date(y,m,d)`
44
+
45
+ An **offset** timestamp is refused too: `1990-04-17T00:00:00+03:00` is 16 April
46
+ in UTC, and only the sender knows which day they meant.
47
+
48
+ Free text is refused rather than guessed at. `04/17/1990` and `17/04/1990` are
49
+ the same eight characters in two orders, this platform serves Kenya, Uganda and
50
+ Tanzania where the second is the common spelling, and there is no reading of
51
+ that string that is safe to infer. A refused date surfaces as a field somebody
52
+ is asked to collect; a drifted one is a permanently wrong birthday at a partner
53
+ that cannot delete a member, on the record a paramedic uses to size a drug dose.
54
+
55
+ ### No minimum age
56
+
57
+ `isPlausibleBirthDate` refuses a future date and one beyond a human lifespan,
58
+ and imposes no lower bound. Dependants on a family emergency-cover plan are
59
+ children by design - that is the whole point of registering them with a
60
+ responder - so a minimum-age rule would refuse exactly the people the feature
61
+ exists for. `AGE_OF_MAJORITY_YEARS` is there for wording, not for gating.
62
+
63
+ ### A decorator alone guarantees nothing
64
+
65
+ `@IsCalendarDate` / `@IsBirthDate` are real validators, but on this platform
66
+ they are not enforcement on their own: clarion-authentication registers **no**
67
+ global `ValidationPipe`, the spreadsheet importer builds plain objects a pipe
68
+ never sees, and internal service-to-service calls bypass HTTP entirely. Any
69
+ service that OWNS a date must re-check it in the service layer, the way
70
+ `UserService.updateCountry` re-checks `isSupportedCountryCode`.
71
+
72
+ ### Postgres round trip
73
+
74
+ A `type: date` column returns a **string** through this stack, not a `Date`:
75
+ `pg` parses OID 1082 into a local-midnight `Date` and TypeORM reads it back with
76
+ local components, so the two cancel out. It is correct, but correct by
77
+ cancellation - never pass a value from a date column into `new Date()`.
78
+
79
+ ---
80
+
81
+ ## Country & Currency (multi-country foundation)
82
+
83
+ Platform-level primitives for country-aware emergency response, newsfeed,
84
+ notifications, billing, maps and admin. This is the shared source of truth so
85
+ services stop hard-coding Kenya-specific values.
86
+
87
+ **Supported countries** (ISO 3166-1 alpha-2):
88
+
89
+ | Code | Country | Calling code | Currency | Symbol | Decimals | Timezone | Locale |
90
+ |------|----------|--------------|----------|--------|----------|------------------------|---------|
91
+ | `KE` | Kenya | `+254` | `KES` | `KSh` | 2 | `Africa/Nairobi` | `en-KE` |
92
+ | `UG` | Uganda | `+256` | `UGX` | `USh` | 0 | `Africa/Kampala` | `en-UG` |
93
+ | `TZ` | Tanzania | `+255` | `TZS` | `TSh` | 0 | `Africa/Dar_es_Salaam` | `en-TZ` |
94
+
95
+ **Supported currencies** (ISO 4217): `KES`, `UGX`, `TZS`.
96
+
97
+ ### Importing
98
+
99
+ Everything is re-exported from the package root:
100
+
101
+ ```ts
102
+ import {
103
+ CountryCode,
104
+ CurrencyCode,
105
+ CallingCode,
106
+ SupportedCountryConfig,
107
+ SupportedCurrencyConfig,
108
+ SUPPORTED_COUNTRIES, // readonly SupportedCountryConfig[]
109
+ SUPPORTED_COUNTRIES_BY_CODE, // Record<CountryCode, SupportedCountryConfig>
110
+ SUPPORTED_CURRENCIES,
111
+ SUPPORTED_CURRENCIES_BY_CODE,
112
+ DEFAULT_COUNTRY_CODE, // CountryCode.KE
113
+ DEFAULT_CURRENCY_CODE, // CurrencyCode.KES
114
+ CountryScopeDto, // optional { countryCode?, currencyCode? } request scope
115
+ getSupportedCountryConfig,
116
+ getSupportedCurrencyConfig,
117
+ getCurrencyForCountry,
118
+ getCallingCodeForCountry,
119
+ getCountryByCallingCode,
120
+ isSupportedCountryCode,
121
+ isSupportedCurrencyCode,
122
+ } from 'grm-shared-library';
123
+
124
+ getCurrencyForCountry(CountryCode.UG); // CurrencyCode.UGX
125
+ getCallingCodeForCountry(CountryCode.TZ); // '+255'
126
+ isSupportedCountryCode('US'); // false
127
+ ```
128
+
129
+ ### Backward compatibility
130
+
131
+ **Kenya (`KE` / `KES`) is the default market.** Wherever a country or currency is
132
+ not explicit, fall back to `DEFAULT_COUNTRY_CODE` / `DEFAULT_CURRENCY_CODE` /
133
+ `DEFAULT_TIMEZONE` / `DEFAULT_LOCALE`. Existing Kenyan users, packages, posts,
134
+ incidents, OTPs, notifications and billing flows are unaffected.
135
+
136
+ ### Scope
137
+
138
+ This package provides the country/currency **primitives only**. Country
139
+ detection (GPS / IP / phone-number inference) and country-aware business logic
140
+ (phone login, newsfeed filtering, multi-currency billing, SMS routing, map
141
+ filtering) are delivered in later phases and should build on top of these types.
142
+
143
+ ### Tests
144
+
145
+ ```bash
146
+ npm test # compiles src/modules/country and runs the country/currency suite
147
+ ```
148
+
149
+ ## Organization notification settings
150
+
151
+ Clarion is multi-tenant, so what a customer sees in a notification belongs to the
152
+ **organization** that serves them. `modules/notification` carries the contract for
153
+ that, shared by clarion-notification (owner), the gateway (proxy) and
154
+ clarion-authentication (emitter):
155
+
156
+ - **`organizationId` is the key.** The four auth event payloads (`CreateOTPPayloadDto`,
157
+ `CreateUserRegisteredPayloadDto`, `PasswordResetPayloadDto`,
158
+ `AccountDeletionRequestPayloadDto`) gained an optional `organizationId`, and
159
+ `EmailSendRequest` / `SmsSendRequest` carry one. `appId` stays on every payload, but
160
+ only as the fallback for an emitter that has not been upgraded. The notification
161
+ service validates with `whitelist: true`, so it must be on a version that declares
162
+ the field or the field is stripped before anything reads it.
163
+ - **Branding resolves field by field** - `NotificationBrandingSource`: the
164
+ organization's profile, then its record in clarion-organization, then the legacy
165
+ per-app defaults, then the platform. `UpsertOrganizationNotificationBrandingDto`
166
+ turns `""` into `null`, and null means *inherit again*, not *blank*.
167
+ - **Email senders are verified per provider** (`EmailSenderProviderVerification`). A
168
+ provider only sends from an address it has `VERIFIED`; otherwise that provider uses
169
+ the platform sender and keeps the organization's display name and reply-to.
170
+ - **SMS senders are requested, then approved** (`NotificationSenderStatus`). Only
171
+ `ACTIVE` senders are used; everything else falls back to the country-aware default.
172
+ `validateSmsSenderId(type, value)` is the one definition of a valid sender.
173
+ - **Providers describe themselves** (`SmsProviderDescriptor`), like tracking providers
174
+ do. `SmsProviderName` only reserves a code; the catalogue says what is usable.
175
+ - Every send result records `sender` and `senderSource`
176
+ (`ORGANIZATION` | `PLATFORM_DEFAULT`), so "why was this Upesy-branded?" is a query.
177
+
178
+ ### clarion-billing → clarion-notification (HTTP)
179
+
180
+ `BillingEmailNotificationDto`, `BillingSmsNotificationDto`, `BillingPushNotificationDto`
181
+ and their result types are the contract for `POST /billing/notifications/email | sms |
182
+ push` — the synchronous path clarion-billing uses when it wants to know a message left.
183
+ clarion-billing's `NotificationClientService` types its requests from these; do not
184
+ redeclare the fields there.
185
+
186
+ - `BillingEmailTemplate` is a **closed set**. The route renders a template by name on
187
+ another service's say-so, so an open string would let a caller render any template.
188
+ A new value needs copy defaults in clarion-notification (`api/billing`).
189
+ - Attachments go **by reference** (`documents[].signedUrl`), because billing PDFs are
190
+ private and their signed URLs expire within the hour. `signedUrl` must be `https`,
191
+ file names must be bare names, and a subject may not contain a line break.
192
+ - Every list and string is bounded; `billing-notification.spec.ts` pins the limits.
193
+
194
+ Values that end up inside email CSS or headers are validated as such: colours are
195
+ `#RRGGBB` only, logos are `https` only, and a sender display name may not contain a
196
+ line break. `notification-sender.spec.ts` pins each of those.
197
+
198
+ ## Response-unit dispatch (`modules/dispatch`)
199
+
200
+ The contracts of response-unit recommendation and dispatch (design: clarion-incident
201
+ `docs/RESPONSE_UNIT_DISPATCH.md`).
202
+
203
+ - **Capabilities** (`modules/response-unit`): `ResponseCapability` and
204
+ `effectiveResponseCapabilities` - configured on the unit, else implied by its type (an
205
+ ambulance is MEDICAL_RESPONSE). `resolveResponseUnitAvailability` reads the manual status.
206
+ - **Recommendation**: `ResponseUnitRecommendation` / `ResponseUnitCandidate` - facts and
207
+ reasons, never scores; `matchResponseCapabilities`, `resolveResponseCapabilityRequirement`
208
+ (type's list, else category's), `resolveIncidentDispatchBlockers`.
209
+ - **Dispatch**: `IncidentResponseUnitDispatch`, `DispatchResponseUnitDto`,
210
+ `UpdateResponseUnitDispatchStatusDto`, the state machine (`RESPONSE_UNIT_DISPATCH_TRANSITIONS`,
211
+ `ACTIVE_RESPONSE_UNIT_DISPATCH_STATUSES` - which MUST equal the statuses in clarion-incident's
212
+ `UQ_ru_dispatch_active_unit` index), `ResponseUnitDispatchErrorCode`.
213
+ - **Cross-service reads**: `ResponseUnitDispatchProfile` (clarion-organization's catalogue),
214
+ `ResponseUnitNearbyResult` (clarion-response-tracking's GEO search).
215
+ - **Events**: `RESPONSE_UNIT_DISPATCH_KAFKA_TOPICS` - created / status-changed (identifiers
216
+ only), notification-requested (the one message with a phone number) and notification-result;
217
+ class DTOs for the two notification events so consumers can validate with `whitelist: true`.
218
+ - **Permissions**: the `Response Unit Dispatch` module - `read|create|update:response-unit-dispatch`
219
+ (never `update:incident`, which every app user holds).
220
+ - **SMS**: the `RESPONSE_UNIT_DISPATCH` template.
221
+
222
+ ## Roles and permissions
223
+
224
+ This package is the authority on both. clarion-authentication seeds them into its
225
+ database on boot; every other service reads them from here.
226
+
227
+ | File | Holds |
228
+ |---|---|
229
+ | `role/enums/role.enum.ts` | `Roles` - the role names. |
230
+ | `role/constants/roles-by-scope.ts` | `ORGANIZATION_ROLES` / `CONTROL_CENTRE_ROLES` / `MOBILE_ROLES` - each role's **scope**. |
231
+ | `role/data/roles.data.ts` | `ROLES_DATA` - the permissions each role is seeded with. |
232
+ | `permission/enums/permission-actions.enum.ts` | `PermissionActions` - every action, as `verb:resource`. |
233
+ | `permission/data/permissions.data.ts` | `PERMISSIONS_DATA` - the catalogue role management shows, grouped by `PermissionsModule`. |
234
+
235
+ **WHAT a role may do and WHERE it may do it are separate.** Permissions answer the
236
+ first and are checked by the gateway's `PermissionGuard` (ANY-of). Scope answers the
237
+ second and comes only from which list in `roles-by-scope.ts` the role is in: the
238
+ gateway (`AccessScopeService`), clarion-authentication (`AuthorizationService`) and
239
+ clarion-socket (tracking rooms) all read those lists, and downstream services confine
240
+ a caller to the scope the gateway built. A role in no list gets no scope at all and
241
+ every tenant-scoped call it makes fails closed.
242
+
243
+ That is how one job exists at two scopes. `Organization:Response-Units-Admin` and
244
+ `Control-Centre:Response-Units-Admin` are seeded from the **same** permission list
245
+ (`RESPONSE_UNITS_ADMIN_PERMISSIONS`) - all of the `Response Unit` and `Tracking`
246
+ modules, plus `read:control-centre` and `read:organization`, which their own screens
247
+ call - and differ only in the list they are in.
248
+
249
+ ### The Tracking permission module
250
+
251
+ clarion-response-tracking's routes used to be guarded by the `Response Unit` actions.
252
+ Every mobile user holds `read:response-unit`, so every app user could read their
253
+ organization's tracking provider connections, and nobody could be given tracking
254
+ without being given the response units. They now have their own:
255
+
256
+ | Action | Covers |
257
+ |---|---|
258
+ | `create` / `read` / `update` / `delete:tracking-provider-connection` | The link to a tracking platform (Telox, Wialon). `update` also covers test, enable, disable, reconnect. |
259
+ | `create` / `read` / `update` / `delete:tracking-device-binding` | Which provider unit is which response unit. `read` also covers the unmapped-units worklist; `update`, resolving / reopening an entry on it. |
260
+ | `read:tracking-position` | Where units are now - the incident map and live tracking. |
261
+ | `read:tracking-history` | Where a unit has been. Separate on purpose: a movement history is a record of somebody's working day. |
262
+ | `read:tracking-operations` | The tracking operations dashboard. |
263
+ | `manage:tracking-operations` | Reconciling the tracking service's copy of the response units (the dry run included). |
264
+
265
+ Who holds them: Owner, Admin, `Control-Centre:Admin` and both Response-Units-Admins -
266
+ all twelve (everybody who could administer tracking before the split still can);
267
+ `Organization:User` - the five `read` actions; `Control-Centre:User` and `Responder` -
268
+ position and history; `Mobile-User` - **none**.
269
+
270
+ ### The Notification permission module
271
+
272
+ An organization's notification settings (clarion-notification) decide what its
273
+ **customers** see: how emails are branded, the address they come from, and the sender
274
+ ID its SMS arrive under. They have actions of their own rather than reusing
275
+ `update:organization`, because a wrong logo or sender goes out on every OTP and alert.
276
+
277
+ | Action | Covers |
278
+ |---|---|
279
+ | `read:notification-settings` | Reading the settings, the effective (resolved) branding and the SMS provider catalogue. |
280
+ | `manage:notification-settings` | Saving branding; adding, verifying and removing email senders; **requesting** SMS senders; sending a test email. |
281
+ | `approve:notification-sender` | Moving an SMS sender to `ACTIVE` / `SUSPENDED` / `REJECTED`. |
282
+
283
+ Owner and Admin hold the first two. **No role holds `approve:notification-sender`** -
284
+ only Super-Admin (`*`) passes it, and `roles.spec.ts` pins that. Activating a sender
285
+ asserts that it is registered with the provider and the telco; an organization able to
286
+ approve its own could send SMS under any name it typed.
287
+
288
+ ### The Billing permission module and Organization:Billing-Admin
289
+
290
+ clarion-billing's routes used to be guarded by the INCIDENT actions (and 45 of the
291
+ gateway's 116 billing routes by nothing but a sign-in). Every app user holds
292
+ `create/read/update:incident` to raise an SOS, so every handset could reach almost
293
+ every billing management route - approving a subscription without payment and
294
+ granting premium trials among them. They now use the Billing module, whose actions
295
+ come in two kinds:
296
+
297
+ | Kind | Actions | Who holds them |
298
+ |---|---|---|
299
+ | **Organization** - an organization's own billing, scoped by clarion-billing | packages, individual and corporate clients, corporate members, beneficiaries, subscriptions (incl. manual approval, cancel, `renew:subscription`), orders, invoices (incl. `mark:invoice-paid`), receipts, payments, `read:settlement`, `read:billing-config` | Owner, Admin and `Organization:Billing-Admin` all 44; `Organization:User` the 13 reads |
300
+ | **Platform** - the platform's own business, or not yet scoped to one organization | billing config writes (revenue share and tax terms), settlement create / send-invoice / mark-paid, settlement batches, `read:billing-operations`, premium trials, `read:entitlement` | **no role** - only Super-Admin (`*`) |
301
+
302
+ `roles.spec.ts` lists the platform actions by hand and fails on any Billing action
303
+ that is in neither list, so a new one has to be placed deliberately.
304
+
305
+ `Organization:Billing-Admin` holds the organization set plus exactly what its screens
306
+ need from other modules: `read:user` (an individual client is a platform user; the
307
+ client pages fetch the name and phone), `read:incident-category-type` (the package
308
+ form lists the incident types a package covers) and `read:organization`. No partner
309
+ permission: billing asks clarion-partner-services service-to-service, and no billing
310
+ screen calls a partner route.
311
+
312
+ `Mobile-User` holds the three billing actions the app uses -
313
+ `initialize:individual-subscription`, `renew:subscription`, `verify:payment`. The
314
+ storefront (`GET /billing/mobile/packages`) and the self-scoped "me" routes need only
315
+ a sign-in; `read:package` is withheld because it would also open the admin's package
316
+ list, which returns every organization's packages to a caller with no organization
317
+ scope. Control-room roles hold no billing action.
318
+
319
+ `BILLING_MANAGER_ROLES` (Owner, Admin, Billing-Admin) and `BILLING_VIEWER_ROLES`
320
+ (+ Organization:User) are for the places that still decide by role NAME -
321
+ clarion-billing's checks on manual approval, marking an invoice paid and renewing on
322
+ a subscriber's behalf, and the admin's billing menu and buttons. The spec asserts each
323
+ manager holds the organization set in full and each viewer every read.
324
+
325
+ ### The Newsfeed permission module and Organization:Newsfeed-Admin
326
+
327
+ clarion-newsfeed's admin routes were guarded by the ORGANIZATION actions and its
328
+ member routes by nothing but a sign-in. `read:organization` is held by every app
329
+ user, so any of them could list their organization's drafts; only the Owner held
330
+ `delete:organization`, so an Admin could not delete a post; and clarion-newsfeed let
331
+ any organization-scoped role moderate comments. The module now has two audiences:
332
+
333
+ | Actions | Audience | Held by |
334
+ |---|---|---|
335
+ | `create`/`read`/`update`/`publish`/`archive`/`delete:newsfeed-post`, `moderate:newsfeed-comment` | STAFF running the posts (`read:newsfeed-post` includes drafts and archived posts) | Owner, Admin, Newsfeed-Admin; Organization:User only `read:newsfeed-post` |
336
+ | `read:newsfeed-feed` (the published feed, a post, its comments), `engage:newsfeed-post` (like, comment, share, change one's own comment) | MEMBERS | Mobile-User, Control-Centre Admin/User, Responder (who could read the feed in the app before), plus the staff above (Organization:User reads only) |
337
+
338
+ `Organization:Newsfeed-Admin` holds the module in full and nothing else - its screens
339
+ call no other module. `NEWSFEED_MANAGER_ROLES` / `NEWSFEED_VIEWER_ROLES` are the
340
+ role-NAME lists clarion-newsfeed checks for managing/moderating and for seeing drafts.
341
+
342
+ ### Adding a role: every step fails silently on its own
343
+
344
+ 1. **Here** - the `Roles` value, a scope list, and a `ROLES_DATA` entry. `roles.spec.ts`
345
+ fails if a scoped role is unseeded, a seeded role is not in the enum, or a role
346
+ holds an action the catalogue does not list.
347
+
348
+ The same spec holds the rule for **actions**: every `PermissionActions` value must
349
+ be in `PERMISSIONS_DATA`, under exactly one module. An action outside the catalogue
350
+ still *works* - the gateway matches on the string - but role management can neither
351
+ show nor assign it, which is how the four `zone` actions went unnoticed for so long
352
+ (they now have a `Zone` module). Role and module names are also pinned to 50
353
+ characters, the width of the columns clarion-authentication stores them in.
354
+ 2. **clarion-authentication** - `users.roles` is a Postgres ENUM array, so the label
355
+ needs an `ALTER TYPE "users_roles_enum" ADD VALUE IF NOT EXISTS` migration. Without
356
+ it the role is seeded, offered by the admin, and rejected at the INSERT.
357
+ 3. **Every service that reads the role lists** - clarion-api-gateway,
358
+ clarion-authentication, clarion-socket - must INSTALL the new version (the lockfile
359
+ decides, not the caret) and redeploy. The gateway also validates `roles` with
360
+ `@IsEnum(Roles)`.
361
+ 4. **clarion-socket** names the organization roles admitted to tracking rooms one by
362
+ one; an organization role that should see the fleet has to be added there.
363
+ 5. **clarion-admin** shows menu entries by role NAME (`requiresAnyRole` in
364
+ `sidebar-data.ts`), from `clarion-shared-types`' mirror of `Roles`.
365
+
366
+ Deploy clarion-authentication first and let it reseed. The gateway caches role
367
+ permissions for five minutes, so a gateway that starts REQUIRING a new action should
368
+ go out after that window - or the roles that were just granted it are refused until
369
+ their cache entry expires.