@endora-commerce/mod-organizations 0.100.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 (288) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -0
  3. package/dist/admin/components/OrganizationSalesRepsTab.d.ts +27 -0
  4. package/dist/admin/components/OrganizationSalesRepsTab.d.ts.map +1 -0
  5. package/dist/admin/components/OrganizationSalesRepsTab.js +170 -0
  6. package/dist/admin/components/OrganizationSalesRepsTab.js.map +1 -0
  7. package/dist/admin/index.d.ts +30 -0
  8. package/dist/admin/index.d.ts.map +1 -0
  9. package/dist/admin/index.js +59 -0
  10. package/dist/admin/index.js.map +1 -0
  11. package/dist/admin/pages/OrganizationDetail.d.ts +9 -0
  12. package/dist/admin/pages/OrganizationDetail.d.ts.map +1 -0
  13. package/dist/admin/pages/OrganizationDetail.js +247 -0
  14. package/dist/admin/pages/OrganizationDetail.js.map +1 -0
  15. package/dist/admin/pages/OrganizationsList.d.ts +9 -0
  16. package/dist/admin/pages/OrganizationsList.d.ts.map +1 -0
  17. package/dist/admin/pages/OrganizationsList.js +73 -0
  18. package/dist/admin/pages/OrganizationsList.js.map +1 -0
  19. package/dist/admin/panels/ApplicablePriceListsPanel.d.ts +12 -0
  20. package/dist/admin/panels/ApplicablePriceListsPanel.d.ts.map +1 -0
  21. package/dist/admin/panels/ApplicablePriceListsPanel.js +49 -0
  22. package/dist/admin/panels/ApplicablePriceListsPanel.js.map +1 -0
  23. package/dist/admin/panels/FulfilmentStrategyPanel.d.ts +16 -0
  24. package/dist/admin/panels/FulfilmentStrategyPanel.d.ts.map +1 -0
  25. package/dist/admin/panels/FulfilmentStrategyPanel.js +29 -0
  26. package/dist/admin/panels/FulfilmentStrategyPanel.js.map +1 -0
  27. package/dist/admin/panels/HierarchyPanel.d.ts +15 -0
  28. package/dist/admin/panels/HierarchyPanel.d.ts.map +1 -0
  29. package/dist/admin/panels/HierarchyPanel.js +65 -0
  30. package/dist/admin/panels/HierarchyPanel.js.map +1 -0
  31. package/dist/admin/panels/ModerationActionsPanel.d.ts +28 -0
  32. package/dist/admin/panels/ModerationActionsPanel.d.ts.map +1 -0
  33. package/dist/admin/panels/ModerationActionsPanel.js +111 -0
  34. package/dist/admin/panels/ModerationActionsPanel.js.map +1 -0
  35. package/dist/admin/panels/RestrictionsPanel.d.ts +24 -0
  36. package/dist/admin/panels/RestrictionsPanel.d.ts.map +1 -0
  37. package/dist/admin/panels/RestrictionsPanel.js +121 -0
  38. package/dist/admin/panels/RestrictionsPanel.js.map +1 -0
  39. package/dist/admin/panels/VatValidationPanel.d.ts +28 -0
  40. package/dist/admin/panels/VatValidationPanel.d.ts.map +1 -0
  41. package/dist/admin/panels/VatValidationPanel.js +79 -0
  42. package/dist/admin/panels/VatValidationPanel.js.map +1 -0
  43. package/dist/backend/commands/move.command.d.ts +11 -0
  44. package/dist/backend/commands/move.command.d.ts.map +1 -0
  45. package/dist/backend/commands/move.command.js +11 -0
  46. package/dist/backend/commands/move.command.js.map +1 -0
  47. package/dist/backend/commands/set-cart-approval-policy.command.d.ts +31 -0
  48. package/dist/backend/commands/set-cart-approval-policy.command.d.ts.map +1 -0
  49. package/dist/backend/commands/set-cart-approval-policy.command.js +56 -0
  50. package/dist/backend/commands/set-cart-approval-policy.command.js.map +1 -0
  51. package/dist/backend/commands/set-credit-mode.command.d.ts +16 -0
  52. package/dist/backend/commands/set-credit-mode.command.d.ts.map +1 -0
  53. package/dist/backend/commands/set-credit-mode.command.js +25 -0
  54. package/dist/backend/commands/set-credit-mode.command.js.map +1 -0
  55. package/dist/backend/commands/set-parent.command.d.ts +26 -0
  56. package/dist/backend/commands/set-parent.command.d.ts.map +1 -0
  57. package/dist/backend/commands/set-parent.command.js +51 -0
  58. package/dist/backend/commands/set-parent.command.js.map +1 -0
  59. package/dist/backend/demo/reset.d.ts +20 -0
  60. package/dist/backend/demo/reset.d.ts.map +1 -0
  61. package/dist/backend/demo/reset.js +11 -0
  62. package/dist/backend/demo/reset.js.map +1 -0
  63. package/dist/backend/demo/rows.d.ts +44 -0
  64. package/dist/backend/demo/rows.d.ts.map +1 -0
  65. package/dist/backend/demo/rows.js +44 -0
  66. package/dist/backend/demo/rows.js.map +1 -0
  67. package/dist/backend/demo/seed.d.ts +20 -0
  68. package/dist/backend/demo/seed.d.ts.map +1 -0
  69. package/dist/backend/demo/seed.js +28 -0
  70. package/dist/backend/demo/seed.js.map +1 -0
  71. package/dist/backend/email-templates/invitation.d.ts +23 -0
  72. package/dist/backend/email-templates/invitation.d.ts.map +1 -0
  73. package/dist/backend/email-templates/invitation.js +35 -0
  74. package/dist/backend/email-templates/invitation.js.map +1 -0
  75. package/dist/backend/email-templates/transactional-defaults.d.ts +54 -0
  76. package/dist/backend/email-templates/transactional-defaults.d.ts.map +1 -0
  77. package/dist/backend/email-templates/transactional-defaults.js +242 -0
  78. package/dist/backend/email-templates/transactional-defaults.js.map +1 -0
  79. package/dist/backend/email-templates/verification.d.ts +14 -0
  80. package/dist/backend/email-templates/verification.d.ts.map +1 -0
  81. package/dist/backend/email-templates/verification.js +34 -0
  82. package/dist/backend/email-templates/verification.js.map +1 -0
  83. package/dist/backend/entities/email-verification-token.entity.d.ts +17 -0
  84. package/dist/backend/entities/email-verification-token.entity.d.ts.map +1 -0
  85. package/dist/backend/entities/email-verification-token.entity.js +60 -0
  86. package/dist/backend/entities/email-verification-token.entity.js.map +1 -0
  87. package/dist/backend/entities/organization-delivery-method-link.entity.d.ts +14 -0
  88. package/dist/backend/entities/organization-delivery-method-link.entity.d.ts.map +1 -0
  89. package/dist/backend/entities/organization-delivery-method-link.entity.js +41 -0
  90. package/dist/backend/entities/organization-delivery-method-link.entity.js.map +1 -0
  91. package/dist/backend/entities/organization-invitation.entity.d.ts +21 -0
  92. package/dist/backend/entities/organization-invitation.entity.d.ts.map +1 -0
  93. package/dist/backend/entities/organization-invitation.entity.js +85 -0
  94. package/dist/backend/entities/organization-invitation.entity.js.map +1 -0
  95. package/dist/backend/entities/organization-payment-method-link.entity.d.ts +14 -0
  96. package/dist/backend/entities/organization-payment-method-link.entity.d.ts.map +1 -0
  97. package/dist/backend/entities/organization-payment-method-link.entity.js +41 -0
  98. package/dist/backend/entities/organization-payment-method-link.entity.js.map +1 -0
  99. package/dist/backend/entities/organization-sales-rep-assignment.entity.d.ts +17 -0
  100. package/dist/backend/entities/organization-sales-rep-assignment.entity.d.ts.map +1 -0
  101. package/dist/backend/entities/organization-sales-rep-assignment.entity.js +56 -0
  102. package/dist/backend/entities/organization-sales-rep-assignment.entity.js.map +1 -0
  103. package/dist/backend/entities/organization-tax-id-validation.entity.d.ts +29 -0
  104. package/dist/backend/entities/organization-tax-id-validation.entity.d.ts.map +1 -0
  105. package/dist/backend/entities/organization-tax-id-validation.entity.js +78 -0
  106. package/dist/backend/entities/organization-tax-id-validation.entity.js.map +1 -0
  107. package/dist/backend/entities/organization-warehouse-link.entity.d.ts +15 -0
  108. package/dist/backend/entities/organization-warehouse-link.entity.d.ts.map +1 -0
  109. package/dist/backend/entities/organization-warehouse-link.entity.js +42 -0
  110. package/dist/backend/entities/organization-warehouse-link.entity.js.map +1 -0
  111. package/dist/backend/entities/organization.entity.d.ts +125 -0
  112. package/dist/backend/entities/organization.entity.d.ts.map +1 -0
  113. package/dist/backend/entities/organization.entity.js +282 -0
  114. package/dist/backend/entities/organization.entity.js.map +1 -0
  115. package/dist/backend/index.d.ts +250 -0
  116. package/dist/backend/index.d.ts.map +1 -0
  117. package/dist/backend/index.js +506 -0
  118. package/dist/backend/index.js.map +1 -0
  119. package/dist/backend/integrations/ministerstwo-finansow-client.d.ts +22 -0
  120. package/dist/backend/integrations/ministerstwo-finansow-client.d.ts.map +1 -0
  121. package/dist/backend/integrations/ministerstwo-finansow-client.js +172 -0
  122. package/dist/backend/integrations/ministerstwo-finansow-client.js.map +1 -0
  123. package/dist/backend/integrations/vies-client.d.ts +18 -0
  124. package/dist/backend/integrations/vies-client.d.ts.map +1 -0
  125. package/dist/backend/integrations/vies-client.js +133 -0
  126. package/dist/backend/integrations/vies-client.js.map +1 -0
  127. package/dist/backend/plugin.d.ts +126 -0
  128. package/dist/backend/plugin.d.ts.map +1 -0
  129. package/dist/backend/plugin.js +139 -0
  130. package/dist/backend/plugin.js.map +1 -0
  131. package/dist/backend/routes.admin.d.ts +66 -0
  132. package/dist/backend/routes.admin.d.ts.map +1 -0
  133. package/dist/backend/routes.admin.js +711 -0
  134. package/dist/backend/routes.admin.js.map +1 -0
  135. package/dist/backend/routes.customer.d.ts +33 -0
  136. package/dist/backend/routes.customer.d.ts.map +1 -0
  137. package/dist/backend/routes.customer.js +139 -0
  138. package/dist/backend/routes.customer.js.map +1 -0
  139. package/dist/backend/routes.members.d.ts +31 -0
  140. package/dist/backend/routes.members.d.ts.map +1 -0
  141. package/dist/backend/routes.members.js +109 -0
  142. package/dist/backend/routes.members.js.map +1 -0
  143. package/dist/backend/routes.public.d.ts +49 -0
  144. package/dist/backend/routes.public.d.ts.map +1 -0
  145. package/dist/backend/routes.public.js +220 -0
  146. package/dist/backend/routes.public.js.map +1 -0
  147. package/dist/backend/routes.sales-reps.d.ts +46 -0
  148. package/dist/backend/routes.sales-reps.d.ts.map +1 -0
  149. package/dist/backend/routes.sales-reps.js +114 -0
  150. package/dist/backend/routes.sales-reps.js.map +1 -0
  151. package/dist/backend/routes.storefront.d.ts +25 -0
  152. package/dist/backend/routes.storefront.d.ts.map +1 -0
  153. package/dist/backend/routes.storefront.js +65 -0
  154. package/dist/backend/routes.storefront.js.map +1 -0
  155. package/dist/backend/schemas/organization.d.ts +59 -0
  156. package/dist/backend/schemas/organization.d.ts.map +1 -0
  157. package/dist/backend/schemas/organization.js +55 -0
  158. package/dist/backend/schemas/organization.js.map +1 -0
  159. package/dist/backend/schemas/settings.d.ts +30 -0
  160. package/dist/backend/schemas/settings.d.ts.map +1 -0
  161. package/dist/backend/schemas/settings.js +35 -0
  162. package/dist/backend/schemas/settings.js.map +1 -0
  163. package/dist/backend/services/email-verification-service.d.ts +45 -0
  164. package/dist/backend/services/email-verification-service.d.ts.map +1 -0
  165. package/dist/backend/services/email-verification-service.js +95 -0
  166. package/dist/backend/services/email-verification-service.js.map +1 -0
  167. package/dist/backend/services/invitation-service.d.ts +85 -0
  168. package/dist/backend/services/invitation-service.d.ts.map +1 -0
  169. package/dist/backend/services/invitation-service.js +266 -0
  170. package/dist/backend/services/invitation-service.js.map +1 -0
  171. package/dist/backend/services/normalize-name.d.ts +14 -0
  172. package/dist/backend/services/normalize-name.d.ts.map +1 -0
  173. package/dist/backend/services/normalize-name.js +14 -0
  174. package/dist/backend/services/normalize-name.js.map +1 -0
  175. package/dist/backend/services/org-registration-notifier.d.ts +36 -0
  176. package/dist/backend/services/org-registration-notifier.d.ts.map +1 -0
  177. package/dist/backend/services/org-registration-notifier.js +109 -0
  178. package/dist/backend/services/org-registration-notifier.js.map +1 -0
  179. package/dist/backend/services/org-template-email.d.ts +18 -0
  180. package/dist/backend/services/org-template-email.d.ts.map +1 -0
  181. package/dist/backend/services/org-template-email.js +16 -0
  182. package/dist/backend/services/org-template-email.js.map +1 -0
  183. package/dist/backend/services/organization-context-service.d.ts +52 -0
  184. package/dist/backend/services/organization-context-service.d.ts.map +1 -0
  185. package/dist/backend/services/organization-context-service.js +70 -0
  186. package/dist/backend/services/organization-context-service.js.map +1 -0
  187. package/dist/backend/services/organization-country-reference.d.ts +11 -0
  188. package/dist/backend/services/organization-country-reference.d.ts.map +1 -0
  189. package/dist/backend/services/organization-country-reference.js +24 -0
  190. package/dist/backend/services/organization-country-reference.js.map +1 -0
  191. package/dist/backend/services/organization-details-port.d.ts +33 -0
  192. package/dist/backend/services/organization-details-port.d.ts.map +1 -0
  193. package/dist/backend/services/organization-details-port.js +95 -0
  194. package/dist/backend/services/organization-details-port.js.map +1 -0
  195. package/dist/backend/services/organization-effective-pricelists-service.d.ts +50 -0
  196. package/dist/backend/services/organization-effective-pricelists-service.d.ts.map +1 -0
  197. package/dist/backend/services/organization-effective-pricelists-service.js +68 -0
  198. package/dist/backend/services/organization-effective-pricelists-service.js.map +1 -0
  199. package/dist/backend/services/organization-inheritance-service.d.ts +73 -0
  200. package/dist/backend/services/organization-inheritance-service.d.ts.map +1 -0
  201. package/dist/backend/services/organization-inheritance-service.js +63 -0
  202. package/dist/backend/services/organization-inheritance-service.js.map +1 -0
  203. package/dist/backend/services/organization-moderation-service.d.ts +124 -0
  204. package/dist/backend/services/organization-moderation-service.d.ts.map +1 -0
  205. package/dist/backend/services/organization-moderation-service.js +337 -0
  206. package/dist/backend/services/organization-moderation-service.js.map +1 -0
  207. package/dist/backend/services/organization-restriction-service.d.ts +70 -0
  208. package/dist/backend/services/organization-restriction-service.d.ts.map +1 -0
  209. package/dist/backend/services/organization-restriction-service.js +210 -0
  210. package/dist/backend/services/organization-restriction-service.js.map +1 -0
  211. package/dist/backend/services/organization-tax-id-validation-service.d.ts +59 -0
  212. package/dist/backend/services/organization-tax-id-validation-service.d.ts.map +1 -0
  213. package/dist/backend/services/organization-tax-id-validation-service.js +115 -0
  214. package/dist/backend/services/organization-tax-id-validation-service.js.map +1 -0
  215. package/dist/backend/services/organization-tree-service.d.ts +102 -0
  216. package/dist/backend/services/organization-tree-service.d.ts.map +1 -0
  217. package/dist/backend/services/organization-tree-service.js +184 -0
  218. package/dist/backend/services/organization-tree-service.js.map +1 -0
  219. package/dist/backend/services/personal-organization-service.d.ts +130 -0
  220. package/dist/backend/services/personal-organization-service.d.ts.map +1 -0
  221. package/dist/backend/services/personal-organization-service.js +207 -0
  222. package/dist/backend/services/personal-organization-service.js.map +1 -0
  223. package/dist/backend/services/registration-service.d.ts +80 -0
  224. package/dist/backend/services/registration-service.d.ts.map +1 -0
  225. package/dist/backend/services/registration-service.js +126 -0
  226. package/dist/backend/services/registration-service.js.map +1 -0
  227. package/dist/backend/services/sales-rep-assignment-service.d.ts +89 -0
  228. package/dist/backend/services/sales-rep-assignment-service.d.ts.map +1 -0
  229. package/dist/backend/services/sales-rep-assignment-service.js +176 -0
  230. package/dist/backend/services/sales-rep-assignment-service.js.map +1 -0
  231. package/dist/backend/services/vat-validator-port.d.ts +20 -0
  232. package/dist/backend/services/vat-validator-port.d.ts.map +1 -0
  233. package/dist/backend/services/vat-validator-port.js +2 -0
  234. package/dist/backend/services/vat-validator-port.js.map +1 -0
  235. package/dist/manifest.d.ts +176 -0
  236. package/dist/manifest.d.ts.map +1 -0
  237. package/dist/manifest.js +503 -0
  238. package/dist/manifest.js.map +1 -0
  239. package/dist/migrations/20260424T205317_organizations_init.d.ts +13 -0
  240. package/dist/migrations/20260424T205317_organizations_init.d.ts.map +1 -0
  241. package/dist/migrations/20260424T205317_organizations_init.js +100 -0
  242. package/dist/migrations/20260424T205317_organizations_init.js.map +1 -0
  243. package/dist/migrations/20260425T051700_organizations_invitations_init.d.ts +13 -0
  244. package/dist/migrations/20260425T051700_organizations_invitations_init.d.ts.map +1 -0
  245. package/dist/migrations/20260425T051700_organizations_invitations_init.js +39 -0
  246. package/dist/migrations/20260425T051700_organizations_invitations_init.js.map +1 -0
  247. package/dist/migrations/20260611T140349_organizations_consolidation.d.ts +35 -0
  248. package/dist/migrations/20260611T140349_organizations_consolidation.d.ts.map +1 -0
  249. package/dist/migrations/20260611T140349_organizations_consolidation.js +180 -0
  250. package/dist/migrations/20260611T140349_organizations_consolidation.js.map +1 -0
  251. package/dist/migrations/20260611T140358_organizations_org_order_confirmation_emails.d.ts +10 -0
  252. package/dist/migrations/20260611T140358_organizations_org_order_confirmation_emails.d.ts.map +1 -0
  253. package/dist/migrations/20260611T140358_organizations_org_order_confirmation_emails.js +14 -0
  254. package/dist/migrations/20260611T140358_organizations_org_order_confirmation_emails.js.map +1 -0
  255. package/dist/migrations/20260611T140402_organizations_org_fulfilment_strategy.d.ts +14 -0
  256. package/dist/migrations/20260611T140402_organizations_org_fulfilment_strategy.d.ts.map +1 -0
  257. package/dist/migrations/20260611T140402_organizations_org_fulfilment_strategy.js +20 -0
  258. package/dist/migrations/20260611T140402_organizations_org_fulfilment_strategy.js.map +1 -0
  259. package/dist/migrations/20260717T151403_organizations_personal_organizations.d.ts +24 -0
  260. package/dist/migrations/20260717T151403_organizations_personal_organizations.d.ts.map +1 -0
  261. package/dist/migrations/20260717T151403_organizations_personal_organizations.js +63 -0
  262. package/dist/migrations/20260717T151403_organizations_personal_organizations.js.map +1 -0
  263. package/dist/migrations/20260718T200340_organizations_organization_custom_field_values.d.ts +12 -0
  264. package/dist/migrations/20260718T200340_organizations_organization_custom_field_values.d.ts.map +1 -0
  265. package/dist/migrations/20260718T200340_organizations_organization_custom_field_values.js +16 -0
  266. package/dist/migrations/20260718T200340_organizations_organization_custom_field_values.js.map +1 -0
  267. package/dist/migrations/20260720T044254_organizations_org_hierarchy.d.ts +23 -0
  268. package/dist/migrations/20260720T044254_organizations_org_hierarchy.d.ts.map +1 -0
  269. package/dist/migrations/20260720T044254_organizations_org_hierarchy.js +37 -0
  270. package/dist/migrations/20260720T044254_organizations_org_hierarchy.js.map +1 -0
  271. package/dist/migrations/20260912T094646_organizations_sales_channel_organizations.d.ts +27 -0
  272. package/dist/migrations/20260912T094646_organizations_sales_channel_organizations.d.ts.map +1 -0
  273. package/dist/migrations/20260912T094646_organizations_sales_channel_organizations.js +44 -0
  274. package/dist/migrations/20260912T094646_organizations_sales_channel_organizations.js.map +1 -0
  275. package/dist/migrations/index.d.ts +35 -0
  276. package/dist/migrations/index.d.ts.map +1 -0
  277. package/dist/migrations/index.js +45 -0
  278. package/dist/migrations/index.js.map +1 -0
  279. package/dist/ports/index.d.ts +53 -0
  280. package/dist/ports/index.d.ts.map +1 -0
  281. package/dist/ports/index.js +2 -0
  282. package/dist/ports/index.js.map +1 -0
  283. package/docs/organization-hierarchy.md +117 -0
  284. package/docs/organizations.md +312 -0
  285. package/i18n/en.json +17 -0
  286. package/i18n/pl.json +17 -0
  287. package/package.json +107 -0
  288. package/tailwind.css +14 -0
@@ -0,0 +1,59 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import type { AuditPort } from '@endora-commerce/platform/kernel';
3
+ import { OrganizationTaxIdValidation, type ReturnedAddress } from '../entities/organization-tax-id-validation.entity.js';
4
+ import type { VatValidator } from './vat-validator-port.js';
5
+ /**
6
+ * Owns the tax-ID validation flow (feature 026 US7).
7
+ *
8
+ * Inputs:
9
+ * - `providerHint`: `'auto' | 'vies' | 'mf_pl' | 'format_only'`
10
+ * `auto` → routes Polish NIPs (PL-prefixed or 10-digit) to MF;
11
+ * anything that parses as a EU country code prefix → VIES; otherwise
12
+ * degrades to `format_only`.
13
+ * - `applyAutoFill`: when the result is `validated` AND the provider
14
+ * returned a legalName / address, the Organization's `legalName` is
15
+ * updated and `version` bumps. The address payload is only used for
16
+ * display in the panel today (no Address-entity writes here — the
17
+ * existing addresses module owns those).
18
+ *
19
+ * Output:
20
+ * - Always persists one `OrganizationTaxIdValidation` row, regardless of
21
+ * outcome. Provider outages produce `deferred` rows that the admin
22
+ * can retry. The Organization itself NEVER fails its save because of
23
+ * a validation outage.
24
+ */
25
+ export interface TriggerValidationInput {
26
+ providerHint?: 'auto' | 'vies' | 'mf_pl' | 'format_only';
27
+ applyAutoFill?: boolean;
28
+ actorAdminUserId?: string | null;
29
+ }
30
+ export interface TriggerValidationResult {
31
+ record: OrganizationTaxIdValidation;
32
+ /** Reflects the org's post-update state when `applyAutoFill` was true. */
33
+ organization: {
34
+ id: string;
35
+ legalName: string | null;
36
+ version: number;
37
+ } | null;
38
+ }
39
+ export interface OrganizationTaxIdValidationServiceDeps {
40
+ emFactory: () => EntityManager;
41
+ /** VIES adapter — production wires `new ViesClient()`; tests inject a stub. */
42
+ vies?: VatValidator;
43
+ /** Ministerstwo Finansów adapter — production wires `new MinisterstwoFinansowClient()`. */
44
+ mfPl?: VatValidator;
45
+ /** Feature 054 — audits the validation write co-transactionally when provided. */
46
+ auditLog?: AuditPort;
47
+ }
48
+ export declare class OrganizationTaxIdValidationService {
49
+ private readonly deps;
50
+ constructor(deps: OrganizationTaxIdValidationServiceDeps);
51
+ listForOrganization(organizationId: string, options?: {
52
+ limit?: number;
53
+ }): Promise<OrganizationTaxIdValidation[]>;
54
+ trigger(organizationId: string, input?: TriggerValidationInput): Promise<TriggerValidationResult>;
55
+ private pickProvider;
56
+ private runProvider;
57
+ }
58
+ export type { ReturnedAddress };
59
+ //# sourceMappingURL=organization-tax-id-validation-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"organization-tax-id-validation-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/organization-tax-id-validation-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAG3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAClE,OAAO,EACL,2BAA2B,EAC3B,KAAK,eAAe,EACrB,MAAM,sDAAsD,CAAC;AAC9D,OAAO,KAAK,EAAE,YAAY,EAAuB,MAAM,yBAAyB,CAAC;AAEjF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,sBAAsB;IACrC,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,aAAa,CAAC;IACzD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,2BAA2B,CAAC;IACpC,0EAA0E;IAC1E,YAAY,EAAE;QACZ,EAAE,EAAE,MAAM,CAAC;QACX,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;QACzB,OAAO,EAAE,MAAM,CAAC;KACjB,GAAG,IAAI,CAAC;CACV;AAED,MAAM,WAAW,sCAAsC;IACrD,SAAS,EAAE,MAAM,aAAa,CAAC;IAC/B,+EAA+E;IAC/E,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,2FAA2F;IAC3F,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,kFAAkF;IAClF,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,qBAAa,kCAAkC;IACjC,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,sCAAsC;IAEnE,mBAAmB,CACvB,cAAc,EAAE,MAAM,EACtB,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAO,GAC/B,OAAO,CAAC,2BAA2B,EAAE,CAAC;IASnC,OAAO,CACX,cAAc,EAAE,MAAM,EACtB,KAAK,GAAE,sBAA2B,GACjC,OAAO,CAAC,uBAAuB,CAAC;IA0DnC,OAAO,CAAC,YAAY;YAkBN,WAAW;CAmC1B;AAGD,YAAY,EAAE,eAAe,EAAE,CAAC"}
@@ -0,0 +1,115 @@
1
+ import { Organization } from '../entities/organization.entity.js';
2
+ import { recordAuditFromContext } from '@endora-commerce/platform/commands';
3
+ import { OrganizationTaxIdValidation, } from '../entities/organization-tax-id-validation.entity.js';
4
+ export class OrganizationTaxIdValidationService {
5
+ deps;
6
+ constructor(deps) {
7
+ this.deps = deps;
8
+ }
9
+ async listForOrganization(organizationId, options = {}) {
10
+ const em = this.deps.emFactory();
11
+ return em.find(OrganizationTaxIdValidation, { organizationId }, { orderBy: { createdAt: 'desc' }, limit: Math.min(options.limit ?? 25, 100) });
12
+ }
13
+ async trigger(organizationId, input = {}) {
14
+ const em = this.deps.emFactory();
15
+ const org = await em.findOneOrFail(Organization, {
16
+ id: organizationId,
17
+ deletedAt: null,
18
+ });
19
+ const providerKind = this.pickProvider(org.taxId, input.providerHint ?? 'auto');
20
+ const result = await this.runProvider(providerKind, org.taxId);
21
+ const record = em.create(OrganizationTaxIdValidation, {
22
+ organizationId,
23
+ provider: providerKind,
24
+ outcome: result.outcome,
25
+ taxIdValue: org.taxId,
26
+ legalNameReturned: result.legalName,
27
+ addressReturned: result.address,
28
+ errorKind: result.errorKind,
29
+ requestedByAdminUserId: input.actorAdminUserId ?? null,
30
+ });
31
+ await em.persistAndFlush(record);
32
+ // Stamp the org's "last validation" summary fields regardless of
33
+ // outcome — the admin panel reads `vatValidatedAt`, `vatValidationProvider`,
34
+ // and `vatValidationOutcome` directly off the Organization.
35
+ org.vatValidatedAt = new Date();
36
+ org.vatValidationProvider = providerKind;
37
+ org.vatValidationOutcome = result.outcome;
38
+ let appliedOrg = null;
39
+ if (input.applyAutoFill && result.outcome === 'validated' && result.legalName) {
40
+ org.legalName = result.legalName;
41
+ }
42
+ if (this.deps.auditLog) {
43
+ recordAuditFromContext(this.deps.auditLog, em, {
44
+ action: 'organization.tax_id_validation',
45
+ objectType: 'organization',
46
+ objectId: org.id,
47
+ stateBefore: null,
48
+ stateAfter: {
49
+ provider: providerKind,
50
+ outcome: result.outcome,
51
+ appliedAutoFill: input.applyAutoFill && result.outcome === 'validated' && !!result.legalName,
52
+ },
53
+ });
54
+ }
55
+ await em.flush();
56
+ if (input.applyAutoFill && result.outcome === 'validated' && result.legalName) {
57
+ appliedOrg = {
58
+ id: org.id,
59
+ legalName: org.legalName ?? null,
60
+ version: org.version,
61
+ };
62
+ }
63
+ return { record, organization: appliedOrg };
64
+ }
65
+ pickProvider(taxId, hint) {
66
+ if (hint === 'vies')
67
+ return 'vies';
68
+ if (hint === 'mf_pl')
69
+ return 'mf_pl';
70
+ if (hint === 'format_only')
71
+ return 'format_only';
72
+ // auto-pick
73
+ const cleaned = taxId.replace(/[\s-]+/g, '').toUpperCase();
74
+ if (/^PL[0-9]{10}$/.test(cleaned) || /^[0-9]{10}$/.test(cleaned)) {
75
+ return 'mf_pl';
76
+ }
77
+ if (/^[A-Z]{2}.+$/.test(cleaned)) {
78
+ return 'vies';
79
+ }
80
+ return 'format_only';
81
+ }
82
+ async runProvider(provider, taxId) {
83
+ if (provider === 'format_only') {
84
+ const cleaned = taxId.replace(/[\s-]+/g, '');
85
+ const looksLikeId = /^[A-Z0-9]{6,32}$/i.test(cleaned);
86
+ return {
87
+ outcome: looksLikeId ? 'unverified' : 'failed',
88
+ legalName: null,
89
+ address: null,
90
+ errorKind: looksLikeId ? null : 'invalid_format',
91
+ };
92
+ }
93
+ if (provider === 'vies') {
94
+ if (!this.deps.vies) {
95
+ return {
96
+ outcome: 'deferred',
97
+ legalName: null,
98
+ address: null,
99
+ errorKind: 'provider_not_wired',
100
+ };
101
+ }
102
+ return this.deps.vies.validate({ taxId });
103
+ }
104
+ if (!this.deps.mfPl) {
105
+ return {
106
+ outcome: 'deferred',
107
+ legalName: null,
108
+ address: null,
109
+ errorKind: 'provider_not_wired',
110
+ };
111
+ }
112
+ return this.deps.mfPl.validate({ taxId });
113
+ }
114
+ }
115
+ //# sourceMappingURL=organization-tax-id-validation-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"organization-tax-id-validation-service.js","sourceRoot":"","sources":["../../../src/backend/services/organization-tax-id-validation-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,102 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import { type OrganizationTreeNode } from '@endora-commerce/contracts';
3
+ import { HttpError } from '@endora-commerce/platform/http';
4
+ import type { Organization } from '../entities/organization.entity.js';
5
+ /**
6
+ * OrganizationTreeService (feature 056 US1) — the organizations-owned tree
7
+ * mechanism over the materialized `path` column (`'/<rootId>/…/<thisId>/'`).
8
+ *
9
+ * All traversal is single-query / index-backed (a `text_pattern_ops` prefix
10
+ * scan on `path`), never a per-node walk (FR-012 / SC-002). Mutations
11
+ * (assign/move) go through the Command Bus; this service supplies the pure
12
+ * cycle/depth checks and the single-UPDATE subtree path rewrite the Commands
13
+ * drive (Principle XIII).
14
+ *
15
+ * Flat behavior is preserved byte-for-byte: a root has `parent_id = NULL` and
16
+ * `path = '/<id>/'`, so `subtreeIds(root) = {root}` and `ancestorIds(root) = []`.
17
+ */
18
+ /** Maximum number of path segments (tree levels) — depth bound (research §R3). */
19
+ export declare const MAX_TREE_DEPTH_SEGMENTS = 10;
20
+ /**
21
+ * The three refusals this module's tree rules produce, as pure functions —
22
+ * D-129's remaining sweep, MR 7.
23
+ *
24
+ * Extracted for the reason `admin_roles`' `roleInUseRefusal` was (issue #168):
25
+ * `check:error-translations` verifies that a code has a sentence and can see
26
+ * neither the token the raise writes nor a `{placeholder}` nothing fills, and
27
+ * the envelope's own `hasUnfilledPlaceholder` fall-back then hides the miss
28
+ * behind the written English. Only a test that builds a **real** refusal and
29
+ * renders the bundle's sentence from its `details` sees all three agree, and a
30
+ * refusal it can build without an `EntityManager` is what makes that test a unit
31
+ * test — `test/unit/organizations/error-code-sentences.test.ts`.
32
+ *
33
+ * Every one of them is tokened, which is the whole shape here: the base key
34
+ * `errors.<CODE>` renders for nobody, because `localizeErrorEnvelope` composes
35
+ * `errors.<CODE>.<token>` whenever `details.code` is present and never re-asks.
36
+ * The base sentences are carried anyway, and this module's manifest records
37
+ * D-190's measurement of why.
38
+ */
39
+ export declare function cycleRefusal(): HttpError;
40
+ /**
41
+ * The depth refusal, carrying the bound it refused against.
42
+ *
43
+ * `maxDepth` and never a second `code`: `details.code` is the refusal token, so
44
+ * a value written there would re-key the sentence lookup instead of filling the
45
+ * sentence. Before MR 7 this raise interpolated the bound into its English
46
+ * message and passed the token alone, so the translated sentence — which
47
+ * replaces the message wholesale — had no way to say how deep is too deep
48
+ * (!1181's finding).
49
+ */
50
+ export declare function maxDepthRefusal(maxDepth?: number): HttpError;
51
+ /**
52
+ * Deleting an organization that still has sub-organizations (FR-010), backed by
53
+ * the database's own `parent_id ON DELETE RESTRICT`.
54
+ *
55
+ * It lives beside the two above because it is the same rule read from the other
56
+ * end — a child is an edge of this tree — and because the admin route that
57
+ * raises it already imports this service.
58
+ */
59
+ export declare function hasChildrenRefusal(): HttpError;
60
+ export declare class OrganizationTreeService {
61
+ #private;
62
+ private readonly emFactory;
63
+ constructor(emFactory: () => EntityManager);
64
+ /**
65
+ * `{orgId} ∪ descendants`, pre-order (by `path`). One indexed prefix scan.
66
+ * Empty when the org does not exist.
67
+ */
68
+ subtreeIds(orgId: string): Promise<string[]>;
69
+ /** Descendants of `orgId` (excludes self), pre-order. */
70
+ descendantIds(orgId: string): Promise<string[]>;
71
+ /** Ancestor ids of `orgId`, nearest-first (parent → … → root). Excludes self. */
72
+ ancestorIds(orgId: string): Promise<string[]>;
73
+ /** Subtree as tree nodes (id, name, parentId, depth, status), pre-order. */
74
+ subtreeNodes(orgId: string): Promise<OrganizationTreeNode[]>;
75
+ /** Ancestor chain as tree nodes, nearest-first. */
76
+ ancestorNodes(orgId: string): Promise<OrganizationTreeNode[]>;
77
+ /**
78
+ * Rejects assigning `node`'s parent to `newParent` when that would create a
79
+ * cycle: `newParent` is `node` itself or lives inside `node`'s own subtree.
80
+ * One path-prefix comparison — no walk (FR-002).
81
+ */
82
+ assertNoCycle(node: Organization, newParent: Organization | null): void;
83
+ /**
84
+ * Rejects a re-parent whose resulting deepest subtree node would exceed the
85
+ * depth bound (`MAX_TREE_DEPTH_SEGMENTS` path segments — research §R3).
86
+ */
87
+ assertMaxDepth(em: EntityManager, node: Organization, newParent: Organization | null): Promise<void>;
88
+ /**
89
+ * Sets `node.parent_id` and rewrites the moved subtree's `path` prefixes in a
90
+ * single UPDATE (`path = :newPath || substring(path from len(oldPath)+1)`).
91
+ * Runs on the transactional `em`. The caller must have already validated the
92
+ * cycle + depth invariants. Returns the affected rows' prior `path` values
93
+ * (for reversible-command capture).
94
+ */
95
+ applyReparentPaths(em: EntityManager, node: Organization, newParent: Organization | null): Promise<{
96
+ affected: Array<{
97
+ id: string;
98
+ oldPath: string;
99
+ }>;
100
+ }>;
101
+ }
102
+ //# sourceMappingURL=organization-tree-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"organization-tree-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/organization-tree-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAe,KAAK,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AACpF,OAAO,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAsB,MAAM,oCAAoC,CAAC;AAE3F;;;;;;;;;;;;GAYG;AAEH,kFAAkF;AAClF,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAO1C;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,YAAY,IAAI,SAAS,CAOxC;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,QAAQ,GAAE,MAAgC,GAAG,SAAS,CAOrF;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,IAAI,SAAS,CAO9C;AAED,qBAAa,uBAAuB;;IACtB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,EAAE,MAAM,aAAa;IAE3D;;;OAGG;IACG,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAYlD,yDAAyD;IACnD,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAKrD,iFAAiF;IAC3E,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAanD,4EAA4E;IACtE,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,EAAE,CAAC;IAYlE,mDAAmD;IAC7C,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,EAAE,CAAC;IAcnE;;;;OAIG;IACH,aAAa,CAAC,IAAI,EAAE,YAAY,EAAE,SAAS,EAAE,YAAY,GAAG,IAAI,GAAG,IAAI;IAOvE;;;OAGG;IACG,cAAc,CAClB,EAAE,EAAE,aAAa,EACjB,IAAI,EAAE,YAAY,EAClB,SAAS,EAAE,YAAY,GAAG,IAAI,GAC7B,OAAO,CAAC,IAAI,CAAC;IAoBhB;;;;;;OAMG;IACG,kBAAkB,CACtB,EAAE,EAAE,aAAa,EACjB,IAAI,EAAE,YAAY,EAClB,SAAS,EAAE,YAAY,GAAG,IAAI,GAC7B,OAAO,CAAC;QAAE,QAAQ,EAAE,KAAK,CAAC;YAAE,EAAE,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAAE,CAAC;CAgDjE"}
@@ -0,0 +1,184 @@
1
+ import { ERROR_CODES } from '@endora-commerce/contracts';
2
+ import { HttpError } from '@endora-commerce/platform/http';
3
+ /**
4
+ * OrganizationTreeService (feature 056 US1) — the organizations-owned tree
5
+ * mechanism over the materialized `path` column (`'/<rootId>/…/<thisId>/'`).
6
+ *
7
+ * All traversal is single-query / index-backed (a `text_pattern_ops` prefix
8
+ * scan on `path`), never a per-node walk (FR-012 / SC-002). Mutations
9
+ * (assign/move) go through the Command Bus; this service supplies the pure
10
+ * cycle/depth checks and the single-UPDATE subtree path rewrite the Commands
11
+ * drive (Principle XIII).
12
+ *
13
+ * Flat behavior is preserved byte-for-byte: a root has `parent_id = NULL` and
14
+ * `path = '/<id>/'`, so `subtreeIds(root) = {root}` and `ancestorIds(root) = []`.
15
+ */
16
+ /** Maximum number of path segments (tree levels) — depth bound (research §R3). */
17
+ export const MAX_TREE_DEPTH_SEGMENTS = 10;
18
+ /** Split a materialized path `'/a/b/c/'` into its ordered id segments `[a, b, c]`. */
19
+ function pathSegments(path) {
20
+ return path.split('/').filter((s) => s.length > 0);
21
+ }
22
+ /**
23
+ * The three refusals this module's tree rules produce, as pure functions —
24
+ * D-129's remaining sweep, MR 7.
25
+ *
26
+ * Extracted for the reason `admin_roles`' `roleInUseRefusal` was (issue #168):
27
+ * `check:error-translations` verifies that a code has a sentence and can see
28
+ * neither the token the raise writes nor a `{placeholder}` nothing fills, and
29
+ * the envelope's own `hasUnfilledPlaceholder` fall-back then hides the miss
30
+ * behind the written English. Only a test that builds a **real** refusal and
31
+ * renders the bundle's sentence from its `details` sees all three agree, and a
32
+ * refusal it can build without an `EntityManager` is what makes that test a unit
33
+ * test — `test/unit/organizations/error-code-sentences.test.ts`.
34
+ *
35
+ * Every one of them is tokened, which is the whole shape here: the base key
36
+ * `errors.<CODE>` renders for nobody, because `localizeErrorEnvelope` composes
37
+ * `errors.<CODE>.<token>` whenever `details.code` is present and never re-asks.
38
+ * The base sentences are carried anyway, and this module's manifest records
39
+ * D-190's measurement of why.
40
+ */
41
+ export function cycleRefusal() {
42
+ return new HttpError(422, ERROR_CODES.ORGANIZATION_TREE_INVALID, 'Cannot set a parent that would create a cycle (the target is inside this organization\'s own subtree).', { code: 'cycle' });
43
+ }
44
+ /**
45
+ * The depth refusal, carrying the bound it refused against.
46
+ *
47
+ * `maxDepth` and never a second `code`: `details.code` is the refusal token, so
48
+ * a value written there would re-key the sentence lookup instead of filling the
49
+ * sentence. Before MR 7 this raise interpolated the bound into its English
50
+ * message and passed the token alone, so the translated sentence — which
51
+ * replaces the message wholesale — had no way to say how deep is too deep
52
+ * (!1181's finding).
53
+ */
54
+ export function maxDepthRefusal(maxDepth = MAX_TREE_DEPTH_SEGMENTS) {
55
+ return new HttpError(422, ERROR_CODES.ORGANIZATION_TREE_INVALID, `Re-parent would exceed the maximum organization tree depth (${maxDepth} levels).`, { code: 'max_depth_exceeded', maxDepth });
56
+ }
57
+ /**
58
+ * Deleting an organization that still has sub-organizations (FR-010), backed by
59
+ * the database's own `parent_id ON DELETE RESTRICT`.
60
+ *
61
+ * It lives beside the two above because it is the same rule read from the other
62
+ * end — a child is an edge of this tree — and because the admin route that
63
+ * raises it already imports this service.
64
+ */
65
+ export function hasChildrenRefusal() {
66
+ return new HttpError(409, ERROR_CODES.ORGANIZATION_HAS_CHILDREN, 'This organization has sub-organizations. Reassign or remove the children first.', { code: 'has_children' });
67
+ }
68
+ export class OrganizationTreeService {
69
+ emFactory;
70
+ constructor(emFactory) {
71
+ this.emFactory = emFactory;
72
+ }
73
+ /**
74
+ * `{orgId} ∪ descendants`, pre-order (by `path`). One indexed prefix scan.
75
+ * Empty when the org does not exist.
76
+ */
77
+ async subtreeIds(orgId) {
78
+ const em = this.emFactory();
79
+ const rows = (await em.getConnection().execute(`select "id" from "organizations"
80
+ where "path" like (select "path" from "organizations" where "id" = ?) || '%'
81
+ and "deleted_at" is null
82
+ order by "path"`, [orgId]));
83
+ return rows.map((r) => r.id);
84
+ }
85
+ /** Descendants of `orgId` (excludes self), pre-order. */
86
+ async descendantIds(orgId) {
87
+ const subtree = await this.subtreeIds(orgId);
88
+ return subtree.filter((id) => id !== orgId);
89
+ }
90
+ /** Ancestor ids of `orgId`, nearest-first (parent → … → root). Excludes self. */
91
+ async ancestorIds(orgId) {
92
+ const em = this.emFactory();
93
+ const rows = (await em.getConnection().execute(`select "path" from "organizations" where "id" = ?`, [orgId]));
94
+ const path = rows[0]?.path;
95
+ if (!path)
96
+ return [];
97
+ const segs = pathSegments(path);
98
+ segs.pop(); // drop self (final segment)
99
+ return segs.reverse(); // nearest-first
100
+ }
101
+ /** Subtree as tree nodes (id, name, parentId, depth, status), pre-order. */
102
+ async subtreeNodes(orgId) {
103
+ const em = this.emFactory();
104
+ const rows = (await em.getConnection().execute(`select "id", "name", "parent_id", "status", "path" from "organizations"
105
+ where "path" like (select "path" from "organizations" where "id" = ?) || '%'
106
+ and "deleted_at" is null
107
+ order by "path"`, [orgId]));
108
+ return rows.map((r) => this.#toNode(r));
109
+ }
110
+ /** Ancestor chain as tree nodes, nearest-first. */
111
+ async ancestorNodes(orgId) {
112
+ const ids = await this.ancestorIds(orgId);
113
+ if (ids.length === 0)
114
+ return [];
115
+ const em = this.emFactory();
116
+ const rows = (await em.getConnection().execute(`select "id", "name", "parent_id", "status", "path" from "organizations"
117
+ where "id" in (${ids.map(() => '?').join(', ')})`, ids));
118
+ const byId = new Map(rows.map((r) => [r.id, this.#toNode(r)]));
119
+ // Preserve nearest-first order from `ids`.
120
+ return ids.map((id) => byId.get(id)).filter((n) => n !== undefined);
121
+ }
122
+ /**
123
+ * Rejects assigning `node`'s parent to `newParent` when that would create a
124
+ * cycle: `newParent` is `node` itself or lives inside `node`'s own subtree.
125
+ * One path-prefix comparison — no walk (FR-002).
126
+ */
127
+ assertNoCycle(node, newParent) {
128
+ if (!newParent)
129
+ return; // detach → root, never a cycle
130
+ if (newParent.id === node.id || newParent.path.startsWith(node.path)) {
131
+ throw cycleRefusal();
132
+ }
133
+ }
134
+ /**
135
+ * Rejects a re-parent whose resulting deepest subtree node would exceed the
136
+ * depth bound (`MAX_TREE_DEPTH_SEGMENTS` path segments — research §R3).
137
+ */
138
+ async assertMaxDepth(em, node, newParent) {
139
+ const rows = (await em.getConnection().execute(`select "path" from "organizations" where "path" like ? and "deleted_at" is null`, [`${node.path}%`], 'all', em.getTransactionContext()));
140
+ const nodeSegs = pathSegments(node.path).length;
141
+ const maxSubtreeSegs = rows.reduce((max, r) => Math.max(max, pathSegments(r.path).length), nodeSegs);
142
+ const relativeMax = maxSubtreeSegs - nodeSegs; // 0 when node is a leaf
143
+ const newNodeSegs = newParent ? pathSegments(newParent.path).length + 1 : 1;
144
+ const newDeepestSegs = newNodeSegs + relativeMax;
145
+ if (newDeepestSegs > MAX_TREE_DEPTH_SEGMENTS) {
146
+ throw maxDepthRefusal();
147
+ }
148
+ }
149
+ /**
150
+ * Sets `node.parent_id` and rewrites the moved subtree's `path` prefixes in a
151
+ * single UPDATE (`path = :newPath || substring(path from len(oldPath)+1)`).
152
+ * Runs on the transactional `em`. The caller must have already validated the
153
+ * cycle + depth invariants. Returns the affected rows' prior `path` values
154
+ * (for reversible-command capture).
155
+ */
156
+ async applyReparentPaths(em, node, newParent) {
157
+ const oldPath = node.path;
158
+ const newPath = `${newParent ? newParent.path : '/'}${node.id}/`;
159
+ // Capture the moved subtree's prior paths (reversibility, R3).
160
+ const before = (await em.getConnection().execute(`select "id", "path" from "organizations" where "path" like ?`, [`${oldPath}%`], 'all', em.getTransactionContext()));
161
+ const affected = before.map((r) => ({ id: r.id, oldPath: r.path }));
162
+ if (newPath !== oldPath) {
163
+ // Rewrite every subtree row's path prefix in one statement.
164
+ await em.getConnection().execute(`update "organizations"
165
+ set "path" = ? || substring("path" from ?)
166
+ where "path" like ?`, [newPath, oldPath.length + 1, `${oldPath}%`], 'run', em.getTransactionContext());
167
+ }
168
+ // Reflect the change on the managed node entity so the transaction flush
169
+ // persists the new parent_id (+ version bump) consistently with the DB.
170
+ node.parentId = newParent ? newParent.id : null;
171
+ node.path = newPath;
172
+ return { affected };
173
+ }
174
+ #toNode(r) {
175
+ return {
176
+ id: r.id,
177
+ name: r.name,
178
+ parentId: r.parent_id,
179
+ depth: pathSegments(r.path).length - 1, // root = 0
180
+ status: r.status,
181
+ };
182
+ }
183
+ }
184
+ //# sourceMappingURL=organization-tree-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"organization-tree-service.js","sourceRoot":"","sources":["../../../src/backend/services/organization-tree-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,130 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import { type CustomerAccountMemberWritePort, type CustomerAccountReadPort, type CustomerAccountRecord } from '@endora-commerce/contracts';
3
+ import type { AuditPort } from '@endora-commerce/platform/kernel';
4
+ import { Organization } from '../entities/organization.entity.js';
5
+ /**
6
+ * `customer_accounts`' own read and write of the row this service binds to an
7
+ * Organization (feature 075, Phase C). The three sites they replace were
8
+ * `em.findOne`, `em.count` and a field assignment on that module's entity.
9
+ *
10
+ * With `customer_accounts` off both fail closed, which is the only coherent
11
+ * answer: provisioning a personal Organization for an account the platform
12
+ * cannot see would create a tenant with no member.
13
+ */
14
+ export interface PersonalOrganizationAccountPorts {
15
+ read: CustomerAccountReadPort;
16
+ write: CustomerAccountMemberWritePort;
17
+ }
18
+ /**
19
+ * The name a scrubbed personal organisation is renamed to. It matches the one
20
+ * `customer_accounts` writes over the member's own first and last name,
21
+ * because a personal organisation's name *is* that person's name.
22
+ */
23
+ export declare const ANONYMIZED_ORGANIZATION_NAME = "Deleted customer";
24
+ /**
25
+ * Feature 051 — provisions and guards single-member Personal Organizations for
26
+ * B2C (individual) customers. One provisioning rule, reused by live registration
27
+ * and the backfill migration's mental model (the migration duplicates the SQL).
28
+ */
29
+ export declare class PersonalOrganizationService {
30
+ #private;
31
+ private readonly emFactory;
32
+ /**
33
+ * Required since `customers`' Phase-C cut: the `ensureFor` overload that
34
+ * needed neither port — it took the caller's managed `CustomerAccount` and
35
+ * its `EntityManager` — went with that cut, and every method left reads
36
+ * this module's neighbour through its ports.
37
+ */
38
+ private readonly accounts;
39
+ /**
40
+ * Feature 075 — `anonymizeIfOrphaned` records the scrub. It used to be
41
+ * written by `customers`' deletion sweep, on a row that module does not
42
+ * own; the write and its one audit row moved here together.
43
+ */
44
+ private readonly auditLog?;
45
+ constructor(emFactory: () => EntityManager,
46
+ /**
47
+ * Required since `customers`' Phase-C cut: the `ensureFor` overload that
48
+ * needed neither port — it took the caller's managed `CustomerAccount` and
49
+ * its `EntityManager` — went with that cut, and every method left reads
50
+ * this module's neighbour through its ports.
51
+ */
52
+ accounts: PersonalOrganizationAccountPorts,
53
+ /**
54
+ * Feature 075 — `anonymizeIfOrphaned` records the scrub. It used to be
55
+ * written by `customers`' deletion sweep, on a row that module does not
56
+ * own; the write and its one audit row moved here together.
57
+ */
58
+ auditLog?: AuditPort | undefined);
59
+ /** Name an individual's organization from their profile, falling back to the email local-part. */
60
+ static personalName(account: Pick<CustomerAccountRecord, 'firstName' | 'lastName' | 'email'>): string;
61
+ /**
62
+ * The provisioning itself — find-or-create the personal organisation for an
63
+ * account, on the `EntityManager` the caller hands in.
64
+ *
65
+ * D-178 made this the one place the row is written, and gave it a caller that
66
+ * has no account row yet: `customer_accounts` inserts the account and its
67
+ * organisation in one transaction, so it passes the id it is *about* to write
68
+ * and the name components it already holds. Nothing here reads
69
+ * `customer_accounts` and nothing here writes it.
70
+ *
71
+ * Idempotent by `taxId`, which is derived from the account id — a retry after
72
+ * a rolled-back attempt re-finds the row rather than creating a second one.
73
+ *
74
+ * command-coverage-ignore: idempotent auto-provisioning of a customer's
75
+ * personal (B2C) organization — a system invariant the constitution mandates
76
+ * (Principle XI), not an operator-initiated write. Its caller's own write is
77
+ * the audited event.
78
+ */
79
+ provisionFor(em: EntityManager, input: {
80
+ customerAccountId: string;
81
+ email: string;
82
+ firstName?: string | null;
83
+ lastName?: string | null;
84
+ }): Promise<Organization>;
85
+ /**
86
+ * Return the customer's organization, provisioning a personal one iff none is
87
+ * linked. Idempotent by customer — a customer that already has an organization
88
+ * is returned as-is (never a second personal org).
89
+ *
90
+ * **Since D-178 this repairs rather than provisions.** Every write path now
91
+ * creates the account and its organisation in one transaction, so a live
92
+ * account always has one and this method's provisioning arm is reached only by
93
+ * a caller holding an account written before that ruling. It is kept because
94
+ * it is idempotent and because `ensureForCustomerAccount` is a published port.
95
+ */
96
+ ensureForCustomerAccountId(customerAccountId: string): Promise<Organization>;
97
+ /**
98
+ * D-178 — the account's **own** personal organisation, whatever it currently
99
+ * belongs to, provisioned if it has never existed.
100
+ *
101
+ * `unassign` on the admin customer screen is what wants it: an operator
102
+ * detaching a member from a company used to write `organization_id = NULL`,
103
+ * and the account it produced could never transact. The operation is now the
104
+ * move it always meant, and this is the destination. The **binding** is not
105
+ * written here — `customers` writes it through
106
+ * `CustomerAccountLifecycleWritePort.detachToPersonalOrganization` once its
107
+ * own authority and org-administrator-depletion guards have passed.
108
+ */
109
+ provisionPersonalOrganizationFor(customerAccountId: string): Promise<Organization>;
110
+ /**
111
+ * Feature 051, the other end of the same rule — the retention sweep's
112
+ * cascade, moved here with `customers`' Phase-C cut.
113
+ *
114
+ * That module ran it by loading this module's `Organization`, renaming it,
115
+ * blanking its registered address and stamping `deletedAt` — three writes on
116
+ * a row it does not own, plus the audit entry describing them. The decision
117
+ * is this module's: a personal organisation whose single member has been
118
+ * anonymised has nobody in it and carries that person's name.
119
+ *
120
+ * Writes nothing and answers `null` whenever the cascade does not apply, so a
121
+ * caller cannot get it wrong by asking too often.
122
+ */
123
+ anonymizeIfOrphaned(customerAccountId: string): Promise<Organization | null>;
124
+ /**
125
+ * Guard: a personal organization is single-member and MUST never gain a second
126
+ * customer. Call before attaching a customer to an organization.
127
+ */
128
+ assertMembershipAllowed(organizationId: string, em?: EntityManager<import("@mikro-orm/postgresql").AbstractSqlDriver<import("@mikro-orm/postgresql").AbstractSqlConnection, import("@mikro-orm/postgresql").AbstractSqlPlatform>>): Promise<void>;
129
+ }
130
+ //# sourceMappingURL=personal-organization-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"personal-organization-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/personal-organization-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAEL,KAAK,8BAA8B,EACnC,KAAK,uBAAuB,EAC5B,KAAK,qBAAqB,EAC3B,MAAM,4BAA4B,CAAC;AAEpC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AAElE;;;;;;;;GAQG;AACH,MAAM,WAAW,gCAAgC;IAC/C,IAAI,EAAE,uBAAuB,CAAC;IAC9B,KAAK,EAAE,8BAA8B,CAAC;CACvC;AAED;;;;GAIG;AACH,eAAO,MAAM,4BAA4B,qBAAqB,CAAC;AAE/D;;;;GAIG;AACH,qBAAa,2BAA2B;;IAEpC,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B;;;;;OAKG;IACH,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;gBAbT,SAAS,EAAE,MAAM,aAAa;IAC/C;;;;;OAKG;IACc,QAAQ,EAAE,gCAAgC;IAC3D;;;;OAIG;IACc,QAAQ,CAAC,EAAE,SAAS,YAAA;IAGvC,kGAAkG;IAClG,MAAM,CAAC,YAAY,CACjB,OAAO,EAAE,IAAI,CAAC,qBAAqB,EAAE,WAAW,GAAG,UAAU,GAAG,OAAO,CAAC,GACvE,MAAM;IAKT;;;;;;;;;;;;;;;;;OAiBG;IACG,YAAY,CAChB,EAAE,EAAE,aAAa,EACjB,KAAK,EAAE;QACL,iBAAiB,EAAE,MAAM,CAAC;QAC1B,KAAK,EAAE,MAAM,CAAC;QACd,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC1B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KAC1B,GACA,OAAO,CAAC,YAAY,CAAC;IAwBxB;;;;;;;;;;OAUG;IACG,0BAA0B,CAAC,iBAAiB,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAoBlF;;;;;;;;;;;OAWG;IACG,gCAAgC,CAAC,iBAAiB,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAYxF;;;;;;;;;;;;OAYG;IACG,mBAAmB,CAAC,iBAAiB,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAsClF;;;OAGG;IACG,uBAAuB,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE,+KAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;CAkB5F"}