@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.
- package/LICENSE +21 -0
- package/README.md +62 -0
- package/dist/admin/components/OrganizationSalesRepsTab.d.ts +27 -0
- package/dist/admin/components/OrganizationSalesRepsTab.d.ts.map +1 -0
- package/dist/admin/components/OrganizationSalesRepsTab.js +170 -0
- package/dist/admin/components/OrganizationSalesRepsTab.js.map +1 -0
- package/dist/admin/index.d.ts +30 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +59 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/OrganizationDetail.d.ts +9 -0
- package/dist/admin/pages/OrganizationDetail.d.ts.map +1 -0
- package/dist/admin/pages/OrganizationDetail.js +247 -0
- package/dist/admin/pages/OrganizationDetail.js.map +1 -0
- package/dist/admin/pages/OrganizationsList.d.ts +9 -0
- package/dist/admin/pages/OrganizationsList.d.ts.map +1 -0
- package/dist/admin/pages/OrganizationsList.js +73 -0
- package/dist/admin/pages/OrganizationsList.js.map +1 -0
- package/dist/admin/panels/ApplicablePriceListsPanel.d.ts +12 -0
- package/dist/admin/panels/ApplicablePriceListsPanel.d.ts.map +1 -0
- package/dist/admin/panels/ApplicablePriceListsPanel.js +49 -0
- package/dist/admin/panels/ApplicablePriceListsPanel.js.map +1 -0
- package/dist/admin/panels/FulfilmentStrategyPanel.d.ts +16 -0
- package/dist/admin/panels/FulfilmentStrategyPanel.d.ts.map +1 -0
- package/dist/admin/panels/FulfilmentStrategyPanel.js +29 -0
- package/dist/admin/panels/FulfilmentStrategyPanel.js.map +1 -0
- package/dist/admin/panels/HierarchyPanel.d.ts +15 -0
- package/dist/admin/panels/HierarchyPanel.d.ts.map +1 -0
- package/dist/admin/panels/HierarchyPanel.js +65 -0
- package/dist/admin/panels/HierarchyPanel.js.map +1 -0
- package/dist/admin/panels/ModerationActionsPanel.d.ts +28 -0
- package/dist/admin/panels/ModerationActionsPanel.d.ts.map +1 -0
- package/dist/admin/panels/ModerationActionsPanel.js +111 -0
- package/dist/admin/panels/ModerationActionsPanel.js.map +1 -0
- package/dist/admin/panels/RestrictionsPanel.d.ts +24 -0
- package/dist/admin/panels/RestrictionsPanel.d.ts.map +1 -0
- package/dist/admin/panels/RestrictionsPanel.js +121 -0
- package/dist/admin/panels/RestrictionsPanel.js.map +1 -0
- package/dist/admin/panels/VatValidationPanel.d.ts +28 -0
- package/dist/admin/panels/VatValidationPanel.d.ts.map +1 -0
- package/dist/admin/panels/VatValidationPanel.js +79 -0
- package/dist/admin/panels/VatValidationPanel.js.map +1 -0
- package/dist/backend/commands/move.command.d.ts +11 -0
- package/dist/backend/commands/move.command.d.ts.map +1 -0
- package/dist/backend/commands/move.command.js +11 -0
- package/dist/backend/commands/move.command.js.map +1 -0
- package/dist/backend/commands/set-cart-approval-policy.command.d.ts +31 -0
- package/dist/backend/commands/set-cart-approval-policy.command.d.ts.map +1 -0
- package/dist/backend/commands/set-cart-approval-policy.command.js +56 -0
- package/dist/backend/commands/set-cart-approval-policy.command.js.map +1 -0
- package/dist/backend/commands/set-credit-mode.command.d.ts +16 -0
- package/dist/backend/commands/set-credit-mode.command.d.ts.map +1 -0
- package/dist/backend/commands/set-credit-mode.command.js +25 -0
- package/dist/backend/commands/set-credit-mode.command.js.map +1 -0
- package/dist/backend/commands/set-parent.command.d.ts +26 -0
- package/dist/backend/commands/set-parent.command.d.ts.map +1 -0
- package/dist/backend/commands/set-parent.command.js +51 -0
- package/dist/backend/commands/set-parent.command.js.map +1 -0
- package/dist/backend/demo/reset.d.ts +20 -0
- package/dist/backend/demo/reset.d.ts.map +1 -0
- package/dist/backend/demo/reset.js +11 -0
- package/dist/backend/demo/reset.js.map +1 -0
- package/dist/backend/demo/rows.d.ts +44 -0
- package/dist/backend/demo/rows.d.ts.map +1 -0
- package/dist/backend/demo/rows.js +44 -0
- package/dist/backend/demo/rows.js.map +1 -0
- package/dist/backend/demo/seed.d.ts +20 -0
- package/dist/backend/demo/seed.d.ts.map +1 -0
- package/dist/backend/demo/seed.js +28 -0
- package/dist/backend/demo/seed.js.map +1 -0
- package/dist/backend/email-templates/invitation.d.ts +23 -0
- package/dist/backend/email-templates/invitation.d.ts.map +1 -0
- package/dist/backend/email-templates/invitation.js +35 -0
- package/dist/backend/email-templates/invitation.js.map +1 -0
- package/dist/backend/email-templates/transactional-defaults.d.ts +54 -0
- package/dist/backend/email-templates/transactional-defaults.d.ts.map +1 -0
- package/dist/backend/email-templates/transactional-defaults.js +242 -0
- package/dist/backend/email-templates/transactional-defaults.js.map +1 -0
- package/dist/backend/email-templates/verification.d.ts +14 -0
- package/dist/backend/email-templates/verification.d.ts.map +1 -0
- package/dist/backend/email-templates/verification.js +34 -0
- package/dist/backend/email-templates/verification.js.map +1 -0
- package/dist/backend/entities/email-verification-token.entity.d.ts +17 -0
- package/dist/backend/entities/email-verification-token.entity.d.ts.map +1 -0
- package/dist/backend/entities/email-verification-token.entity.js +60 -0
- package/dist/backend/entities/email-verification-token.entity.js.map +1 -0
- package/dist/backend/entities/organization-delivery-method-link.entity.d.ts +14 -0
- package/dist/backend/entities/organization-delivery-method-link.entity.d.ts.map +1 -0
- package/dist/backend/entities/organization-delivery-method-link.entity.js +41 -0
- package/dist/backend/entities/organization-delivery-method-link.entity.js.map +1 -0
- package/dist/backend/entities/organization-invitation.entity.d.ts +21 -0
- package/dist/backend/entities/organization-invitation.entity.d.ts.map +1 -0
- package/dist/backend/entities/organization-invitation.entity.js +85 -0
- package/dist/backend/entities/organization-invitation.entity.js.map +1 -0
- package/dist/backend/entities/organization-payment-method-link.entity.d.ts +14 -0
- package/dist/backend/entities/organization-payment-method-link.entity.d.ts.map +1 -0
- package/dist/backend/entities/organization-payment-method-link.entity.js +41 -0
- package/dist/backend/entities/organization-payment-method-link.entity.js.map +1 -0
- package/dist/backend/entities/organization-sales-rep-assignment.entity.d.ts +17 -0
- package/dist/backend/entities/organization-sales-rep-assignment.entity.d.ts.map +1 -0
- package/dist/backend/entities/organization-sales-rep-assignment.entity.js +56 -0
- package/dist/backend/entities/organization-sales-rep-assignment.entity.js.map +1 -0
- package/dist/backend/entities/organization-tax-id-validation.entity.d.ts +29 -0
- package/dist/backend/entities/organization-tax-id-validation.entity.d.ts.map +1 -0
- package/dist/backend/entities/organization-tax-id-validation.entity.js +78 -0
- package/dist/backend/entities/organization-tax-id-validation.entity.js.map +1 -0
- package/dist/backend/entities/organization-warehouse-link.entity.d.ts +15 -0
- package/dist/backend/entities/organization-warehouse-link.entity.d.ts.map +1 -0
- package/dist/backend/entities/organization-warehouse-link.entity.js +42 -0
- package/dist/backend/entities/organization-warehouse-link.entity.js.map +1 -0
- package/dist/backend/entities/organization.entity.d.ts +125 -0
- package/dist/backend/entities/organization.entity.d.ts.map +1 -0
- package/dist/backend/entities/organization.entity.js +282 -0
- package/dist/backend/entities/organization.entity.js.map +1 -0
- package/dist/backend/index.d.ts +250 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +506 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/integrations/ministerstwo-finansow-client.d.ts +22 -0
- package/dist/backend/integrations/ministerstwo-finansow-client.d.ts.map +1 -0
- package/dist/backend/integrations/ministerstwo-finansow-client.js +172 -0
- package/dist/backend/integrations/ministerstwo-finansow-client.js.map +1 -0
- package/dist/backend/integrations/vies-client.d.ts +18 -0
- package/dist/backend/integrations/vies-client.d.ts.map +1 -0
- package/dist/backend/integrations/vies-client.js +133 -0
- package/dist/backend/integrations/vies-client.js.map +1 -0
- package/dist/backend/plugin.d.ts +126 -0
- package/dist/backend/plugin.d.ts.map +1 -0
- package/dist/backend/plugin.js +139 -0
- package/dist/backend/plugin.js.map +1 -0
- package/dist/backend/routes.admin.d.ts +66 -0
- package/dist/backend/routes.admin.d.ts.map +1 -0
- package/dist/backend/routes.admin.js +711 -0
- package/dist/backend/routes.admin.js.map +1 -0
- package/dist/backend/routes.customer.d.ts +33 -0
- package/dist/backend/routes.customer.d.ts.map +1 -0
- package/dist/backend/routes.customer.js +139 -0
- package/dist/backend/routes.customer.js.map +1 -0
- package/dist/backend/routes.members.d.ts +31 -0
- package/dist/backend/routes.members.d.ts.map +1 -0
- package/dist/backend/routes.members.js +109 -0
- package/dist/backend/routes.members.js.map +1 -0
- package/dist/backend/routes.public.d.ts +49 -0
- package/dist/backend/routes.public.d.ts.map +1 -0
- package/dist/backend/routes.public.js +220 -0
- package/dist/backend/routes.public.js.map +1 -0
- package/dist/backend/routes.sales-reps.d.ts +46 -0
- package/dist/backend/routes.sales-reps.d.ts.map +1 -0
- package/dist/backend/routes.sales-reps.js +114 -0
- package/dist/backend/routes.sales-reps.js.map +1 -0
- package/dist/backend/routes.storefront.d.ts +25 -0
- package/dist/backend/routes.storefront.d.ts.map +1 -0
- package/dist/backend/routes.storefront.js +65 -0
- package/dist/backend/routes.storefront.js.map +1 -0
- package/dist/backend/schemas/organization.d.ts +59 -0
- package/dist/backend/schemas/organization.d.ts.map +1 -0
- package/dist/backend/schemas/organization.js +55 -0
- package/dist/backend/schemas/organization.js.map +1 -0
- package/dist/backend/schemas/settings.d.ts +30 -0
- package/dist/backend/schemas/settings.d.ts.map +1 -0
- package/dist/backend/schemas/settings.js +35 -0
- package/dist/backend/schemas/settings.js.map +1 -0
- package/dist/backend/services/email-verification-service.d.ts +45 -0
- package/dist/backend/services/email-verification-service.d.ts.map +1 -0
- package/dist/backend/services/email-verification-service.js +95 -0
- package/dist/backend/services/email-verification-service.js.map +1 -0
- package/dist/backend/services/invitation-service.d.ts +85 -0
- package/dist/backend/services/invitation-service.d.ts.map +1 -0
- package/dist/backend/services/invitation-service.js +266 -0
- package/dist/backend/services/invitation-service.js.map +1 -0
- package/dist/backend/services/normalize-name.d.ts +14 -0
- package/dist/backend/services/normalize-name.d.ts.map +1 -0
- package/dist/backend/services/normalize-name.js +14 -0
- package/dist/backend/services/normalize-name.js.map +1 -0
- package/dist/backend/services/org-registration-notifier.d.ts +36 -0
- package/dist/backend/services/org-registration-notifier.d.ts.map +1 -0
- package/dist/backend/services/org-registration-notifier.js +109 -0
- package/dist/backend/services/org-registration-notifier.js.map +1 -0
- package/dist/backend/services/org-template-email.d.ts +18 -0
- package/dist/backend/services/org-template-email.d.ts.map +1 -0
- package/dist/backend/services/org-template-email.js +16 -0
- package/dist/backend/services/org-template-email.js.map +1 -0
- package/dist/backend/services/organization-context-service.d.ts +52 -0
- package/dist/backend/services/organization-context-service.d.ts.map +1 -0
- package/dist/backend/services/organization-context-service.js +70 -0
- package/dist/backend/services/organization-context-service.js.map +1 -0
- package/dist/backend/services/organization-country-reference.d.ts +11 -0
- package/dist/backend/services/organization-country-reference.d.ts.map +1 -0
- package/dist/backend/services/organization-country-reference.js +24 -0
- package/dist/backend/services/organization-country-reference.js.map +1 -0
- package/dist/backend/services/organization-details-port.d.ts +33 -0
- package/dist/backend/services/organization-details-port.d.ts.map +1 -0
- package/dist/backend/services/organization-details-port.js +95 -0
- package/dist/backend/services/organization-details-port.js.map +1 -0
- package/dist/backend/services/organization-effective-pricelists-service.d.ts +50 -0
- package/dist/backend/services/organization-effective-pricelists-service.d.ts.map +1 -0
- package/dist/backend/services/organization-effective-pricelists-service.js +68 -0
- package/dist/backend/services/organization-effective-pricelists-service.js.map +1 -0
- package/dist/backend/services/organization-inheritance-service.d.ts +73 -0
- package/dist/backend/services/organization-inheritance-service.d.ts.map +1 -0
- package/dist/backend/services/organization-inheritance-service.js +63 -0
- package/dist/backend/services/organization-inheritance-service.js.map +1 -0
- package/dist/backend/services/organization-moderation-service.d.ts +124 -0
- package/dist/backend/services/organization-moderation-service.d.ts.map +1 -0
- package/dist/backend/services/organization-moderation-service.js +337 -0
- package/dist/backend/services/organization-moderation-service.js.map +1 -0
- package/dist/backend/services/organization-restriction-service.d.ts +70 -0
- package/dist/backend/services/organization-restriction-service.d.ts.map +1 -0
- package/dist/backend/services/organization-restriction-service.js +210 -0
- package/dist/backend/services/organization-restriction-service.js.map +1 -0
- package/dist/backend/services/organization-tax-id-validation-service.d.ts +59 -0
- package/dist/backend/services/organization-tax-id-validation-service.d.ts.map +1 -0
- package/dist/backend/services/organization-tax-id-validation-service.js +115 -0
- package/dist/backend/services/organization-tax-id-validation-service.js.map +1 -0
- package/dist/backend/services/organization-tree-service.d.ts +102 -0
- package/dist/backend/services/organization-tree-service.d.ts.map +1 -0
- package/dist/backend/services/organization-tree-service.js +184 -0
- package/dist/backend/services/organization-tree-service.js.map +1 -0
- package/dist/backend/services/personal-organization-service.d.ts +130 -0
- package/dist/backend/services/personal-organization-service.d.ts.map +1 -0
- package/dist/backend/services/personal-organization-service.js +207 -0
- package/dist/backend/services/personal-organization-service.js.map +1 -0
- package/dist/backend/services/registration-service.d.ts +80 -0
- package/dist/backend/services/registration-service.d.ts.map +1 -0
- package/dist/backend/services/registration-service.js +126 -0
- package/dist/backend/services/registration-service.js.map +1 -0
- package/dist/backend/services/sales-rep-assignment-service.d.ts +89 -0
- package/dist/backend/services/sales-rep-assignment-service.d.ts.map +1 -0
- package/dist/backend/services/sales-rep-assignment-service.js +176 -0
- package/dist/backend/services/sales-rep-assignment-service.js.map +1 -0
- package/dist/backend/services/vat-validator-port.d.ts +20 -0
- package/dist/backend/services/vat-validator-port.d.ts.map +1 -0
- package/dist/backend/services/vat-validator-port.js +2 -0
- package/dist/backend/services/vat-validator-port.js.map +1 -0
- package/dist/manifest.d.ts +176 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +503 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260424T205317_organizations_init.d.ts +13 -0
- package/dist/migrations/20260424T205317_organizations_init.d.ts.map +1 -0
- package/dist/migrations/20260424T205317_organizations_init.js +100 -0
- package/dist/migrations/20260424T205317_organizations_init.js.map +1 -0
- package/dist/migrations/20260425T051700_organizations_invitations_init.d.ts +13 -0
- package/dist/migrations/20260425T051700_organizations_invitations_init.d.ts.map +1 -0
- package/dist/migrations/20260425T051700_organizations_invitations_init.js +39 -0
- package/dist/migrations/20260425T051700_organizations_invitations_init.js.map +1 -0
- package/dist/migrations/20260611T140349_organizations_consolidation.d.ts +35 -0
- package/dist/migrations/20260611T140349_organizations_consolidation.d.ts.map +1 -0
- package/dist/migrations/20260611T140349_organizations_consolidation.js +180 -0
- package/dist/migrations/20260611T140349_organizations_consolidation.js.map +1 -0
- package/dist/migrations/20260611T140358_organizations_org_order_confirmation_emails.d.ts +10 -0
- package/dist/migrations/20260611T140358_organizations_org_order_confirmation_emails.d.ts.map +1 -0
- package/dist/migrations/20260611T140358_organizations_org_order_confirmation_emails.js +14 -0
- package/dist/migrations/20260611T140358_organizations_org_order_confirmation_emails.js.map +1 -0
- package/dist/migrations/20260611T140402_organizations_org_fulfilment_strategy.d.ts +14 -0
- package/dist/migrations/20260611T140402_organizations_org_fulfilment_strategy.d.ts.map +1 -0
- package/dist/migrations/20260611T140402_organizations_org_fulfilment_strategy.js +20 -0
- package/dist/migrations/20260611T140402_organizations_org_fulfilment_strategy.js.map +1 -0
- package/dist/migrations/20260717T151403_organizations_personal_organizations.d.ts +24 -0
- package/dist/migrations/20260717T151403_organizations_personal_organizations.d.ts.map +1 -0
- package/dist/migrations/20260717T151403_organizations_personal_organizations.js +63 -0
- package/dist/migrations/20260717T151403_organizations_personal_organizations.js.map +1 -0
- package/dist/migrations/20260718T200340_organizations_organization_custom_field_values.d.ts +12 -0
- package/dist/migrations/20260718T200340_organizations_organization_custom_field_values.d.ts.map +1 -0
- package/dist/migrations/20260718T200340_organizations_organization_custom_field_values.js +16 -0
- package/dist/migrations/20260718T200340_organizations_organization_custom_field_values.js.map +1 -0
- package/dist/migrations/20260720T044254_organizations_org_hierarchy.d.ts +23 -0
- package/dist/migrations/20260720T044254_organizations_org_hierarchy.d.ts.map +1 -0
- package/dist/migrations/20260720T044254_organizations_org_hierarchy.js +37 -0
- package/dist/migrations/20260720T044254_organizations_org_hierarchy.js.map +1 -0
- package/dist/migrations/20260912T094646_organizations_sales_channel_organizations.d.ts +27 -0
- package/dist/migrations/20260912T094646_organizations_sales_channel_organizations.d.ts.map +1 -0
- package/dist/migrations/20260912T094646_organizations_sales_channel_organizations.js +44 -0
- package/dist/migrations/20260912T094646_organizations_sales_channel_organizations.js.map +1 -0
- package/dist/migrations/index.d.ts +35 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +45 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/ports/index.d.ts +53 -0
- package/dist/ports/index.d.ts.map +1 -0
- package/dist/ports/index.js +2 -0
- package/dist/ports/index.js.map +1 -0
- package/docs/organization-hierarchy.md +117 -0
- package/docs/organizations.md +312 -0
- package/i18n/en.json +17 -0
- package/i18n/pl.json +17 -0
- package/package.json +107 -0
- 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"}
|