@endora-commerce/mod-customer-accounts 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 +59 -0
- package/dist/admin/api/customer-groups-client.d.ts +27 -0
- package/dist/admin/api/customer-groups-client.d.ts.map +1 -0
- package/dist/admin/api/customer-groups-client.js +17 -0
- package/dist/admin/api/customer-groups-client.js.map +1 -0
- package/dist/admin/index.d.ts +33 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +38 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/CustomerGroupsPage.d.ts +18 -0
- package/dist/admin/pages/CustomerGroupsPage.d.ts.map +1 -0
- package/dist/admin/pages/CustomerGroupsPage.js +108 -0
- package/dist/admin/pages/CustomerGroupsPage.js.map +1 -0
- package/dist/backend/entities/customer-account.entity.d.ts +111 -0
- package/dist/backend/entities/customer-account.entity.d.ts.map +1 -0
- package/dist/backend/entities/customer-account.entity.js +230 -0
- package/dist/backend/entities/customer-account.entity.js.map +1 -0
- package/dist/backend/entities/customer-group.entity.d.ts +29 -0
- package/dist/backend/entities/customer-group.entity.d.ts.map +1 -0
- package/dist/backend/entities/customer-group.entity.js +70 -0
- package/dist/backend/entities/customer-group.entity.js.map +1 -0
- package/dist/backend/entities/password-reset-token.entity.d.ts +16 -0
- package/dist/backend/entities/password-reset-token.entity.d.ts.map +1 -0
- package/dist/backend/entities/password-reset-token.entity.js +59 -0
- package/dist/backend/entities/password-reset-token.entity.js.map +1 -0
- package/dist/backend/index.d.ts +144 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +407 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/routes.admin.d.ts +26 -0
- package/dist/backend/routes.admin.d.ts.map +1 -0
- package/dist/backend/routes.admin.js +35 -0
- package/dist/backend/routes.admin.js.map +1 -0
- package/dist/backend/services/audit-references.d.ts +22 -0
- package/dist/backend/services/audit-references.d.ts.map +1 -0
- package/dist/backend/services/audit-references.js +46 -0
- package/dist/backend/services/audit-references.js.map +1 -0
- package/dist/backend/services/customer-account-lifecycle-ports.d.ts +168 -0
- package/dist/backend/services/customer-account-lifecycle-ports.d.ts.map +1 -0
- package/dist/backend/services/customer-account-lifecycle-ports.js +420 -0
- package/dist/backend/services/customer-account-lifecycle-ports.js.map +1 -0
- package/dist/backend/services/customer-account-ports.d.ts +143 -0
- package/dist/backend/services/customer-account-ports.d.ts.map +1 -0
- package/dist/backend/services/customer-account-ports.js +422 -0
- package/dist/backend/services/customer-account-ports.js.map +1 -0
- package/dist/backend/services/customer-auth-service.d.ts +62 -0
- package/dist/backend/services/customer-auth-service.d.ts.map +1 -0
- package/dist/backend/services/customer-auth-service.js +105 -0
- package/dist/backend/services/customer-auth-service.js.map +1 -0
- package/dist/backend/services/customer-group-read-port.d.ts +24 -0
- package/dist/backend/services/customer-group-read-port.d.ts.map +1 -0
- package/dist/backend/services/customer-group-read-port.js +44 -0
- package/dist/backend/services/customer-group-read-port.js.map +1 -0
- package/dist/backend/services/customer-group-service.d.ts +21 -0
- package/dist/backend/services/customer-group-service.d.ts.map +1 -0
- package/dist/backend/services/customer-group-service.js +97 -0
- package/dist/backend/services/customer-group-service.js.map +1 -0
- package/dist/backend/services/customer-rollup-scope.d.ts +30 -0
- package/dist/backend/services/customer-rollup-scope.d.ts.map +1 -0
- package/dist/backend/services/customer-rollup-scope.js +42 -0
- package/dist/backend/services/customer-rollup-scope.js.map +1 -0
- package/dist/backend/services/password-reset-service.d.ts +15 -0
- package/dist/backend/services/password-reset-service.d.ts.map +1 -0
- package/dist/backend/services/password-reset-service.js +90 -0
- package/dist/backend/services/password-reset-service.js.map +1 -0
- package/dist/backend/services/role-service.d.ts +22 -0
- package/dist/backend/services/role-service.d.ts.map +1 -0
- package/dist/backend/services/role-service.js +83 -0
- package/dist/backend/services/role-service.js.map +1 -0
- package/dist/backend/services/two-factor-enrolments.d.ts +18 -0
- package/dist/backend/services/two-factor-enrolments.d.ts.map +1 -0
- package/dist/backend/services/two-factor-enrolments.js +2 -0
- package/dist/backend/services/two-factor-enrolments.js.map +1 -0
- package/dist/manifest.d.ts +169 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +212 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260425T055041_customer_accounts_password_reset_tokens.d.ts +6 -0
- package/dist/migrations/20260425T055041_customer_accounts_password_reset_tokens.d.ts.map +1 -0
- package/dist/migrations/20260425T055041_customer_accounts_password_reset_tokens.js +25 -0
- package/dist/migrations/20260425T055041_customer_accounts_password_reset_tokens.js.map +1 -0
- package/dist/migrations/20260611T140351_customer_accounts_organization_optional.d.ts +34 -0
- package/dist/migrations/20260611T140351_customer_accounts_organization_optional.d.ts.map +1 -0
- package/dist/migrations/20260611T140351_customer_accounts_organization_optional.js +43 -0
- package/dist/migrations/20260611T140351_customer_accounts_organization_optional.js.map +1 -0
- package/dist/migrations/20260611T140403_customer_accounts_lifecycle.d.ts +54 -0
- package/dist/migrations/20260611T140403_customer_accounts_lifecycle.d.ts.map +1 -0
- package/dist/migrations/20260611T140403_customer_accounts_lifecycle.js +97 -0
- package/dist/migrations/20260611T140403_customer_accounts_lifecycle.js.map +1 -0
- package/dist/migrations/20260718T200341_customer_accounts_customer_account_custom_field_values.d.ts +12 -0
- package/dist/migrations/20260718T200341_customer_accounts_customer_account_custom_field_values.d.ts.map +1 -0
- package/dist/migrations/20260718T200341_customer_accounts_customer_account_custom_field_values.js +16 -0
- package/dist/migrations/20260718T200341_customer_accounts_customer_account_custom_field_values.js.map +1 -0
- package/dist/migrations/20260720T044255_customer_accounts_customer_subtree_rollup.d.ts +14 -0
- package/dist/migrations/20260720T044255_customer_accounts_customer_subtree_rollup.d.ts.map +1 -0
- package/dist/migrations/20260720T044255_customer_accounts_customer_subtree_rollup.js +18 -0
- package/dist/migrations/20260720T044255_customer_accounts_customer_subtree_rollup.js.map +1 -0
- package/dist/migrations/20260819T074816_customer_accounts_password_set_at.d.ts +22 -0
- package/dist/migrations/20260819T074816_customer_accounts_password_set_at.d.ts.map +1 -0
- package/dist/migrations/20260819T074816_customer_accounts_password_set_at.js +26 -0
- package/dist/migrations/20260819T074816_customer_accounts_password_set_at.js.map +1 -0
- package/dist/migrations/20260819T142837_customer_accounts_fold_email_case.d.ts +46 -0
- package/dist/migrations/20260819T142837_customer_accounts_fold_email_case.d.ts.map +1 -0
- package/dist/migrations/20260819T142837_customer_accounts_fold_email_case.js +97 -0
- package/dist/migrations/20260819T142837_customer_accounts_fold_email_case.js.map +1 -0
- package/dist/migrations/20260825T124759_customer_accounts_drop_legacy_two_factor_secret.d.ts +31 -0
- package/dist/migrations/20260825T124759_customer_accounts_drop_legacy_two_factor_secret.d.ts.map +1 -0
- package/dist/migrations/20260825T124759_customer_accounts_drop_legacy_two_factor_secret.js +35 -0
- package/dist/migrations/20260825T124759_customer_accounts_drop_legacy_two_factor_secret.js.map +1 -0
- package/dist/migrations/20260825T141659_customer_accounts_organization_required.d.ts +58 -0
- package/dist/migrations/20260825T141659_customer_accounts_organization_required.d.ts.map +1 -0
- package/dist/migrations/20260825T141659_customer_accounts_organization_required.js +133 -0
- package/dist/migrations/20260825T141659_customer_accounts_organization_required.js.map +1 -0
- package/dist/migrations/20260912T094701_customer_accounts_sales_channel_customer_accounts.d.ts +27 -0
- package/dist/migrations/20260912T094701_customer_accounts_sales_channel_customer_accounts.d.ts.map +1 -0
- package/dist/migrations/20260912T094701_customer_accounts_sales_channel_customer_accounts.js +44 -0
- package/dist/migrations/20260912T094701_customer_accounts_sales_channel_customer_accounts.js.map +1 -0
- package/dist/migrations/index.d.ts +36 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +47 -0
- package/dist/migrations/index.js.map +1 -0
- package/docs/customer_accounts.md +38 -0
- package/i18n/en.json +30 -0
- package/i18n/pl.json +30 -0
- package/package.json +94 -0
- package/tailwind.css +14 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import { type CustomerAccountAdminSearchCriteria, type CustomerAccountAdminSearchPort, type CustomerAccountAdminSearchResult, type CustomerAccountLifecycleWritePort, type CustomerAccountRecord, type CustomerAccountStandaloneCreateInput, type CustomerAccountWriteRequestMeta } from '@endora-commerce/contracts';
|
|
3
|
+
import type { CommandBus } from '@endora-commerce/platform/commands';
|
|
4
|
+
import type { AuditPort } from '@endora-commerce/platform/kernel';
|
|
5
|
+
import type { PersonalOrganizationProvisionApi } from '@endora-commerce/mod-organizations/ports';
|
|
6
|
+
import type { TwoFactorEnrolmentReader } from './two-factor-enrolments.js';
|
|
7
|
+
/**
|
|
8
|
+
* The account lifecycle `customers` runs over this module's table
|
|
9
|
+
* (feature 075, Phase C).
|
|
10
|
+
*
|
|
11
|
+
* It ran it by loading and mutating the `CustomerAccount` entity in five of its
|
|
12
|
+
* own services and two route files — block, unblock, soft-delete, restore,
|
|
13
|
+
* anonymise, organisation assignment, group assignment, the standalone
|
|
14
|
+
* registration and the admin custom-field patch. Each is one method here, each
|
|
15
|
+
* one unit of work on this table alone, because a write port that took the
|
|
16
|
+
* caller's `EntityManager` would publish the coupling instead of removing it
|
|
17
|
+
* (D-78).
|
|
18
|
+
*
|
|
19
|
+
* **The policy stayed with the caller.** Whether the acting staff member may
|
|
20
|
+
* touch this customer, and whether the write would strand an organisation
|
|
21
|
+
* without an administrator, are `customers`' questions and are still asked
|
|
22
|
+
* there; the counting half of the second is
|
|
23
|
+
* `CustomerAccountReadPort.countByOrganizationRole`, which already existed.
|
|
24
|
+
*
|
|
25
|
+
* **The audit row came here** — one row, with the action string the caller used
|
|
26
|
+
* to write, so nothing an operator can see moved. Unlike
|
|
27
|
+
* `CustomerAccountMemberWriteService` the caller keeps no second row: there,
|
|
28
|
+
* "an operator edited this member on the organisation panel" and "this
|
|
29
|
+
* account's e-mail changed" are two facts; here "an operator blocked this
|
|
30
|
+
* customer" *is* the write, and recording it twice would be a defect rather
|
|
31
|
+
* than a pair.
|
|
32
|
+
*/
|
|
33
|
+
/** The name a scrubbed account's first and last name are replaced with. */
|
|
34
|
+
export declare const ANONYMIZED_CUSTOMER_NAME = "Deleted customer";
|
|
35
|
+
/**
|
|
36
|
+
* The placeholder written over `password_hash`. It is deliberately not a hash
|
|
37
|
+
* of anything: the column is NOT NULL, and nothing may authenticate against a
|
|
38
|
+
* scrubbed account. `passwordSetAt` is cleared beside it (issue #222).
|
|
39
|
+
*/
|
|
40
|
+
export declare const ANONYMIZED_PASSWORD_HASH = "anonymized-no-login";
|
|
41
|
+
export declare class CustomerAccountLifecycleWriteService implements CustomerAccountLifecycleWritePort {
|
|
42
|
+
#private;
|
|
43
|
+
private readonly emFactory;
|
|
44
|
+
private readonly auditLog;
|
|
45
|
+
/**
|
|
46
|
+
* `mfa`'s answer to which of these accounts hold a second factor. Every
|
|
47
|
+
* method here answers with a `CustomerAccountRecord`, and that record
|
|
48
|
+
* carries `twoFactorEnabled` — so the alternative to threading it is a
|
|
49
|
+
* record that says `false` about an enrolled buyer, which is the defect
|
|
50
|
+
* this parameter exists to remove.
|
|
51
|
+
*/
|
|
52
|
+
private readonly twoFactorEnrolments;
|
|
53
|
+
/**
|
|
54
|
+
* D-178 — `organizations`' co-transactional provisioning seam, held here
|
|
55
|
+
* because {@link createStandalone} writes the account **and** the individual's
|
|
56
|
+
* personal organisation in one transaction. See
|
|
57
|
+
* `organizations/ports/personal-organization-provision.ts` for the foreign
|
|
58
|
+
* key that makes that a requirement rather than a preference.
|
|
59
|
+
*/
|
|
60
|
+
private readonly personalOrganizations;
|
|
61
|
+
/**
|
|
62
|
+
* Only {@link setCustomFieldValues} needs it, because that one write is a
|
|
63
|
+
* read-modify-write and has to stay in one transaction. The method says so
|
|
64
|
+
* and refuses without it.
|
|
65
|
+
*/
|
|
66
|
+
private readonly commandBus?;
|
|
67
|
+
constructor(emFactory: () => EntityManager, auditLog: AuditPort,
|
|
68
|
+
/**
|
|
69
|
+
* `mfa`'s answer to which of these accounts hold a second factor. Every
|
|
70
|
+
* method here answers with a `CustomerAccountRecord`, and that record
|
|
71
|
+
* carries `twoFactorEnabled` — so the alternative to threading it is a
|
|
72
|
+
* record that says `false` about an enrolled buyer, which is the defect
|
|
73
|
+
* this parameter exists to remove.
|
|
74
|
+
*/
|
|
75
|
+
twoFactorEnrolments: TwoFactorEnrolmentReader,
|
|
76
|
+
/**
|
|
77
|
+
* D-178 — `organizations`' co-transactional provisioning seam, held here
|
|
78
|
+
* because {@link createStandalone} writes the account **and** the individual's
|
|
79
|
+
* personal organisation in one transaction. See
|
|
80
|
+
* `organizations/ports/personal-organization-provision.ts` for the foreign
|
|
81
|
+
* key that makes that a requirement rather than a preference.
|
|
82
|
+
*/
|
|
83
|
+
personalOrganizations: PersonalOrganizationProvisionApi,
|
|
84
|
+
/**
|
|
85
|
+
* Only {@link setCustomFieldValues} needs it, because that one write is a
|
|
86
|
+
* read-modify-write and has to stay in one transaction. The method says so
|
|
87
|
+
* and refuses without it.
|
|
88
|
+
*/
|
|
89
|
+
commandBus?: CommandBus | undefined);
|
|
90
|
+
/**
|
|
91
|
+
* Self-registration: the account row **and** the tenant that scopes it, in one
|
|
92
|
+
* transaction (D-178).
|
|
93
|
+
*
|
|
94
|
+
* It used to be two committed units of work — this method wrote
|
|
95
|
+
* `organizationId: null` and flushed, and `customers`' registration service
|
|
96
|
+
* then called `personalOrganizationPort.ensureForCustomerAccount`. A failure
|
|
97
|
+
* between them (the port disabled, the database blipping, the process killed)
|
|
98
|
+
* left a durable account with no organisation, no session for the visitor to
|
|
99
|
+
* retry from, and nothing anywhere that re-ran the provisioning. That is one
|
|
100
|
+
* of the three producers D-178 closed, and the column is `NOT NULL` now, so
|
|
101
|
+
* the old order would fail at the insert rather than leave a bad row.
|
|
102
|
+
*
|
|
103
|
+
* The account id is generated here rather than by the entity default because
|
|
104
|
+
* the organisation has to exist first — its `taxId` is derived from that id,
|
|
105
|
+
* which is what makes the provisioning re-runnable — and the account's foreign
|
|
106
|
+
* key has to point at it.
|
|
107
|
+
*/
|
|
108
|
+
createStandalone(input: CustomerAccountStandaloneCreateInput): Promise<CustomerAccountRecord>;
|
|
109
|
+
block(customerAccountId: string, input: {
|
|
110
|
+
actorAdminUserId: string;
|
|
111
|
+
reason?: string | null;
|
|
112
|
+
audit?: CustomerAccountWriteRequestMeta;
|
|
113
|
+
}): Promise<CustomerAccountRecord>;
|
|
114
|
+
unblock(customerAccountId: string, input: {
|
|
115
|
+
actorAdminUserId: string;
|
|
116
|
+
audit?: CustomerAccountWriteRequestMeta;
|
|
117
|
+
}): Promise<CustomerAccountRecord>;
|
|
118
|
+
softDelete(customerAccountId: string, input: {
|
|
119
|
+
actorAdminUserId: string;
|
|
120
|
+
}): Promise<CustomerAccountRecord>;
|
|
121
|
+
restore(customerAccountId: string, input: {
|
|
122
|
+
actorAdminUserId: string;
|
|
123
|
+
}): Promise<CustomerAccountRecord>;
|
|
124
|
+
setOrganization(customerAccountId: string, organizationId: string, input: {
|
|
125
|
+
actorAdminUserId: string;
|
|
126
|
+
}): Promise<CustomerAccountRecord>;
|
|
127
|
+
/**
|
|
128
|
+
* D-178 — an operator detaching a member from a company.
|
|
129
|
+
*
|
|
130
|
+
* The write is byte-identical to {@link setOrganization}'s; only the audit
|
|
131
|
+
* verb differs, and it is the verb this operation has always recorded. What
|
|
132
|
+
* changed is what it writes: `organization_id = NULL` until D-178, which left
|
|
133
|
+
* an account that could not place an order, submit an RFQ or read an address —
|
|
134
|
+
* and which the column now refuses outright.
|
|
135
|
+
*
|
|
136
|
+
* The destination is the account's own personal organisation, resolved by the
|
|
137
|
+
* caller through `organizations`. This module does not resolve it, because
|
|
138
|
+
* that would make the account table's owner responsible for the tenancy
|
|
139
|
+
* policy of the module it points at.
|
|
140
|
+
*/
|
|
141
|
+
detachToPersonalOrganization(customerAccountId: string, personalOrganizationId: string, input: {
|
|
142
|
+
actorAdminUserId: string;
|
|
143
|
+
}): Promise<CustomerAccountRecord>;
|
|
144
|
+
setCustomerGroup(customerAccountId: string, customerGroupId: string | null, input: {
|
|
145
|
+
actorAdminUserId: string;
|
|
146
|
+
}): Promise<CustomerAccountRecord>;
|
|
147
|
+
setCustomFieldValues(customerAccountId: string, merge: (current: Record<string, unknown>) => Promise<Record<string, unknown>>): Promise<CustomerAccountRecord>;
|
|
148
|
+
listDueForAnonymization(cutoff: Date): Promise<CustomerAccountRecord[]>;
|
|
149
|
+
anonymize(customerAccountId: string): Promise<CustomerAccountRecord>;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The admin customer list (feature 040, US5).
|
|
153
|
+
*
|
|
154
|
+
* `customers` built the whole query itself — the lifecycle filter, the
|
|
155
|
+
* sales-rep visibility scope, the free-text predicate and the page — against
|
|
156
|
+
* this module's entity. The **shape** of the answer is that module's screen and
|
|
157
|
+
* the **statement** is this module's table, so the criteria cross as plain data
|
|
158
|
+
* and the SQL stays here. It has to be one statement: narrowing a page after
|
|
159
|
+
* the fact returns fewer rows than the caller asked for, and paging after
|
|
160
|
+
* narrowing reads the whole table.
|
|
161
|
+
*/
|
|
162
|
+
export declare class CustomerAccountAdminSearchService implements CustomerAccountAdminSearchPort {
|
|
163
|
+
private readonly emFactory;
|
|
164
|
+
private readonly twoFactorEnrolments;
|
|
165
|
+
constructor(emFactory: () => EntityManager, twoFactorEnrolments: TwoFactorEnrolmentReader);
|
|
166
|
+
search(criteria: CustomerAccountAdminSearchCriteria): Promise<CustomerAccountAdminSearchResult>;
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=customer-account-lifecycle-ports.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"customer-account-lifecycle-ports.d.ts","sourceRoot":"","sources":["../../../src/backend/services/customer-account-lifecycle-ports.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { UniqueConstraintViolationException } from '@mikro-orm/core';
|
|
3
|
+
import { ERROR_CODES, normalizeEmailAddress, } from '@endora-commerce/contracts';
|
|
4
|
+
import { recordAuditFromContext } from '@endora-commerce/platform/commands';
|
|
5
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
6
|
+
import { hashPassword } from '@endora-commerce/platform/kernel';
|
|
7
|
+
import { CustomerAccount } from '../entities/customer-account.entity.js';
|
|
8
|
+
import { toCustomerAccountRecord, toCustomerAccountRecords, toOneCustomerAccountRecord, } from './customer-account-ports.js';
|
|
9
|
+
/**
|
|
10
|
+
* The account lifecycle `customers` runs over this module's table
|
|
11
|
+
* (feature 075, Phase C).
|
|
12
|
+
*
|
|
13
|
+
* It ran it by loading and mutating the `CustomerAccount` entity in five of its
|
|
14
|
+
* own services and two route files — block, unblock, soft-delete, restore,
|
|
15
|
+
* anonymise, organisation assignment, group assignment, the standalone
|
|
16
|
+
* registration and the admin custom-field patch. Each is one method here, each
|
|
17
|
+
* one unit of work on this table alone, because a write port that took the
|
|
18
|
+
* caller's `EntityManager` would publish the coupling instead of removing it
|
|
19
|
+
* (D-78).
|
|
20
|
+
*
|
|
21
|
+
* **The policy stayed with the caller.** Whether the acting staff member may
|
|
22
|
+
* touch this customer, and whether the write would strand an organisation
|
|
23
|
+
* without an administrator, are `customers`' questions and are still asked
|
|
24
|
+
* there; the counting half of the second is
|
|
25
|
+
* `CustomerAccountReadPort.countByOrganizationRole`, which already existed.
|
|
26
|
+
*
|
|
27
|
+
* **The audit row came here** — one row, with the action string the caller used
|
|
28
|
+
* to write, so nothing an operator can see moved. Unlike
|
|
29
|
+
* `CustomerAccountMemberWriteService` the caller keeps no second row: there,
|
|
30
|
+
* "an operator edited this member on the organisation panel" and "this
|
|
31
|
+
* account's e-mail changed" are two facts; here "an operator blocked this
|
|
32
|
+
* customer" *is* the write, and recording it twice would be a defect rather
|
|
33
|
+
* than a pair.
|
|
34
|
+
*/
|
|
35
|
+
/** The name a scrubbed account's first and last name are replaced with. */
|
|
36
|
+
export const ANONYMIZED_CUSTOMER_NAME = 'Deleted customer';
|
|
37
|
+
/**
|
|
38
|
+
* The placeholder written over `password_hash`. It is deliberately not a hash
|
|
39
|
+
* of anything: the column is NOT NULL, and nothing may authenticate against a
|
|
40
|
+
* scrubbed account. `passwordSetAt` is cleared beside it (issue #222).
|
|
41
|
+
*/
|
|
42
|
+
export const ANONYMIZED_PASSWORD_HASH = 'anonymized-no-login';
|
|
43
|
+
function blockSnapshot(account) {
|
|
44
|
+
return {
|
|
45
|
+
blockedAt: account.blockedAt ? account.blockedAt.toISOString() : null,
|
|
46
|
+
blockSource: account.blockSource ?? null,
|
|
47
|
+
blockReason: account.blockReason ?? null,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export class CustomerAccountLifecycleWriteService {
|
|
51
|
+
emFactory;
|
|
52
|
+
auditLog;
|
|
53
|
+
twoFactorEnrolments;
|
|
54
|
+
personalOrganizations;
|
|
55
|
+
commandBus;
|
|
56
|
+
constructor(emFactory, auditLog,
|
|
57
|
+
/**
|
|
58
|
+
* `mfa`'s answer to which of these accounts hold a second factor. Every
|
|
59
|
+
* method here answers with a `CustomerAccountRecord`, and that record
|
|
60
|
+
* carries `twoFactorEnabled` — so the alternative to threading it is a
|
|
61
|
+
* record that says `false` about an enrolled buyer, which is the defect
|
|
62
|
+
* this parameter exists to remove.
|
|
63
|
+
*/
|
|
64
|
+
twoFactorEnrolments,
|
|
65
|
+
/**
|
|
66
|
+
* D-178 — `organizations`' co-transactional provisioning seam, held here
|
|
67
|
+
* because {@link createStandalone} writes the account **and** the individual's
|
|
68
|
+
* personal organisation in one transaction. See
|
|
69
|
+
* `organizations/ports/personal-organization-provision.ts` for the foreign
|
|
70
|
+
* key that makes that a requirement rather than a preference.
|
|
71
|
+
*/
|
|
72
|
+
personalOrganizations,
|
|
73
|
+
/**
|
|
74
|
+
* Only {@link setCustomFieldValues} needs it, because that one write is a
|
|
75
|
+
* read-modify-write and has to stay in one transaction. The method says so
|
|
76
|
+
* and refuses without it.
|
|
77
|
+
*/
|
|
78
|
+
commandBus) {
|
|
79
|
+
this.emFactory = emFactory;
|
|
80
|
+
this.auditLog = auditLog;
|
|
81
|
+
this.twoFactorEnrolments = twoFactorEnrolments;
|
|
82
|
+
this.personalOrganizations = personalOrganizations;
|
|
83
|
+
this.commandBus = commandBus;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Self-registration: the account row **and** the tenant that scopes it, in one
|
|
87
|
+
* transaction (D-178).
|
|
88
|
+
*
|
|
89
|
+
* It used to be two committed units of work — this method wrote
|
|
90
|
+
* `organizationId: null` and flushed, and `customers`' registration service
|
|
91
|
+
* then called `personalOrganizationPort.ensureForCustomerAccount`. A failure
|
|
92
|
+
* between them (the port disabled, the database blipping, the process killed)
|
|
93
|
+
* left a durable account with no organisation, no session for the visitor to
|
|
94
|
+
* retry from, and nothing anywhere that re-ran the provisioning. That is one
|
|
95
|
+
* of the three producers D-178 closed, and the column is `NOT NULL` now, so
|
|
96
|
+
* the old order would fail at the insert rather than leave a bad row.
|
|
97
|
+
*
|
|
98
|
+
* The account id is generated here rather than by the entity default because
|
|
99
|
+
* the organisation has to exist first — its `taxId` is derived from that id,
|
|
100
|
+
* which is what makes the provisioning re-runnable — and the account's foreign
|
|
101
|
+
* key has to point at it.
|
|
102
|
+
*/
|
|
103
|
+
async createStandalone(input) {
|
|
104
|
+
const em = this.emFactory();
|
|
105
|
+
// Folded, like the other write on this table and like every read of it.
|
|
106
|
+
// This entrance stored the address verbatim while
|
|
107
|
+
// `CustomerAccountMemberWriteService.create` folded it, so which spelling a
|
|
108
|
+
// row held depended on which door the buyer came through; the login read
|
|
109
|
+
// matched neither reliably. One normalisation now answers for all of them.
|
|
110
|
+
const email = normalizeEmailAddress(input.email);
|
|
111
|
+
const existing = await em.findOne(CustomerAccount, { email });
|
|
112
|
+
if (existing) {
|
|
113
|
+
throw new HttpError(409, ERROR_CODES.EMAIL_ALREADY_REGISTERED, 'An account with this email already exists.');
|
|
114
|
+
}
|
|
115
|
+
const customerAccountId = randomUUID();
|
|
116
|
+
const passwordHash = await hashPassword(input.password);
|
|
117
|
+
return em.transactional(async (tem) => {
|
|
118
|
+
const organization = await this.personalOrganizations.provisionFor(tem, {
|
|
119
|
+
customerAccountId,
|
|
120
|
+
email,
|
|
121
|
+
firstName: input.firstName,
|
|
122
|
+
lastName: input.lastName,
|
|
123
|
+
});
|
|
124
|
+
const account = tem.create(CustomerAccount, {
|
|
125
|
+
id: customerAccountId,
|
|
126
|
+
email,
|
|
127
|
+
passwordHash,
|
|
128
|
+
// Issue #222 — the visitor typed this password on the registration form.
|
|
129
|
+
passwordSetAt: new Date(),
|
|
130
|
+
firstName: input.firstName,
|
|
131
|
+
lastName: input.lastName,
|
|
132
|
+
organizationId: organization.id,
|
|
133
|
+
});
|
|
134
|
+
// Self-registration is pre-auth, so there is no ambient actor and the row
|
|
135
|
+
// is recorded with a null one — as it was before this write moved here.
|
|
136
|
+
recordAuditFromContext(this.auditLog, tem, {
|
|
137
|
+
action: 'customer_account.register_standalone',
|
|
138
|
+
objectType: 'customer_account',
|
|
139
|
+
objectId: account.id,
|
|
140
|
+
stateBefore: null,
|
|
141
|
+
stateAfter: { email: account.email },
|
|
142
|
+
});
|
|
143
|
+
try {
|
|
144
|
+
await tem.flush();
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
// The unique index is the real arbiter; the pre-check above only buys a
|
|
148
|
+
// better message for the common case. The port call is deliberately
|
|
149
|
+
// outside this `catch`: a `ModuleDisabledError` from it must reach the
|
|
150
|
+
// caller as the 503 it is, not as a duplicate-address 409.
|
|
151
|
+
if (err instanceof UniqueConstraintViolationException) {
|
|
152
|
+
throw new HttpError(409, ERROR_CODES.EMAIL_ALREADY_REGISTERED, 'An account with this email already exists.');
|
|
153
|
+
}
|
|
154
|
+
throw err;
|
|
155
|
+
}
|
|
156
|
+
// `false` is **derived** here rather than defaulted: the account was
|
|
157
|
+
// created microseconds ago inside this transaction, `mfa_enrolments` is
|
|
158
|
+
// keyed on its id and nobody has had a chance to enrol against it. Asking
|
|
159
|
+
// `mfa` would take a second pooled connection while this transaction
|
|
160
|
+
// holds one, on the pre-auth registration path, to be told the same
|
|
161
|
+
// thing.
|
|
162
|
+
return toCustomerAccountRecord(account, false);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
async block(customerAccountId, input) {
|
|
166
|
+
const em = this.emFactory();
|
|
167
|
+
const account = await this.#loadLive(em, customerAccountId);
|
|
168
|
+
if (account.blockedAt)
|
|
169
|
+
return toOneCustomerAccountRecord(this.twoFactorEnrolments, account);
|
|
170
|
+
const before = blockSnapshot(account);
|
|
171
|
+
account.blockedAt = new Date();
|
|
172
|
+
account.blockSource = 'staff';
|
|
173
|
+
account.blockedByAdminUserId = input.actorAdminUserId;
|
|
174
|
+
account.blockedByCustomerAccountId = null;
|
|
175
|
+
account.blockReason = input.reason ?? null;
|
|
176
|
+
await em.flush();
|
|
177
|
+
await this.#record('customer_account.blocked', account.id, input.actorAdminUserId, before, blockSnapshot(account), input.audit);
|
|
178
|
+
return toOneCustomerAccountRecord(this.twoFactorEnrolments, account);
|
|
179
|
+
}
|
|
180
|
+
async unblock(customerAccountId, input) {
|
|
181
|
+
const em = this.emFactory();
|
|
182
|
+
const account = await this.#loadLive(em, customerAccountId);
|
|
183
|
+
if (!account.blockedAt) {
|
|
184
|
+
return toOneCustomerAccountRecord(this.twoFactorEnrolments, account);
|
|
185
|
+
}
|
|
186
|
+
const before = blockSnapshot(account);
|
|
187
|
+
account.blockedAt = null;
|
|
188
|
+
account.blockSource = null;
|
|
189
|
+
account.blockedByAdminUserId = null;
|
|
190
|
+
account.blockedByCustomerAccountId = null;
|
|
191
|
+
account.blockReason = null;
|
|
192
|
+
await em.flush();
|
|
193
|
+
await this.#record('customer_account.unblocked', account.id, input.actorAdminUserId, before, blockSnapshot(account), input.audit);
|
|
194
|
+
return toOneCustomerAccountRecord(this.twoFactorEnrolments, account);
|
|
195
|
+
}
|
|
196
|
+
async softDelete(customerAccountId, input) {
|
|
197
|
+
const em = this.emFactory();
|
|
198
|
+
const account = await this.#load(em, customerAccountId);
|
|
199
|
+
if (account.deletedAt) {
|
|
200
|
+
throw new HttpError(409, ERROR_CODES.CUSTOMER_ALREADY_DELETED, 'Customer is already deleted.');
|
|
201
|
+
}
|
|
202
|
+
account.deletedAt = new Date();
|
|
203
|
+
account.deletionRequestedByAdminUserId = input.actorAdminUserId;
|
|
204
|
+
await em.flush();
|
|
205
|
+
await this.#record('customer_account.deleted', account.id, input.actorAdminUserId, { deletedAt: null }, { deletedAt: account.deletedAt.toISOString() });
|
|
206
|
+
return toOneCustomerAccountRecord(this.twoFactorEnrolments, account);
|
|
207
|
+
}
|
|
208
|
+
async restore(customerAccountId, input) {
|
|
209
|
+
const em = this.emFactory();
|
|
210
|
+
const account = await this.#load(em, customerAccountId);
|
|
211
|
+
if (!account.deletedAt) {
|
|
212
|
+
throw new HttpError(409, ERROR_CODES.CUSTOMER_NOT_DELETED, 'Customer is not deleted.');
|
|
213
|
+
}
|
|
214
|
+
if (account.anonymizedAt) {
|
|
215
|
+
throw new HttpError(409, ERROR_CODES.CUSTOMER_RESTORE_WINDOW_ELAPSED, 'The restore window has elapsed; this account has been permanently anonymized.');
|
|
216
|
+
}
|
|
217
|
+
account.deletedAt = null;
|
|
218
|
+
account.deletionRequestedByAdminUserId = null;
|
|
219
|
+
await em.flush();
|
|
220
|
+
await this.#record('customer_account.restored', account.id, input.actorAdminUserId, { deletedAt: 'set' }, { deletedAt: null });
|
|
221
|
+
return toOneCustomerAccountRecord(this.twoFactorEnrolments, account);
|
|
222
|
+
}
|
|
223
|
+
async setOrganization(customerAccountId, organizationId, input) {
|
|
224
|
+
return this.#writeOrganization('customer_account.organization_assigned', customerAccountId, organizationId, input);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* D-178 — an operator detaching a member from a company.
|
|
228
|
+
*
|
|
229
|
+
* The write is byte-identical to {@link setOrganization}'s; only the audit
|
|
230
|
+
* verb differs, and it is the verb this operation has always recorded. What
|
|
231
|
+
* changed is what it writes: `organization_id = NULL` until D-178, which left
|
|
232
|
+
* an account that could not place an order, submit an RFQ or read an address —
|
|
233
|
+
* and which the column now refuses outright.
|
|
234
|
+
*
|
|
235
|
+
* The destination is the account's own personal organisation, resolved by the
|
|
236
|
+
* caller through `organizations`. This module does not resolve it, because
|
|
237
|
+
* that would make the account table's owner responsible for the tenancy
|
|
238
|
+
* policy of the module it points at.
|
|
239
|
+
*/
|
|
240
|
+
async detachToPersonalOrganization(customerAccountId, personalOrganizationId, input) {
|
|
241
|
+
return this.#writeOrganization('customer_account.organization_unassigned', customerAccountId, personalOrganizationId, input);
|
|
242
|
+
}
|
|
243
|
+
async #writeOrganization(action, customerAccountId, organizationId, input) {
|
|
244
|
+
const em = this.emFactory();
|
|
245
|
+
const account = await this.#loadLive(em, customerAccountId);
|
|
246
|
+
const before = { organizationId: account.organizationId };
|
|
247
|
+
account.organizationId = organizationId;
|
|
248
|
+
await em.flush();
|
|
249
|
+
await this.#record(action, account.id, input.actorAdminUserId, before, { organizationId });
|
|
250
|
+
return toOneCustomerAccountRecord(this.twoFactorEnrolments, account);
|
|
251
|
+
}
|
|
252
|
+
async setCustomerGroup(customerAccountId, customerGroupId, input) {
|
|
253
|
+
const em = this.emFactory();
|
|
254
|
+
const account = await this.#loadLive(em, customerAccountId);
|
|
255
|
+
const before = { customerGroupId: account.customerGroupId ?? null };
|
|
256
|
+
account.customerGroupId = customerGroupId;
|
|
257
|
+
await em.flush();
|
|
258
|
+
await this.#record('customer_account.group_assigned', account.id, input.actorAdminUserId, before, { customerGroupId });
|
|
259
|
+
return toOneCustomerAccountRecord(this.twoFactorEnrolments, account);
|
|
260
|
+
}
|
|
261
|
+
async setCustomFieldValues(customerAccountId, merge) {
|
|
262
|
+
const commandBus = this.commandBus;
|
|
263
|
+
if (!commandBus) {
|
|
264
|
+
throw new Error('CustomerAccountLifecycleWriteService: the custom-field write runs a Command; construct the service with a CommandBus.');
|
|
265
|
+
}
|
|
266
|
+
// Asked **before** the Command opens its transaction. The answer belongs to
|
|
267
|
+
// another module's table and is only needed to shape the response, so
|
|
268
|
+
// reading it inside `run` would put a foreign read on the caller's
|
|
269
|
+
// transaction for no benefit.
|
|
270
|
+
const enrolled = await this.twoFactorEnrolments([customerAccountId]);
|
|
271
|
+
const command = {
|
|
272
|
+
action: 'customer.custom_fields.update',
|
|
273
|
+
objectType: 'customer_account',
|
|
274
|
+
objectId: customerAccountId,
|
|
275
|
+
capture: async ({ em }) => {
|
|
276
|
+
const account = await em.findOne(CustomerAccount, { id: customerAccountId });
|
|
277
|
+
return account ? { customFieldValues: account.customFieldValues ?? {} } : null;
|
|
278
|
+
},
|
|
279
|
+
run: async ({ em }) => {
|
|
280
|
+
const account = await em.findOne(CustomerAccount, { id: customerAccountId });
|
|
281
|
+
if (!account) {
|
|
282
|
+
throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'Customer not found.');
|
|
283
|
+
}
|
|
284
|
+
account.customFieldValues = await merge(account.customFieldValues ?? {});
|
|
285
|
+
return {
|
|
286
|
+
result: toCustomerAccountRecord(account, enrolled.has(account.id)),
|
|
287
|
+
after: { customFieldValues: account.customFieldValues },
|
|
288
|
+
};
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
return commandBus.run(command);
|
|
292
|
+
}
|
|
293
|
+
async listDueForAnonymization(cutoff) {
|
|
294
|
+
const accounts = await this.emFactory().find(CustomerAccount, {
|
|
295
|
+
deletedAt: { $ne: null, $lt: cutoff },
|
|
296
|
+
anonymizedAt: null,
|
|
297
|
+
});
|
|
298
|
+
return toCustomerAccountRecords(this.twoFactorEnrolments, accounts);
|
|
299
|
+
}
|
|
300
|
+
async anonymize(customerAccountId) {
|
|
301
|
+
const em = this.emFactory();
|
|
302
|
+
const account = await this.#load(em, customerAccountId);
|
|
303
|
+
if (account.anonymizedAt) {
|
|
304
|
+
return toOneCustomerAccountRecord(this.twoFactorEnrolments, account);
|
|
305
|
+
}
|
|
306
|
+
const before = { email: account.email };
|
|
307
|
+
account.email = `deleted+${account.id}@anonymized.invalid`;
|
|
308
|
+
account.firstName = ANONYMIZED_CUSTOMER_NAME;
|
|
309
|
+
account.lastName = ANONYMIZED_CUSTOMER_NAME;
|
|
310
|
+
account.passwordHash = ANONYMIZED_PASSWORD_HASH;
|
|
311
|
+
// Issue #222 — the scrubbed hash is a placeholder, not a credential, so the
|
|
312
|
+
// account has no password on record any more. Leaving the stamp would say
|
|
313
|
+
// the opposite of what the scrub just did.
|
|
314
|
+
account.passwordSetAt = null;
|
|
315
|
+
account.twoFactorConfirmedAt = null;
|
|
316
|
+
account.anonymizedAt = new Date();
|
|
317
|
+
await em.flush();
|
|
318
|
+
// The sweep runs on a timer, so there is no operator to attribute it to.
|
|
319
|
+
await this.#record('customer_account.anonymized', account.id, null, before, {
|
|
320
|
+
anonymizedAt: account.anonymizedAt.toISOString(),
|
|
321
|
+
});
|
|
322
|
+
return toOneCustomerAccountRecord(this.twoFactorEnrolments, account);
|
|
323
|
+
}
|
|
324
|
+
async #record(action, objectId, actorAdminUserId, stateBefore, stateAfter, meta) {
|
|
325
|
+
await this.auditLog.record({
|
|
326
|
+
actorAdminUserId,
|
|
327
|
+
action,
|
|
328
|
+
objectType: 'customer_account',
|
|
329
|
+
objectId,
|
|
330
|
+
stateBefore,
|
|
331
|
+
stateAfter,
|
|
332
|
+
ipAddress: meta?.ipAddress ?? null,
|
|
333
|
+
userAgent: meta?.userAgent ?? null,
|
|
334
|
+
requestId: meta?.requestId ?? null,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
async #load(em, id) {
|
|
338
|
+
const account = await em.findOne(CustomerAccount, { id });
|
|
339
|
+
if (!account) {
|
|
340
|
+
throw new HttpError(404, ERROR_CODES.CUSTOMER_NOT_FOUND, 'Customer not found.');
|
|
341
|
+
}
|
|
342
|
+
return account;
|
|
343
|
+
}
|
|
344
|
+
async #loadLive(em, id) {
|
|
345
|
+
const account = await em.findOne(CustomerAccount, { id, deletedAt: null });
|
|
346
|
+
if (!account) {
|
|
347
|
+
throw new HttpError(404, ERROR_CODES.CUSTOMER_NOT_FOUND, 'Customer not found.');
|
|
348
|
+
}
|
|
349
|
+
return account;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* The admin customer list (feature 040, US5).
|
|
354
|
+
*
|
|
355
|
+
* `customers` built the whole query itself — the lifecycle filter, the
|
|
356
|
+
* sales-rep visibility scope, the free-text predicate and the page — against
|
|
357
|
+
* this module's entity. The **shape** of the answer is that module's screen and
|
|
358
|
+
* the **statement** is this module's table, so the criteria cross as plain data
|
|
359
|
+
* and the SQL stays here. It has to be one statement: narrowing a page after
|
|
360
|
+
* the fact returns fewer rows than the caller asked for, and paging after
|
|
361
|
+
* narrowing reads the whole table.
|
|
362
|
+
*/
|
|
363
|
+
export class CustomerAccountAdminSearchService {
|
|
364
|
+
emFactory;
|
|
365
|
+
twoFactorEnrolments;
|
|
366
|
+
constructor(emFactory, twoFactorEnrolments) {
|
|
367
|
+
this.emFactory = emFactory;
|
|
368
|
+
this.twoFactorEnrolments = twoFactorEnrolments;
|
|
369
|
+
}
|
|
370
|
+
async search(criteria) {
|
|
371
|
+
const and = [];
|
|
372
|
+
if (criteria.status === 'blocked') {
|
|
373
|
+
and.push({ blockedAt: { $ne: null }, deletedAt: null });
|
|
374
|
+
}
|
|
375
|
+
else if (criteria.status === 'deleted') {
|
|
376
|
+
and.push({ deletedAt: { $ne: null } });
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
// The default "active" view excludes deleted accounts.
|
|
380
|
+
and.push({ deletedAt: null });
|
|
381
|
+
}
|
|
382
|
+
if (criteria.organizationId)
|
|
383
|
+
and.push({ organizationId: criteria.organizationId });
|
|
384
|
+
if (criteria.customerGroupId)
|
|
385
|
+
and.push({ customerGroupId: criteria.customerGroupId });
|
|
386
|
+
// `null` is the platform administrator's unscoped read. Anything else is a
|
|
387
|
+
// sales rep's territory.
|
|
388
|
+
//
|
|
389
|
+
// D-178 removed an `$or` arm here that also matched `organizationId: null`.
|
|
390
|
+
// Its comment said a standalone account belonged to nobody's territory and
|
|
391
|
+
// the predicate put it in **everybody's**: every sales representative's
|
|
392
|
+
// customer list contained every null-organisation account on the platform,
|
|
393
|
+
// and the population grew on every federated sign-in. That was a live
|
|
394
|
+
// cross-territory disclosure, not an optimisation, and it is gone rather
|
|
395
|
+
// than left unreachable — a predicate with no population still reads as
|
|
396
|
+
// policy to whoever finds it next.
|
|
397
|
+
if (criteria.allowedOrganizationIds !== null) {
|
|
398
|
+
and.push({ organizationId: { $in: [...criteria.allowedOrganizationIds] } });
|
|
399
|
+
}
|
|
400
|
+
const q = criteria.q?.trim();
|
|
401
|
+
if (q) {
|
|
402
|
+
const like = `%${q}%`;
|
|
403
|
+
and.push({
|
|
404
|
+
$or: [
|
|
405
|
+
{ email: { $ilike: like } },
|
|
406
|
+
{ firstName: { $ilike: like } },
|
|
407
|
+
{ lastName: { $ilike: like } },
|
|
408
|
+
],
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
const where = and.length ? { $and: and } : {};
|
|
412
|
+
const [rows, total] = await this.emFactory().findAndCount(CustomerAccount, where, {
|
|
413
|
+
orderBy: { createdAt: 'desc' },
|
|
414
|
+
limit: criteria.pageSize,
|
|
415
|
+
offset: (criteria.page - 1) * criteria.pageSize,
|
|
416
|
+
});
|
|
417
|
+
return { rows: await toCustomerAccountRecords(this.twoFactorEnrolments, rows), total };
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
//# sourceMappingURL=customer-account-lifecycle-ports.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"customer-account-lifecycle-ports.js","sourceRoot":"","sources":["../../../src/backend/services/customer-account-lifecycle-ports.ts"],"names":[],"mappings":"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"}
|